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

相关推荐
华清远见IT开放实验室10 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
AI的探索之旅12 小时前
AI辅助原理图评审:电源去耦、BOOT引脚、VCAP——19项逐一核查,遗漏?不存在的
人工智能·vscode·嵌入式硬件
茯苓gao12 小时前
嵌入式开发笔记:EtherCAT协议从硬件到软件完整配置指南——从零搭建一套EtherCAT通信系统
笔记·嵌入式硬件·学习
GeekArch13 小时前
第24讲:Vibe模式代码风格控制——适配Keil/STM32工程规范
人工智能·stm32·单片机·嵌入式硬件·mcu·决策树·ai编程
Freedom_my14 小时前
STM32项目3
stm32·单片机·嵌入式硬件
国科安芯15 小时前
星间光链路:AS32S601型抗辐射MCU在空间激光通信终端控制中的技术实现
服务器·网络·单片机·嵌入式硬件·物联网·安全·信息与通信
小李不困还能学16 小时前
基于 51 单片机的8 路抢答器设计教程
单片机·嵌入式硬件·mongodb·抢答器
CedarQR16 小时前
万字长文:从零在 RK3588 上部署 PaddleSpeech 中文 TTS 全流程(FastSpeech2 + HiFiGAN)
开发语言·c++·嵌入式硬件·ubuntu·json
萌动的小火苗16 小时前
嵌入式开发中的栈与队列:任务调度为什么依赖数据结构
数据结构·c++·单片机·嵌入式硬件
振浩微433射频芯片17 小时前
工业遥控器总在关键时刻“掉链子”?——433MHz芯片方案如何破局
网络·单片机·嵌入式硬件·物联网·工业遥控