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

相关推荐
azwsm4 分钟前
电路元器件和GPIO控制器
单片机·嵌入式硬件
qq_366566502 小时前
视频配音自动化Pipeline:TTS选型+音色克隆+批量处理(附完整代码)
自动化·新媒体运营·音视频·音频
kebidaixu3 小时前
FreeRTOS 移植到 STM32F407VETX 记录(一)
stm32·单片机·嵌入式硬件
CSDN官方博客4 小时前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
点灯小铭4 小时前
基于单片机的数码管定时插座设计与定时开关功能实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
云栖梦泽4 小时前
玩转RK3506SDK
linux·嵌入式硬件
DogDaoDao6 小时前
【GitHub】VoxCPM2 实战全解析:原理、部署与效果对比
深度学习·大模型·github·音频·语音模型·tss·文本生成语音
数智工坊6 小时前
机器人四大主控板系统分层选型指南:树莓派、ESP32、STM32与Arduino的能力边界与实战定位
stm32·嵌入式硬件·机器人
进击的小头7 小时前
第8篇:IGBT 从零到精通:核心原理、关键参数、选型指南与工业级应用要点
经验分享·嵌入式硬件·学习
点灯小铭7 小时前
基于单片机的多模式智能洗衣机设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业