2. 播放内置WAV音频文件
任务目标
在ESP32-S3上通过I2S接口播放存储在Flash中的WAV音频文件。本案例使用乐鑫官方esp32-s3-eye开发板(板载ES8311音频编解码器),演示从Flash读取WAV文件数据并经由I2S输出音频的完整最小实现。
环境与依赖
- ESP-IDF v5.x(需启用CONFIG_IDF_TARGET_ESP32S3=y)
- 开发板:ESP32-S3-EYE(I2S0连接ES8311,使用内置PA)
- 音频文件:将8kHz/16bit/单声道WAV文件放入main目录下,命名为audio.wav,通过组件管理器或idf.py create-project后手动添加
实现原理
ESP32-S3的I2S外设支持从Flash直接读取音频数据。本案例将WAV文件作为嵌入式二进制资源编译进固件,运行时通过esp_i2s驱动将PCM数据连续写入I2S总线。ES8311编解码器配置为从模式,I2S提供主时钟与位时钟。
关键完整代码
// main.c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "esp_heap_caps.h"
// 嵌入WAV文件(需在CMakeLists.txt中添加:target_add_binary_data(main "audio.wav" BINARY))
extern const uint8_t audio_start[] asm("_binary_audio_wav_start");
extern const uint8_t audio_end[] asm("_binary_audio_wav_end");
#define I2S_NUM I2S_NUM_0
#define SAMPLE_RATE 8000
#define SAMPLE_BITS 16
#define DMA_BUF_SIZE 1024
// ESP32-S3-EYE ES8311引脚
#define I2S_SCLK 41
#define I2S_LRCK 42
#define I2S_DOUT 15
#define I2S_MCLK 16
#define PA_GPIO 48
static void i2s_init(void) {
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = SAMPLE_RATE,
.clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_16BIT,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_LEFT,
.ws_pol = false,
.bit_shift = true
},
.gpio_cfg = {
.mclk = I2S_MCLK,
.bclk = I2S_SCLK,
.ws = I2S_LRCK,
.dout = I2S_DOUT,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = DMA_BUF_SIZE,
.auto_clear = true
};
i2s_chan_handle_t tx_chan;
i2s_new_channel(&chan_cfg, &tx_chan, NULL);
i2s_channel_init_std_mode(tx_chan, &std_cfg);
i2s_channel_enable(tx_chan);
// 使能PA功放
gpio_config_t pa_cfg = {
.pin_bit_mask = (1ULL << PA_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&pa_cfg);
gpio_set_level(PA_GPIO, 1);
}
void app_main(void) {
i2s_init();
size_t wav_size = audio_end - audio_start;
// 跳过WAV文件头(44字节标准PCM头)
const uint8_t *pcm_data = audio_start + 44;
size_t pcm_size = wav_size - 44;
ESP_LOGI("WAV", "PCM data size: %d bytes", pcm_size);
i2s_chan_handle_t tx_chan;
i2s_channel_get_handle(I2S_NUM, &tx_chan);
size_t bytes_written = 0;
while (bytes_written < pcm_size) {
size_t chunk = (pcm_size - bytes_written) > DMA_BUF_SIZE * 2 ?
DMA_BUF_SIZE * 2 : (pcm_size - bytes_written);
size_t written = 0;
i2s_channel_write(tx_chan, pcm_data + bytes_written, chunk, &written, portMAX_DELAY);
bytes_written += written;
}
// 播放完成后等待DMA排空
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI("WAV", "Playback finished");
}
CMakeLists.txt 配置
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
target_add_binary_data(${COMPONENT_LIB} "audio.wav" BINARY)
运行与验证
- 准备音频:使用ffmpeg或Audacity生成8kHz/16bit/单声道WAV文件,时长建议3~5秒,文件大小控制在100KB以内。
- 将文件命名为audio.wav放入main目录。
- 编译烧录:idf.py build flash monitor
- 验证标准:
- 串口输出日志包含PCM data size: xxx bytes,且数值与WAV文件数据段大小一致(文件总字节数减44)。
- 日志出现Playback finished后,开发板音频输出插孔(或板载喇叭)能听到清晰、无爆音的音频内容。
- 若听不到声音,检查PA引脚电平是否为高(可用万用表测GPIO48对地电压约3.3V),以及ES8311的I2C配置是否与板卡匹配。
常见问题
- 播放速度异常:确认WAV文件采样率与代码中SAMPLE_RATE一致。若WAV为44100Hz而代码配置8000Hz,播放会变慢约5.5倍。
- 只有噪声:检查WAV是否为16bit线性PCM格式,不支持压缩格式(如ADPCM)。
- 编译报错找不到audio.wav:确认文件确实位于main目录且文件名拼写完全一致(区分大小写)。