使用树莓派RP2040测试MAX98357时,RP2040本身没有I2S,用的是软件模拟方式,效果不理想,表现为播放的音频速度明显变慢。最终使用ESP32C3成功。ESP32C3有一个I2S外设,可以使用这个外设完成播放。
在测试之前,我准备了四个用于测试的WAV文件,音频的整体时间长度不完全相同。
t1.wav:16位、双通道、采样率44100Hz。
t2.wav:16位、双通道、采样率48000Hz。
t3.wav:16位、单通道、采样率48000Hz。
t4.wav:16位、双通道、采样率48000Hz。
把这四个wav文件拷贝到sd卡中。然后将sd卡取出,插入到SD卡读写模块里。将SD卡读写模块连接到ESP32C3。同时,将Max98357也连接到ESP32C3中。

实物:

程序代码如下:
#include <SPI.h>
#include <SD.h>
#include <driver/i2s.h>
// ========== SD卡引脚 (避开GPIO8/9,防止下载冲突) ==========
#define SD_CS 10
#define SD_MOSI 6
#define SD_MISO 7
#define SD_SCK 4
// ========== MAX98357A I2S引脚 ==========
#define I2S_BCK 5 // 位时钟 (BCLK)
#define I2S_WS 3 // 左右时钟 (LRC)
#define I2S_DOUT 2 // 数据输出 (DIN)
// ========== I2S配置 ==========
#define I2S_PORT I2S_NUM_0
#define BUFFER_SIZE 512 // 音频缓冲区大小
// WAV文件头结构
struct wav_header_t {
char chunkID[4]; // "RIFF"
uint32_t chunkSize;
char format[4]; // "WAVE"
char subchunk1ID[4]; // "fmt "
uint32_t subchunk1Size;
uint16_t audioFormat;
uint16_t numChannels;
uint32_t sampleRate;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample;
char subchunk2ID[4]; // "data"
uint32_t subchunk2Size;
};
wav_header_t wavHeader;
File currentFile;
// I2S引脚配置
i2s_pin_config_t i2s_pins = {
.bck_io_num = I2S_BCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_DOUT,
.data_in_num = I2S_PIN_NO_CHANGE
};
// I2S配置 - 标准模式
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 256,
.use_apll = true, // <--- 启用APLL,提高时钟精度
.tx_desc_auto_clear = true,
.fixed_mclk = -1
};
// ========== 函数声明 ==========
bool readWAVHeader(File& file, wav_header_t* header);
void printWAVInfo(wav_header_t* header);
void setI2SSampleRate(uint32_t sampleRate);
bool playWAVFile(const char* filename);
// ========== 初始化I2S (带GPIO驱动强度修复) ==========
void initI2S() {
Serial.println("正在初始化I2S...");
// 安装I2S驱动
esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("I2S驱动安装失败: %d\n", err);
return;
}
// 设置I2S引脚
err = i2s_set_pin(I2S_PORT, &i2s_pins);
if (err != ESP_OK) {
Serial.printf("I2S引脚设置失败: %d\n", err);
return;
}
gpio_set_drive_capability((gpio_num_t)I2S_DOUT, GPIO_DRIVE_CAP_0);
Serial.println("I2S初始化完成 (DOUT驱动能力已降为最低)");
}
// ========== 初始化SD卡 ==========
bool initSDCard() {
Serial.println("正在初始化SD卡...");
// 初始化自定义SPI总线
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, SPI)) {
Serial.println("SD卡初始化失败!");
return false;
}
Serial.println("SD卡初始化成功");
// 打印SD卡信息
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD卡容量: %llu MB\n", cardSize);
return true;
}
// ========== 读取WAV文件头 ==========
bool readWAVHeader(File& file, wav_header_t* header) {
if (file.read((uint8_t*)header, sizeof(wav_header_t)) != sizeof(wav_header_t)) {
Serial.println("读取WAV头失败");
return false;
}
// 验证WAV格式
if (memcmp(header->chunkID, "RIFF", 4) != 0 ||
memcmp(header->format, "WAVE", 4) != 0 ||
memcmp(header->subchunk1ID, "fmt ", 4) != 0) {
Serial.println("不是有效的WAV文件");
return false;
}
return true;
}
// ========== 打印WAV文件信息 ==========
void printWAVInfo(wav_header_t* header) {
Serial.println("========== WAV文件信息 ==========");
Serial.printf("采样率: %lu Hz\n", header->sampleRate);
Serial.printf("声道数: %d\n", header->numChannels);
Serial.printf("位深: %d bit\n", header->bitsPerSample);
Serial.printf("数据大小: %lu bytes\n", header->subchunk2Size);
Serial.println("=================================");
}
// ========== 动态调整I2S采样率 ==========
void setI2SSampleRate(uint32_t sampleRate) {
// 停止I2S
i2s_stop(I2S_PORT);
// 更新配置(采样率降低一半,否则播放太快,变调;双声道变单声道,减半?)
i2s_config.sample_rate = sampleRate/2;
// 重新配置
i2s_driver_uninstall(I2S_PORT);
i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
i2s_set_pin(I2S_PORT, &i2s_pins);
// 重新应用驱动强度修复
gpio_set_drive_capability((gpio_num_t)I2S_DOUT, GPIO_DRIVE_CAP_0);
Serial.printf("I2S采样率已调整为: %lu Hz\n", sampleRate/2);
}
// ========== 播放WAV文件 ==========
bool playWAVFile(const char* filename) {
Serial.printf("正在播放: %s\n", filename);
// 打开文件
File file = SD.open(filename);
if (!file) {
Serial.println("无法打开文件");
return false;
}
// 读取并验证WAV头
if (!readWAVHeader(file, &wavHeader)) {
file.close();
return false;
}
printWAVInfo(&wavHeader);
// 根据WAV文件的采样率调整I2S
setI2SSampleRate(wavHeader.sampleRate);
// 准备缓冲区
uint8_t* buffer = (uint8_t*)malloc(BUFFER_SIZE);
if (!buffer) {
Serial.println("内存分配失败");
file.close();
return false;
}
// ⚠️ 立体声转单声道处理
// MAX98357A是单声道功放,立体声数据需要转换[citation:1]
bool isStereo = (wavHeader.numChannels == 2);
size_t bytesRemaining = wavHeader.subchunk2Size;
size_t bytesToRead;
int16_t* sampleBuffer;
int16_t* monoSamples;
Serial.println("开始播放...");
while (bytesRemaining > 0) {
bytesToRead = (bytesRemaining < BUFFER_SIZE) ? bytesRemaining : BUFFER_SIZE;
// 读取音频数据
size_t bytesRead = file.read(buffer, bytesToRead);
if (bytesRead == 0) break;
if (isStereo) {
// 立体声转单声道:只取左声道(每4字节为一对立体声16位样本)
size_t sampleCount = bytesRead / 4;
monoSamples = (int16_t*)malloc(sampleCount * sizeof(int16_t));
if (monoSamples) {
sampleBuffer = (int16_t*)buffer;
for (size_t i = 0; i < sampleCount; i++) {
monoSamples[i] = sampleBuffer[i * 2]; // 只取左声道
}
// 发送转换后的单声道数据
i2s_write(I2S_PORT, monoSamples, sampleCount * sizeof(int16_t), &bytesRead, portMAX_DELAY);
free(monoSamples);
} else {
// 内存不足时发送原始数据(可能产生噪音)
i2s_write(I2S_PORT, buffer, bytesRead, &bytesRead, portMAX_DELAY);
}
} else {
// 单声道直接发送
i2s_write(I2S_PORT, buffer, bytesRead, &bytesRead, portMAX_DELAY);
}
bytesRemaining -= bytesToRead;
// 简单的进度显示
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 1000) {
int progress = (int)((wavHeader.subchunk2Size - bytesRemaining) * 100.0 / wavHeader.subchunk2Size);
Serial.printf("播放进度: %d%%\n", progress);
lastPrint = millis();
}
}
free(buffer);
file.close();
Serial.println("播放完成!");
return true;
}
// ========== 列出SD卡中的WAV文件 ==========
void listWAVFiles() {
Serial.println("\nSD卡中的WAV文件:");
File root = SD.open("/");
if (!root) {
Serial.println("无法打开根目录");
return;
}
File file = root.openNextFile();
int count = 0;
while (file) {
if (!file.isDirectory()) {
String name = file.name();
if (name.endsWith(".wav") || name.endsWith(".WAV")) {
Serial.printf(" - %s (%llu bytes)\n", name.c_str(), file.size());
count++;
}
}
file = root.openNextFile();
}
if (count == 0) {
Serial.println(" 没有找到WAV文件");
}
root.close();
}
// ========== Arduino标准函数 ==========
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n==================================");
Serial.println("ESP32-C3 SD卡 WAV播放器");
Serial.println("==================================\n");
// 初始化SD卡
if (!initSDCard()) {
Serial.println("SD卡初始化失败,系统停止");
while (1) delay(100);
}
// 列出所有WAV文件
listWAVFiles();
// 初始化I2S音频
initI2S();
// 设置音量(通过修改数据幅度)
// MAX98357A没有软件音量控制,可以在播放时调整buffer数据
// 查找并播放第一个WAV文件
File root = SD.open("/");
File file = root.openNextFile();
bool found = false;
//while (file && !found) {
while (file) {
if (!file.isDirectory()) {
String name = file.name();
if (name.endsWith(".wav") || name.endsWith(".WAV")) {
playWAVFile((String(F("/")) + name).c_str());
//found = true;
} else {
//found = false;
}
}
file = root.openNextFile();
}
root.close();
if (!found) {
Serial.println("未找到任何WAV文件!");
}
}
void loop() {
// 播放完成后空闲
delay(1000);
}注意在处理采样率那一块(函数:setI2SSampleRate):
代码:i2s_config.sample_rate = sampleRate/2;
进行了人为干涉,将采样率降低了一半。不这样做的结果,播放声音会音调变高。
以下是测试过程中的输出日志:
================================== ESP32-C3 SD卡 WAV播放器 ================================== 正在初始化SD卡... SD卡初始化成功 SD卡容量: 59 MB SD卡中的WAV文件: - t2.wav (6381996054500764 bytes) - t3.wav (3191093051595824 bytes) - t1.wav (23053684708038044 bytes) - t4.wav (6381996054486064 bytes) 正在初始化I2S... I2S初始化完成 (DOUT驱动能力已降为最低) 正在播放: /t2.wav ========== WAV文件信息 ========== 采样率: 48000 Hz 声道数: 2 位深: 16 bit 数据大小: 1485880 bytes ================================= I2S采样率已调整为: 24000 Hz 开始播放... 播放进度: 0% 播放进度: 12% 播放进度: 25% 播放进度: 38% 播放进度: 51% 播放进度: 64% 播放进度: 77% 播放进度: 90% 播放完成! 正在播放: /t3.wav ========== WAV文件信息 ========== 采样率: 48000 Hz 声道数: 1 位深: 16 bit 数据大小: 742940 bytes ================================= I2S采样率已调整为: 24000 Hz 开始播放... 播放进度: 3% 播放进度: 15% 播放进度: 29% 播放进度: 41% 播放进度: 54% 播放进度: 67% 播放进度: 80% 播放进度: 93% 播放完成! 正在播放: /t1.wav ========== WAV文件信息 ========== 采样率: 44100 Hz 声道数: 2 位深: 16 bit 数据大小: 5367560 bytes ================================= I2S采样率已调整为: 22050 Hz 开始播放... 播放进度: 1% 播放进度: 4% 播放进度: 8% 播放进度: 11% 播放进度: 14% 播放进度: 18% 播放进度: 21% 播放进度: 24% 播放进度: 28% 播放进度: 31% 播放进度: 34% 播放进度: 38% 播放进度: 41% 播放进度: 44% 播放进度: 47% 播放进度: 51% 播放进度: 54% 播放进度: 57% 播放进度: 61% 播放进度: 64% 播放进度: 67% 播放进度: 71% 播放进度: 74% 播放进度: 77% 播放进度: 81% 播放进度: 84% 播放进度: 87% 播放进度: 91% 播放进度: 94% 播放进度: 97% 播放完成! 正在播放: /t4.wav ========== WAV文件信息 ========== 采样率: 48000 Hz 声道数: 2 位深: 16 bit 数据大小: 1485880 bytes ================================= I2S采样率已调整为: 24000 Hz 开始播放... 播放进度: 3% 播放进度: 16% 播放进度: 29% 播放进度: 42% 播放进度: 55% 播放进度: 68% 播放进度: 81% 播放进度: 94% 播放完成! 未找到任何WAV文件!
测试结果(音频部分对比):

按照制作时的采样率播放,音调变高了。从对比图上看,按我的理解,相当于按照高频率的模式播放,所以音调变高了。而减半后播放,声音才变得正常。
我要赚赏金
