【ESP32 C++教程】Unit10-1:音频播放

本小节介绍音频的基础知识、音频开发框架和AudioCodec的简介,用一个音频播放示例来说明音频管道的使用。

音频

音频的数字化处理是计算机领域非常重要的一门学科,包含许多算法、协议、软件和硬件等。近来人工智能的兴起,也让嵌入式领域的音频处理焕发出新的活力,典型场景如陪护型玩具、大模型对话机器人、微型AI代理等。

在学习本小节前,我们先来了解下音频的基础知识

1.音频是什么

音频就是声波,如下图所示:
【ESP32 C++教程】Unit10-1:音频播放
比如两个人对话,由嘴巴发出声波,通过空气传递,然后耳朵接收声波。
不难看出,音频是模拟信号,它具有时间性。那么如何在计算机上存储它呢?

2.音频数字化

在计算机领域,要存储模拟量,通常做法是存储采样值,当采样率越高,占用存储空间就越多,数据就越完整,反之占用存储空间越少,数据细节丢失就越多,在项目中需要均衡考虑。
【ESP32 C++教程】Unit10-1:音频播放
在存储音乐上,要想得到高的音质,一般会选高采样率,如192KHz,而普通录音,用44.1KHz就足够了。
音频数据的数字化也叫A/D转换,有一些数字麦克风内部自带ADC硬件,可直接输出音频数字信号。

3.音频编码

要想高品质和低存储两者兼具,那么就需要使用音频编码了,编码实际上是一种数据组式和压缩方式,可有效降低存储空间。
音频数据编码一般会将数据分成若干个小的块数据(帧)来打包和压缩,当需要流式处理时,就可以边传输边解包处理。
常见的音频编码格式有MP3、ACC、Ogg等。

4.音频输出

要将数字化的音频数据输出,如用喇叭播放出来,需要数字到模拟的转换处理,即D/A转换,一般由DAC硬件实现。

I2S协议和DAC放大模块

I2S(Inter-IC Sound)协议是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,它主要用于连接数字音频编解码器、微控制器、数字信号处理器等设备,确保音频数据能够准确、高效地在不同芯片或模块间传输。

本例使用的I2S DAC模块,是一款高效、低噪声、高集成度的 D 类音频功放模块,特别适合对功耗和抗干扰要求高的便携式音频设备。
【ESP32 C++教程】Unit10-1:音频播放
I2S音频放大器模块

音频开发框架

本开发框架提供一个基于管道的音频开发框架,音频数据从流入管道开始、经过解码、过滤、编码,最后流出管道。如下图:
【ESP32 C++教程】Unit10-1:音频播放
输入:可以是文件、网络流、麦克风等;
解码:可选的,若输入的是编码数据,如MP3,则需要用MP3解码器处理;
过滤:可选的,可在此环节加音效,改变输出速率,混音等处理;
编码:可选的,若要保存文件或网络流,可在此环节进行编码处理;
输出:可以是文件、网络流、喇叭等;

AudioCodec类

AudioCodec是封装音频芯片驱动的基类,它是Board类的成员之一,在应用侧可通过Board类实例来获取AudioCodec实例。
【ESP32 C++教程】Unit10-1:音频播放
音频驱动类核心功能就是读/写音频数据,本开发框架用一套类体系来设计实现,如上图所示
AudioI2sCodec:为所有使用I2S协议的音频驱动类基类
AudioI2sSimplex:为包含I2S输入或输出的简单模块驱动,如麦克风、音频放大(DAC)
AudioI2sDuplex:为包含I2S输入和输出的复杂模块驱动
AudioI2sComplex:组合I2S输入(麦克风)和I2S输出(DAC放大)的模块驱动
AudioEs8388Codec:使用Es8388音频芯片的驱动

AudioPipe类

AudioPipe类是音频开发框架提供的音频管道类,作用就是构建音频数据管道处理流程。
AudioPipe类主程序如下(src/framework/audio/audio_pipe.h):

void AudioPipe::Execute()
{
    // 初始化
    bool ret = input_->Init();
    if (!ret)
    {
        last_error_ = "audio input init fail.";
        Log::Error(TAG, last_error_.c_str());
        return;
    }

    audio_config_t audio_cfg = input_->audio_config();
    output_->SetAudioConfig(audio_cfg);

    ret = output_->Init();
    if (!ret)
    {
        last_error_ = "audio output init fail.";
        Log::Error(TAG, last_error_.c_str());
        return;
    }

    input_->SetAudioListener(audio_listener_);
    output_->SetAudioListener(audio_listener_);

    if (pipe_listener_) 
    {
        Log::Info(TAG, "Pipe inited.");
        pipe_listener_(PipeAction::Inited);
    }

    // 处理数据
    // AudioInput使用Handle方法拉取数据;
    // AudioOutput使用WriteXxx方法推送数据;
    while (running_)
    {
        if (input_->isEOF()) 
        {
            Log::Info(TAG, "Input EOF.");
            break;
        }

        //--- STEP1:数据输入,从输入端取到原始数据
        sample_data_t input_data;
        if (!input_->Handle(input_data))
        { // 无数据
            Log::Info(TAG, "no data read.");
            continue;
        }

        audio_listener_->OnDataInput(input_data);

        sample_data_t output_data = input_data;

        // --- STEP2:数据加工,对数据进行处理,如变声,混音等
        if (!filter_set_.empty())
        {
            // 代码略...
        }

        audio_listener_->OnDataOutput(output_data);

        //--- STEP3:数据输出
        output_->WriteSamples(output_data);
    }

    Log::Info(TAG, "handle end.");
}

本程序在一个独立任务中执行,通过一个循环体来持续处理音频数据。
更多细节见参阅源码。

示例1:音频文件播放

本示例使用I2S DAC放大模块来播放SD存储卡的MP3音频文件。

从 https://gitee.com/billyzh/esp32-cpp-lesson 下载本教程的源码到本地硬盘文件夹,如d:\esp32-cpp-lesson
在VSCode中,选择【文件】->【打开文件夹...】选择上一步保存的文件夹打开

打开项目后,选择config.h文件,修改第10行为
#define APP_LESSON101_A 1

打开unit10-lesson101a/app_config.h文件,设置应用的相关配置
#define CONFIG_USE_FS 1 //启用文件系统模块
#define CONFIG_USE_AUDIO 1 //启用音频模块
#define CONFIG_USE_DISPLAY 1 //启用显示模块
#define CONFIG_USE_TFT_ESPI 1 //使用TFT_eSPI显示驱动


打开unit10-lesson101a/board_config.h文件,设置显示硬件的相关配置
#define SD_MOSI_PIN GPIO_NUM_17 //SD存储模块引脚
#define SD_MISO_PIN GPIO_NUM_16
#define SD_CLK_PIN GPIO_NUM_15
#define SD_CS_PIN GPIO_NUM_14
#define SPK_BCLK_PIN GPIO_NUM_27 //DAC模块引脚
#define SPK_WS_PIN GPIO_NUM_26
#define SPK_DOUT_PIN GPIO_NUM_25
#define DISPLAY_WIDTH 240 //屏幕宽度像素
#define DISPLAY_HEIGHT 320 //屏幕高度像素
#define DISPLAY_INVERT_COLOR true //是否反转颜色


创建FileSystem实例和Display实例,同前一小节,代码见(unit10-lesson101a/my_board.cpp)


创建AudioCodec实例,代码如下(unit10-lesson101a/my_board.cpp)

void MyBoard::InitAudioCodec() {
    Log::Info(TAG, "initial audio codec.");
    audio_codec_ = new AudioI2sSimplexSpeaker(SPK_BCLK_PIN, SPK_WS_PIN, SPK_DOUT_PIN);
}

这里使用内置的Speaker驱动类来构造AudioCodec实例。


音频播放代码(unit10-lesson101a/my_application.cpp)

void MyApplication::OnInit() {
    Display *display = Board::GetInstance().GetDisplay();
    display->Rotate(1);

    FileSystem *fsys = Board::GetInstance().GetFileSystem();
    std::string path = "/test.mp3";
    //std::string path = "/test.wav";
    if (!fsys->ExistsFile(path.c_str())) {
        std::string msg = "file " + path + " not found.";
        Log::Warn(TAG, msg.c_str());
        display->GetWindow()->SetText(1, msg);
        return;
    }

    display->GetWindow()->SetText(1, "Play file " + path);

    // 音频处理流
    // FileSource(文件源) -> Decoder(解码) -> I2sOutput(输出到喇叭)

    // 文件源
    AudioFileSource *file_source = new AudioFileSource(fsys, path);

    // 解码输入
    AudioDecodeInput *input = new AudioDecodeInput(file_source, "mp3");
    //AudioDecodeInput *input = new AudioDecodeInput(file_source, "wav");

    // I2S输出
    AudioCodec *audio_codec = Board::GetInstance().GetAudioCodec();
    AudioI2sOutput *output = new AudioI2sOutput(audio_codec);

    // 音频管道
    pipe_ = new AudioPipe();

    pipe_->SetPipeListener([](PipeAction action){
        AudioCodec *codec = Board::GetInstance().GetAudioCodec();
        if (action==PipeAction::Inited)
        {
            codec->SetOutputVolume(30); //0-100
            codec->Start();
        }
        else if (action==PipeAction::Ended)
        {
            codec->EnableOutput(false);
        }
    });

    // 启动管道
    pipe_->Start(input, output);
}

程序解读
1.先检查音频文件test.mp3是否存在;
2.创建音频输入AudioInput的实例,本例为文件输入,需要解码,使用AudioDecodeInput类;
3.创建音频输出AudioOutput的实例,本例为DAC输出,使用AudioI2sOuput类,并关联AudioCodec实例;
4.创建音频管道实例,设置音频管道监听程序,音频管道处理中会在各处理节点进行回调;
5.启动音频管道。


下载一首MP3,复制到SD卡的根文件下,并命名为test.mp3

编译项目并上传开发板检验

【ESP32 C++教程】Unit10-1:音频播放

示例2:网络音频流播放

本示例使用I2S DAC放大模块来播放网络流MP3音频文件。

Lesson101b为播放网络音频流的示例项目,除了音频输入源改为AudioHttpStreamInput类外,其它部分与示例1基本一致,
这里就不细述了,请自行查阅项目源码。

- 本文由用户 老张 发布,文中观点仅代表作者本人,不代表本站立场。
- 如需转载,请联系作者;如有侵权,请联系本站处理。

04-02   阅读(17)   评论(0)
 标签: 编程 ESP32 ESP32-ArduinoFx I2S

涨知识
LTE Cat.1

Cat.1技术是LTE(Long-Term Evolution)技术的一种调制及编码技术,可以提供相对较高的数据传输速率,同时又具有低功耗、低成本的特点,可以为物联网设备的连接提供更好的解决方案。

评论:
相关文章
MimiClaw应用与开发教程1:部署和测试

MimiClaw‌ 是一款基于 ‌ESP32-S3‌ 芯片的超轻量级AI助手,适合嵌入式AI与物联网开发者快速部署本地化AI代理。本系列教程基于MimiClaw的Arduino移植版本进行讲解,小节主要讲解部署和测试。


ESP32扫描wifi 热点列表

就像我们用手机打开WiFi功能后可以浏览附近的可用WiFi。要将手机连接到热点,通常需要打开Wi-Fi设置应用程序,列出可用的网络,然后选择所需的热点。然后输入密码(或不输入密码),可以使用ESP32进行相同的操作。


MimiClaw 配置飞书机器人和添加硬件控制技能

本文本介绍配置飞书机器人为MimiClaw的一个输入/输出端,和添加一个控制WS2812与LED的控制技能。


ESP32-S3 部署 MimicLaw 完整教程:从零到成功调用 DeepSeek

一块 30 块钱的开发板 + 一个大模型 API,就能做出可以听懂人话的智能硬件。 本文记录完整安装过程和踩坑经验,确保你跟着做就能跑通。


MimiClaw 架构全解析,把 “智能龙虾” 跑在 ESP32 上

本文将从手绘架构图入手,逐层拆解 MimiClaw 的分层设计、核心模块、数据流转与底层实现,带你解剖这只“智能虾”的技术骨架,看懂在 C 语言加持下,AI 智能体如何以可穿戴设备的形态,在你身边稳稳运行、离线服务、主动响应。


如何用 platform.local.txt 深度定制 ESP32 编译流程?

本文介绍如何在不脱离 ArduinoIDE 可视化开发的前提下,通过一个名为 platform.local.txt 的小文件,实现对 ESP32 编译流程的精准控制。


优化Arduino-ESP32程序体积

本文将系统分析程序体积增长的五大根源,并提供经过验证的优化方案,帮助减小固件大小。


开发ESP32大模型AI语音助手-从软件到硬件

本文所DIY的语音助手设备端使用的是MicroPython、服务端是Python,对于很多开发者来说MicroPython入门没难度。


【ESP32 C++教程】Unit10-2:音频录制

本小节使用音频开发框架实现一个音频录制到文件的示例。


ESP32 I2S 接口深度解析:从时序、格式到 ESP-IDF 驱动实战

I2S协议通过BCLK、LRCLK和DATA三线精准传输音频数据,但时序边沿、帧格式、时钟源等细节常引发噪声或断连。本文详解ESP32的I2S实现,从协议原理到ESP-IDF v5.x代码配置,助你避开常见陷阱,确保音频稳定传输。