硬件准备:因为一开始只是想DIY智能硬件,但我之前是搞软件的,不太懂,就直接网购了一套小智AI各种硬件,回来才一个个查有啥用的。

1、认识下MAX98357A和喇叭

我也是才知道,原来放出声音还要加个功放。。。下面是MAX98357A

模块引脚说明(从左到右)

引脚名称功能接 ESP32-S3
LRCLeft/Right Clock左右声道时钟(帧时钟 WS)GPIO41
BCLKBit Clock位时钟GPIO40
DINData In音频数据输入GPIO39
GAINGain Setting增益设置GND 接地
SDShutdown关断控制 3.3V 正极
GNDGroundGND 接地
VinVoltage 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生成的:

/*
 * 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("完成");
}

代码里可以看到,这五个变量就是用来存放音频数据的。

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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐