硬件准备:因为一开始只是想DIY智能硬件,但我之前是搞软件的,不太懂,就直接网购了一套小智AI各种硬件,回来才一个个查有啥用的。
1、认识下MAX98357A和喇叭
我也是才知道,原来放出声音还要加个功放。。。下面是MAX98357A


模块引脚说明(从左到右)
| 引脚 | 名称 | 功能 | 接 ESP32-S3 |
|---|---|---|---|
| LRC | Left/Right Clock | 左右声道时钟(帧时钟 WS) | GPIO41 |
| BCLK | Bit Clock | 位时钟 | GPIO40 |
| DIN | Data In | 音频数据输入 | GPIO39 |
| GAIN | Gain Setting | 增益设置 | GND 接地 |
| SD | Shutdown | 关断控制 | 3.3V 正极 |
| GND | Ground | 地 | GND 接地 |
| Vin | Voltage Input | 电源输入(2.5V-5.5V) | 3.3V 正极 |
接地至关重要,不接的话会有杂音。
还有一个喇叭硬件,按照下面的图 红色接正,黑色接负即可,要注意MAX98357A右图中绿色位置左右是有正负的。
2、硬件接线
刚开始根本不懂引脚,面包板之类的,跟着卖家给的图,一通插,确保插对,后来才慢慢领悟。


最终我自己接出来的如下,其他的屏幕和mic这次没用到,先不接。
这里也一样,要认真阅读卖家给的第一张图里的端口号,后面代码里要用。
搞硬件还有一个问题,图中连接喇叭和功放的线要注意;喇叭上红色线我接了白色的线,然后接 正极,就是绿色那个位置的左边,这个我遇到一个坑货问题,线当时插不进去绿色那个小东西里头,那玩意要搞个小螺丝刀松一松,手头一开始啥工具没有,又下单了一套螺丝刀工具,哎。

这里要注意基本信息:
硬件配置: ESP32-S3 + MAX98357(紫色那个小东西) 多音频状态播放(48kHz 立体声版)
音频文件的基础信息:5个音频采样率都是 48kHz, 16bit, 立体声,这个参数至关重要,写错或者不知道的话,后面你播放的声音要么是杂音,要么能听出来内容但声调跟牛叫一样。
功放的接线:DIN→39, BCLK→40, LRC→41
关于音频文件,我是自己通过手机录音机录制的几个1秒左右的音频文件,测试的时候是转成数组做的测试,代码是用AI生成的:
cpp
/*
* ESP32-S3 + MAX98357 多音频状态播放(48kHz 立体声版)
* 5个音频都是 48kHz, 16bit, 立体声
* 接线:DIN→39, BCLK→40, LRC→41
*/
#include <driver/i2s.h>
#define I2S_DIN 39
#define I2S_BCLK 40
#define I2S_LRC 41
#define VOLUME 70
// 固定参数(所有音频相同) 采样率和声道数,这个用工具去查看即可,但必须要设置对
#define SAMPLE_RATE 48000 //采样率
#define CHANNELS 2 //声道数
// ========== 5个音频数组(48kHz 立体声)==========
const unsigned char audio1[] = { /* 状态1音频 */ };
const unsigned char audio2[] = { /* 状态2音频 */ };
const unsigned char audio3[] = { /* 状态3音频 */ };
const unsigned char audio4[] = { /* 状态4音频 */ };
const unsigned char audio5[] = { /* 状态5音频 */ };
// 音频表
const unsigned char* audioTable[] = {audio1, audio2, audio3, audio4, audio5};
const size_t audioSizes[] = {sizeof(audio1), sizeof(audio2), sizeof(audio3),
sizeof(audio4), sizeof(audio5)};
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("多音频播放系统(48kHz 立体声)");
Serial.println("发送 1-5 播放对应音频");
// 只初始化一次 I2S(所有音频参数相同)
initI2S();
}
void loop() {
if (Serial.available()) {
char cmd = Serial.read();
int state = cmd - '0';
if (state >= 1 && state <= 5) {
Serial.printf("\n=== 状态 %d ===\n", state);
playAudio(state);
}
}
delay(100);
}
// 初始化 I2S(只执行一次)
void initI2S() {
i2s_config_t cfg = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // 立体声
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8, // 增加缓冲区适应高采样率
.dma_buf_len = 1024, // 增大缓冲区
.use_apll = true, // 高精度时钟支持 48kHz
.tx_desc_auto_clear = true,
.fixed_mclk = SAMPLE_RATE * 256 // 固定 MCLK
};
i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL);
i2s_pin_config_t pins = {
.bck_io_num = I2S_BCLK,
.ws_io_num = I2S_LRC,
.data_out_num = I2S_DIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_set_pin(I2S_NUM_0, &pins);
Serial.printf("I2S就绪: %d Hz, %d声道\n", SAMPLE_RATE, CHANNELS);
}
// 播放指定状态音频
void playAudio(int state) {
const unsigned char* wavData = audioTable[state - 1];
size_t wavSize = audioSizes[state - 1];
// 解析 data 位置
uint32_t dataOffset = 44;
uint32_t dataSize = wavSize - 44;
for (size_t i = 36; i < wavSize - 8; i++) {
if (wavData[i] == 'd' && wavData[i+1] == 'a' &&
wavData[i+2] == 't' && wavData[i+3] == 'a') {
dataOffset = i + 8;
dataSize = (wavData[i+4] | (wavData[i+5] << 8) |
(wavData[i+6] << 16) | (wavData[i+7] << 24));
break;
}
}
if (dataSize > wavSize - dataOffset) {
dataSize = wavSize - dataOffset;
}
Serial.printf("播放: %d bytes\n", dataSize);
// 播放(立体声数据直接透传)
const uint8_t* data = wavData + dataOffset;
uint32_t bytesLeft = dataSize;
// 大缓冲区减少中断
static int16_t buffer[1024];
while (bytesLeft > 0) {
uint32_t chunk = (bytesLeft < 2048) ? bytesLeft : 2048;
uint32_t samples = chunk / 2; // 16bit = 2 bytes
// 应用音量
for (uint32_t i = 0; i < samples && i < 1024; i++) {
uint32_t idx = i * 2;
int16_t sample = data[idx] | (data[idx + 1] << 8);
buffer[i] = (sample * VOLUME) / 100;
}
size_t written = 0;
i2s_write(I2S_NUM_0, buffer, samples * 2, &written, portMAX_DELAY);
data += chunk;
bytesLeft -= chunk;
}
// 静音收尾
i2s_zero_dma_buffer(I2S_NUM_0);
Serial.println("完成");
}
代码里可以看到,这五个变量就是用来存放音频数据的。
cpp
// ========== 5个音频数组(48kHz 立体声)==========
const unsigned char audio1[] = { /* 状态1音频 */ };
const unsigned char audio2[] = { /* 状态2音频 */ };
const unsigned char audio3[] = { /* 状态3音频 */ };
const unsigned char audio4[] = { /* 状态4音频 */ };
const unsigned char audio5[] = { /* 状态5音频 */ };
这个数据怎么来的呢?
1、先用手机录音机录制1个wav格式的音频,文件越小越好。
2、将wav文件转换为数组,用我这个转换工具,下载后网页打开即可使用,这个工具也是AI帮我做的,简直太方便了,就是页面顶部的《wav与CArray互转工具》。
选择第二个tab,选择要转的wav文件,点击转换C数组,还要再强调下,要记住你的音频文件的采样率,声道,位深,后面播放音频的代码是要用到的。
// 固定参数(所有音频相同) 采样率和声道数,这个用工具去查看即可,但必须要设置对
#define SAMPLE_RATE 48000 //采样率
#define CHANNELS 2 //声道数

设置要播放的采样率,声道数以后,点复制到剪贴板 按钮,然后把数组文件替换掉代码里第一个数组,编译上传代码即可。因为数组很大,我就不贴全代码了,贴了替换第一个数组的截图,然后就可以在上传代码跑一跑了。

代码跑起来后,在输入框里输入1到5,就可以播放音频了,因为数组很大,放一个实验一下能正常播放就行了,要不然编译巨慢无比,我录制了一个结果视频,在文章最开始,资源名称:播放你好
