ESP32-S3-CAM+MAX98357播放pcm格式的wav文件(本地文件和网络文件)
以下是适配16000Hz单声道PCM编码WAV文件的完整代码,包含详细注释和调试信息,可直接用于ESP32-S3-CAM+MAX98357方案:
完整代码(板载卡)
cpp
#include <WiFi.h>
#include <HTTPClient.h>
#include <driver/i2s.h>
// -------------------------- 配置参数(需根据实际情况修改)--------------------------
// WiFi信息
const char* ssid = "你的WiFi名称"; // 替换为你的WiFi名称
const char* password = "你的WiFi密码"; // 替换为你的WiFi密码
// 网络WAV音频地址(必须是16000Hz单声道16位PCM编码)
const char* audioUrl = "http://example.com/11.wav"; // 替换为实际音频URL
// TF卡文件路径(11.wav需放在TF卡根目录)
//const char* wavFilePath = "/11.wav"; // 根目录下的11.wav
const char* MOUNT_POINT="/SDCARD";
//根据板子引脚来定义
#define SD_PIN_CLK 39
#define SD_PIN_CMD 38
#define SD_PIN_D0 40
// I2S硬件引脚(与MAX98357连接)
#define I2S_PORT I2S_NUM_0 // 使用ESP32-S3板子上的I2S端口0,不涉及物理连接
#define I2S_BCLK_PIN 12 // 时钟线(BCLK)
#define I2S_LRCLK_PIN 13 // 左右声道时钟(LRC)
#define I2S_DATA_PIN 14 // 数据线(DIN)
// -----------------------------------------------------------------------------------
void setup() {
// 初始化串口(用于调试信息输出)
Serial.begin(115200);
while (!Serial) delay(10); // 等待串口准备就绪
Serial.println("ESP32-S3-CAM 音频播放测试");
// 连接WiFi
Serial.printf("连接WiFi: %s ...", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi连接成功!IP地址: " + WiFi.localIP().toString());
// 初始化I2S(适配16000Hz单声道)
if (initI2S() != ESP_OK) {
Serial.println("I2S初始化失败!请检查引脚连接");
while (1) delay(1000); // 初始化失败则卡死
}
Serial.println("I2S初始化完成");
// 开始播放网络WAV音频
Serial.printf("开始播放音频: %s\n", audioUrl);
playNetworkWav(audioUrl);
// 播放TF卡上的11.wav
// playTFcardWav(wavFilePath);
Serial.println("音频播放结束");
}
void loop() {
// 播放完成后无需重复执行,若需循环播放可在此处调用playNetworkWav()
}
// 初始化I2S配置(适配16000Hz单声道16位PCM)
esp_err_t initI2S() {
// 配置I2S参数
i2s_config_t i2sConfig = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX, // 主机模式+发送模式
.sample_rate = 16000, // 采样率:16000Hz(与音频文件匹配)
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 位深:16位
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道(仅左声道输出)
.communication_format = I2S_COMM_FORMAT_STAND_I2S, // 标准I2S格式
.dma_buf_count = 8, // DMA缓冲区数量(增大可减少卡顿)
.dma_buf_len = 512, // 每个缓冲区大小(字节)
.use_apll = false, // 不使用APLL时钟(简化配置)
.tx_desc_auto_clear = true, // 自动清理发送描述符
.fixed_mclk = 0 // 不固定MCLK时钟
};
// 安装I2S驱动
esp_err_t err = i2s_driver_install(I2S_PORT, &i2sConfig, 0, NULL);
if (err != ESP_OK) return err;
// 配置I2S引脚映射
i2s_pin_config_t pinConfig = {
.bck_io_num = I2S_BCLK_PIN, // 时钟线引脚
.ws_io_num = I2S_LRCLK_PIN, // 左右声道时钟引脚
.data_out_num = I2S_DATA_PIN, // 数据线引脚
.data_in_num = I2S_PIN_NO_CHANGE // 不使用输入
};
// 设置I2S引脚
err = i2s_set_pin(I2S_PORT, &pinConfig);
if (err != ESP_OK) {
i2s_driver_uninstall(I2S_PORT); // 引脚配置失败则卸载驱动
return err;
}
// 启动I2S发送
i2s_start(I2S_PORT);
return ESP_OK;
}
// 从网络获取WAV文件并通过I2S播放
void playNetworkWav(const char* url) {
HTTPClient http;
// 启动HTTP连接
if (!http.begin(url)) {
Serial.println("HTTP初始化失败");
return;
}
// 发送HTTP GET请求
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("HTTP请求失败,错误码: %d\n", httpCode);
http.end();
return;
}
// 获取音频数据流
WiFiClient* stream = http.getStreamPtr();
if (!stream) {
Serial.println("无法获取音频流");
http.end();
return;
}
// 跳过WAV文件头部(标准PCM WAV头部为44字节)
uint8_t wavHeader[44];
size_t headerRead = stream->readBytes(wavHeader, 44);
if (headerRead != 44) {
Serial.println("WAV文件头部不完整,可能不是标准PCM文件");
http.end();
return;
}
Serial.println("已跳过WAV头部,开始播放音频数据");
// 读取音频数据并通过I2S输出
const size_t bufferSize = 1024; // 数据缓冲区大小(需为偶数,16位数据对齐)
uint8_t audioBuffer[bufferSize];
size_t totalBytesPlayed = 0;
while (stream->available()) {
// 从网络读取数据到缓冲区
size_t bytesRead = stream->readBytes(audioBuffer, bufferSize);
if (bytesRead == 0) break;
// 通过I2S输出音频数据
size_t bytesWritten = 0;
esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
if (err != ESP_OK) {
Serial.printf("I2S写入失败,错误码: %d\n", err);
break;
}
totalBytesPlayed += bytesWritten;
// 每播放4KB数据打印一次进度(可选)
if (totalBytesPlayed % 4096 == 0) {
Serial.printf("已播放: %.2f KB\n", totalBytesPlayed / 1024.0);
}
}
Serial.printf("播放完成,总数据量: %.2f KB\n", totalBytesPlayed / 1024.0);
http.end(); // 关闭HTTP连接
}
// 播放TF卡上的WAV文件
void playTFcardWav(const char* path) {
// 打开WAV文件(只读模式)
File wavFile = SD_MMC.open(path, FILE_READ);
if (!wavFile) {
Serial.printf("无法打开文件: %s\n", path);
return;
}
// 跳过WAV头部(标准PCM WAV头部44字节)
uint8_t wavHeader[44];
if (wavFile.read(wavHeader, 44) != 44) {
Serial.println("WAV文件头部不完整");
wavFile.close();
return;
}
Serial.println("已跳过WAV头部,开始播放");
// 读取PCM数据并通过I2S输出
const size_t bufferSize = 1024;
uint8_t audioBuffer[bufferSize];
size_t totalBytesPlayed = 0;
while (wavFile.available()) {
// 从文件读取数据到缓冲区
size_t bytesRead = wavFile.read(audioBuffer, bufferSize);
if (bytesRead == 0) break;
// I2S输出
size_t bytesWritten = 0;
esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
if (err != ESP_OK) {
Serial.printf("I2S写入失败,错误码: %d\n", err);
break;
}
totalBytesPlayed += bytesWritten;
if (totalBytesPlayed % 4096 == 0) {
Serial.printf("已播放: %.2f KB\n", totalBytesPlayed / 1024.0);
}
}
Serial.printf("播放完成,总数据量: %.2f KB\n", totalBytesPlayed / 1024.0);
wavFile.close(); // 关闭文件
}
// 初始化TF卡
bool initTFcard() {
// 初始化SD_MMC(1-bit模式,适配大多数TF卡)
if (!SD_MMC.begin("/sdcard", true)) { // "/sdcard"为挂载点
Serial.println("TF卡挂载失败,请检查:");
Serial.println("1. 卡是否插入正确");
Serial.println("2. 卡是否为FAT32格式");
Serial.println("3. 引脚连接是否正确");
return false;
}
// 检查卡容量(可选)
uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
Serial.printf("TF卡容量: %llu MB\n", cardSize);
// 检查文件是否存在
if (!SD_MMC.exists(wavFilePath)) {
Serial.printf("未找到文件: %s\n", wavFilePath);
return false;
}
return true;
}
关键代码(外接卡)
主要修改了sd卡的初始化和读写
cpp
//根据板子引脚来定义
#define SD_PIN_CLK 39
#define SD_PIN_CMD 38
#define SD_PIN_D0 40
const char* MOUNT_POINT="/SDCARD";
bool initTFcard() {
esp_vfs_fat_sdmmc_mount_config_t mount_config ={
.format_if_mount_failed = false,
.max_files=5,
.allocation_unit_size=16*1024,
};
sdmmc_card_t *card;
const char mount_point[] = MOUNT_POINT;
ESP_LOGI(TAG, "Initializing SD card");
ESP_LOGI(TAG, "Using SDMMC peripheral");
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.width=1;
slot_config.clk=SD_PIN_CLK;
slot_config.cmd=SD_PIN_CMD;
slot_config.d0=SD_PIN_D0;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
ESP_LOGI(TAG, "Mounting filesystem");
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
}
return false;
}
ESP_LOGI(TAG, "Filesystem mounted");
sdmmc_card_print_info(stdout, card);
return true;
}
#include <stdio.h>
#include <stdint.h>
#include <esp_log.h>
#include "driver/i2s.h"
// 需与SD卡挂载点一致(例如初始化时定义的MOUNT_POINT为"/sdcard")
static const char* TAG = "WAV_PLAYER";
// 播放TF卡上的WAV文件(不依赖SD_MMC库,使用标准C库)
void playTFcardWav(const char* path) {
// 拼接完整路径(挂载点 + 文件名,例如"/sdcard/test.wav")
char full_path[256];
snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, path);
// 打开WAV文件(二进制只读模式)
FILE* wavFile = fopen(full_path, "rb");
if (wavFile == NULL) {
ESP_LOGE(TAG, "无法打开文件: %s", full_path);
return;
}
ESP_LOGI(TAG, "成功打开文件: %s", full_path);
// 跳过WAV头部(标准PCM WAV头部44字节)
uint8_t wavHeader[44];
size_t headerRead = fread(wavHeader, 1, 44, wavFile);
if (headerRead != 44) {
ESP_LOGE(TAG, "WAV文件头部不完整(仅读取到%d字节)", headerRead);
fclose(wavFile);
return;
}
ESP_LOGI(TAG, "已跳过WAV头部,开始播放");
// 读取PCM数据并通过I2S输出
const size_t bufferSize = 1024;
uint8_t audioBuffer[bufferSize];
size_t totalBytesPlayed = 0;
// 循环读取文件直到结束
while (!feof(wavFile)) {
// 从文件读取数据到缓冲区
size_t bytesRead = fread(audioBuffer, 1, bufferSize, wavFile);
if (bytesRead == 0) {
// 检查是否为文件结束(正常)或读取错误
if (ferror(wavFile)) {
ESP_LOGE(TAG, "文件读取错误");
}
break;
}
// I2S输出音频数据
size_t bytesWritten = 0;
esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2S写入失败,错误码: %s", esp_err_to_name(err));
break;
}
totalBytesPlayed += bytesWritten;
// 每播放4KB打印一次进度
if (totalBytesPlayed % 4096 == 0) {
ESP_LOGI(TAG, "已播放: %.2f KB", totalBytesPlayed / 1024.0);
}
}
ESP_LOGI(TAG, "播放完成,总数据量: %.2f KB", totalBytesPlayed / 1024.0);
fclose(wavFile); // 关闭文件
}
使用说明
-
参数修改:
- 替换代码中
ssid和password为你的WiFi信息 - 替换
audioUrl为实际的16000Hz单声道PCM WAV文件URL(本地服务器或公网地址均可)
- 替换代码中
-
硬件连接(务必对应):
- ESP32-S3-CAM GPIO12 → MAX98357 BCLK
- ESP32-S3-CAM GPIO13 → MAX98357 LRC
- ESP32-S3-CAM GPIO14 → MAX98357 DIN
- 共地(ESP32 GND与MAX98357 GND连接)
- MAX98357需单独5V供电(避免从ESP32取电导致电流不足)
-
调试提示:
- 串口监视器波特率设为115200,可查看连接状态和播放进度
- 若无声,检查:
- 引脚是否接错(BCLK/LRC/DATA必须对应)
- MAX98357供电是否正常(5V电压是否稳定)
- 音频URL是否可访问(可先用浏览器测试能否下载)
- 音频文件是否确实为16000Hz单声道PCM(用ffmpeg再次验证)
优化说明
- 增大
dma_buf_count和dma_buf_len可减少网络波动导致的卡顿(但会增加延迟) - 单声道使用
I2S_CHANNEL_FMT_ONLY_LEFT或I2S_CHANNEL_FMT_ONLY_RIGHT均可(MAX98357会自动混合输出) - 若音频文件头部不是44字节(非标准WAV),需根据实际头部长度修改
wavHeader数组大小和读取长度
上传代码前确保已安装ESP32开发板支持(在Arduino IDE的「工具→开发板→ Boards Manager」中搜索"ESP32"安装)。