ESP32-S3-CAM:接MAX98357A+喇叭播放音频

硬件准备:因为一开始只是想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,就可以播放音频了,因为数组很大,放一个实验一下能正常播放就行了,要不然编译巨慢无比,我录制了一个结果视频,在文章最开始,资源名称:播放你好

相关推荐
敬畏_上帝2 小时前
PCtolLCD2002完美版下载以及教程
单片机·嵌入式硬件
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(五):库函数移植全解析 —— 从底层原理到移植实操(含环境适配 + 报错解决)
vscode·单片机·嵌入式硬件·代理模式·智能硬件·pcb工艺·嵌入式实时数据库
天月风沙3 小时前
幻尔总线舵机测试板BusLinker V2.5 控制代码
单片机·嵌入式硬件·机器人·舵机
somi73 小时前
51单片机-01-基础概念
单片机·嵌入式硬件·学习·51单片机
✎ ﹏梦醒͜ღ҉繁华落℘3 小时前
单片机基础知识 -- TFT-LCD
单片机·嵌入式硬件
孤芳剑影5 小时前
Cadence Allegro 如何修改板框大小
嵌入式硬件
csg11076 小时前
PIC单片机高阶实战(三):PIC32MX电平变化中断输入
单片机·嵌入式硬件·物联网
孤芳剑影7 小时前
Allegro测量查看通孔尺寸方法
嵌入式硬件
银月光科技7 小时前
红外LED加热应用的市场格局与增长潜力
单片机·嵌入式硬件