【ESP32-S3-CAM】 ESP32-S3-CAM+MAX98357播放pcm格式的wav文件(本地文件和网络文件)

ESP32-S3-CAM+MAX98357播放pcm格式的wav文件(本地文件和网络文件)

以下是适配16000Hz单声道PCM编码WAV文件的完整代码,包含详细注释和调试信息,可直接用于ESP32-S3-CAM+MAX98357方案:

完整代码(板载卡)

cpp 复制代码
#include <WiFi.h>
#include <HTTPClient.h>
#include <driver/i2s.h>

// -------------------------- 配置参数(需根据实际情况修改)--------------------------
// WiFi信息
const char* ssid = "你的WiFi名称";         // 替换为你的WiFi名称
const char* password = "你的WiFi密码";     // 替换为你的WiFi密码
// 网络WAV音频地址(必须是16000Hz单声道16位PCM编码)
const char* audioUrl = "http://example.com/11.wav";  // 替换为实际音频URL
// TF卡文件路径(11.wav需放在TF卡根目录)
//const char* wavFilePath = "/11.wav";  // 根目录下的11.wav
const char* MOUNT_POINT="/SDCARD";
//根据板子引脚来定义
#define SD_PIN_CLK 39
#define SD_PIN_CMD 38
#define SD_PIN_D0  40


// I2S硬件引脚(与MAX98357连接)
#define I2S_PORT         I2S_NUM_0       // 使用ESP32-S3板子上的I2S端口0,不涉及物理连接
#define I2S_BCLK_PIN     12              // 时钟线(BCLK)
#define I2S_LRCLK_PIN    13              // 左右声道时钟(LRC)
#define I2S_DATA_PIN     14              // 数据线(DIN)
// -----------------------------------------------------------------------------------

void setup() {
  // 初始化串口(用于调试信息输出)
  Serial.begin(115200);
  while (!Serial) delay(10);  // 等待串口准备就绪
  Serial.println("ESP32-S3-CAM 音频播放测试");

  // 连接WiFi
  Serial.printf("连接WiFi: %s ...", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi连接成功!IP地址: " + WiFi.localIP().toString());

  // 初始化I2S(适配16000Hz单声道)
  if (initI2S() != ESP_OK) {
    Serial.println("I2S初始化失败!请检查引脚连接");
    while (1) delay(1000);  // 初始化失败则卡死
  }
  Serial.println("I2S初始化完成");

  // 开始播放网络WAV音频
  Serial.printf("开始播放音频: %s\n", audioUrl);
  playNetworkWav(audioUrl);
  // 播放TF卡上的11.wav
  // playTFcardWav(wavFilePath);
  Serial.println("音频播放结束");
}

void loop() {
  // 播放完成后无需重复执行,若需循环播放可在此处调用playNetworkWav()
}

// 初始化I2S配置(适配16000Hz单声道16位PCM)
esp_err_t initI2S() {
  // 配置I2S参数
  i2s_config_t i2sConfig = {
    .mode = I2S_MODE_MASTER | I2S_MODE_TX,  // 主机模式+发送模式
    .sample_rate = 16000,                   // 采样率:16000Hz(与音频文件匹配)
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,  // 位深:16位
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,    // 单声道(仅左声道输出)
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,  // 标准I2S格式
    .dma_buf_count = 8,                     // DMA缓冲区数量(增大可减少卡顿)
    .dma_buf_len = 512,                     // 每个缓冲区大小(字节)
    .use_apll = false,                      // 不使用APLL时钟(简化配置)
    .tx_desc_auto_clear = true,             // 自动清理发送描述符
    .fixed_mclk = 0                         // 不固定MCLK时钟
  };

  // 安装I2S驱动
  esp_err_t err = i2s_driver_install(I2S_PORT, &i2sConfig, 0, NULL);
  if (err != ESP_OK) return err;

  // 配置I2S引脚映射
  i2s_pin_config_t pinConfig = {
    .bck_io_num = I2S_BCLK_PIN,    // 时钟线引脚
    .ws_io_num = I2S_LRCLK_PIN,    // 左右声道时钟引脚
    .data_out_num = I2S_DATA_PIN,  // 数据线引脚
    .data_in_num = I2S_PIN_NO_CHANGE  // 不使用输入
  };

  // 设置I2S引脚
  err = i2s_set_pin(I2S_PORT, &pinConfig);
  if (err != ESP_OK) {
    i2s_driver_uninstall(I2S_PORT);  // 引脚配置失败则卸载驱动
    return err;
  }

  // 启动I2S发送
  i2s_start(I2S_PORT);
  return ESP_OK;
}

// 从网络获取WAV文件并通过I2S播放
void playNetworkWav(const char* url) {
  HTTPClient http;

  // 启动HTTP连接
  if (!http.begin(url)) {
    Serial.println("HTTP初始化失败");
    return;
  }

  // 发送HTTP GET请求
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    Serial.printf("HTTP请求失败,错误码: %d\n", httpCode);
    http.end();
    return;
  }

  // 获取音频数据流
  WiFiClient* stream = http.getStreamPtr();
  if (!stream) {
    Serial.println("无法获取音频流");
    http.end();
    return;
  }

  // 跳过WAV文件头部(标准PCM WAV头部为44字节)
  uint8_t wavHeader[44];
  size_t headerRead = stream->readBytes(wavHeader, 44);
  if (headerRead != 44) {
    Serial.println("WAV文件头部不完整,可能不是标准PCM文件");
    http.end();
    return;
  }
  Serial.println("已跳过WAV头部,开始播放音频数据");

  // 读取音频数据并通过I2S输出
  const size_t bufferSize = 1024;  // 数据缓冲区大小(需为偶数,16位数据对齐)
  uint8_t audioBuffer[bufferSize];
  size_t totalBytesPlayed = 0;

  while (stream->available()) {
    // 从网络读取数据到缓冲区
    size_t bytesRead = stream->readBytes(audioBuffer, bufferSize);
    if (bytesRead == 0) break;

    // 通过I2S输出音频数据
    size_t bytesWritten = 0;
    esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
    if (err != ESP_OK) {
      Serial.printf("I2S写入失败,错误码: %d\n", err);
      break;
    }

    totalBytesPlayed += bytesWritten;
    // 每播放4KB数据打印一次进度(可选)
    if (totalBytesPlayed % 4096 == 0) {
      Serial.printf("已播放: %.2f KB\n", totalBytesPlayed / 1024.0);
    }
  }

  Serial.printf("播放完成,总数据量: %.2f KB\n", totalBytesPlayed / 1024.0);
  http.end();  // 关闭HTTP连接
}

// 播放TF卡上的WAV文件
void playTFcardWav(const char* path) {
  // 打开WAV文件(只读模式)
  File wavFile = SD_MMC.open(path, FILE_READ);
  if (!wavFile) {
    Serial.printf("无法打开文件: %s\n", path);
    return;
  }

  // 跳过WAV头部(标准PCM WAV头部44字节)
  uint8_t wavHeader[44];
  if (wavFile.read(wavHeader, 44) != 44) {
    Serial.println("WAV文件头部不完整");
    wavFile.close();
    return;
  }
  Serial.println("已跳过WAV头部,开始播放");

  // 读取PCM数据并通过I2S输出
  const size_t bufferSize = 1024;
  uint8_t audioBuffer[bufferSize];
  size_t totalBytesPlayed = 0;

  while (wavFile.available()) {
    // 从文件读取数据到缓冲区
    size_t bytesRead = wavFile.read(audioBuffer, bufferSize);
    if (bytesRead == 0) break;

    // I2S输出
    size_t bytesWritten = 0;
    esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
    if (err != ESP_OK) {
      Serial.printf("I2S写入失败,错误码: %d\n", err);
      break;
    }

    totalBytesPlayed += bytesWritten;
    if (totalBytesPlayed % 4096 == 0) {
      Serial.printf("已播放: %.2f KB\n", totalBytesPlayed / 1024.0);
    }
  }

  Serial.printf("播放完成,总数据量: %.2f KB\n", totalBytesPlayed / 1024.0);
  wavFile.close();  // 关闭文件
}
// 初始化TF卡
bool initTFcard() {
  // 初始化SD_MMC(1-bit模式,适配大多数TF卡)
  if (!SD_MMC.begin("/sdcard", true)) {  // "/sdcard"为挂载点
    Serial.println("TF卡挂载失败,请检查:");
    Serial.println("1. 卡是否插入正确");
    Serial.println("2. 卡是否为FAT32格式");
    Serial.println("3. 引脚连接是否正确");
    return false;
  }

  // 检查卡容量(可选)
  uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
  Serial.printf("TF卡容量: %llu MB\n", cardSize);

  // 检查文件是否存在
  if (!SD_MMC.exists(wavFilePath)) {
    Serial.printf("未找到文件: %s\n", wavFilePath);
    return false;
  }
  return true;
}

关键代码(外接卡)

主要修改了sd卡的初始化和读写

cpp 复制代码
//根据板子引脚来定义
#define SD_PIN_CLK 39
#define SD_PIN_CMD 38
#define SD_PIN_D0  40

const char* MOUNT_POINT="/SDCARD";

bool initTFcard() {
   esp_vfs_fat_sdmmc_mount_config_t mount_config ={
        .format_if_mount_failed = false,
        .max_files=5,
        .allocation_unit_size=16*1024,
     };
    sdmmc_card_t *card;
    const char mount_point[] = MOUNT_POINT;
    ESP_LOGI(TAG, "Initializing SD card");
    ESP_LOGI(TAG, "Using SDMMC peripheral");
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width=1;
    slot_config.clk=SD_PIN_CLK;
    slot_config.cmd=SD_PIN_CMD;
    slot_config.d0=SD_PIN_D0;
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
    ESP_LOGI(TAG, "Mounting filesystem");
    
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);
    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount filesystem. "
                     "If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
        } else {
            ESP_LOGE(TAG, "Failed to initialize the card (%s). "
                     "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
        }
        return false;
    }
   ESP_LOGI(TAG, "Filesystem mounted");
   sdmmc_card_print_info(stdout, card);
   return true;
}
#include <stdio.h>
#include <stdint.h>
#include <esp_log.h>
#include "driver/i2s.h"

// 需与SD卡挂载点一致(例如初始化时定义的MOUNT_POINT为"/sdcard")
static const char* TAG = "WAV_PLAYER";

// 播放TF卡上的WAV文件(不依赖SD_MMC库,使用标准C库)
void playTFcardWav(const char* path) {
    // 拼接完整路径(挂载点 + 文件名,例如"/sdcard/test.wav")
    char full_path[256];
    snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, path);

    // 打开WAV文件(二进制只读模式)
    FILE* wavFile = fopen(full_path, "rb");
    if (wavFile == NULL) {
        ESP_LOGE(TAG, "无法打开文件: %s", full_path);
        return;
    }
    ESP_LOGI(TAG, "成功打开文件: %s", full_path);

    // 跳过WAV头部(标准PCM WAV头部44字节)
    uint8_t wavHeader[44];
    size_t headerRead = fread(wavHeader, 1, 44, wavFile);
    if (headerRead != 44) {
        ESP_LOGE(TAG, "WAV文件头部不完整(仅读取到%d字节)", headerRead);
        fclose(wavFile);
        return;
    }
    ESP_LOGI(TAG, "已跳过WAV头部,开始播放");

    // 读取PCM数据并通过I2S输出
    const size_t bufferSize = 1024;
    uint8_t audioBuffer[bufferSize];
    size_t totalBytesPlayed = 0;

    // 循环读取文件直到结束
    while (!feof(wavFile)) {
        // 从文件读取数据到缓冲区
        size_t bytesRead = fread(audioBuffer, 1, bufferSize, wavFile);
        if (bytesRead == 0) {
            // 检查是否为文件结束(正常)或读取错误
            if (ferror(wavFile)) {
                ESP_LOGE(TAG, "文件读取错误");
            }
            break;
        }

        // I2S输出音频数据
        size_t bytesWritten = 0;
        esp_err_t err = i2s_write(I2S_PORT, audioBuffer, bytesRead, &bytesWritten, portMAX_DELAY);
        if (err != ESP_OK) {
            ESP_LOGE(TAG, "I2S写入失败,错误码: %s", esp_err_to_name(err));
            break;
        }

        totalBytesPlayed += bytesWritten;
        // 每播放4KB打印一次进度
        if (totalBytesPlayed % 4096 == 0) {
            ESP_LOGI(TAG, "已播放: %.2f KB", totalBytesPlayed / 1024.0);
        }
    }

    ESP_LOGI(TAG, "播放完成,总数据量: %.2f KB", totalBytesPlayed / 1024.0);
    fclose(wavFile);  // 关闭文件
}

使用说明

  1. 参数修改:

    • 替换代码中ssidpassword为你的WiFi信息
    • 替换audioUrl为实际的16000Hz单声道PCM WAV文件URL(本地服务器或公网地址均可)
  2. 硬件连接(务必对应):

    • ESP32-S3-CAM GPIO12 → MAX98357 BCLK
    • ESP32-S3-CAM GPIO13 → MAX98357 LRC
    • ESP32-S3-CAM GPIO14 → MAX98357 DIN
    • 共地(ESP32 GND与MAX98357 GND连接)
    • MAX98357需单独5V供电(避免从ESP32取电导致电流不足)
  3. 调试提示:

    • 串口监视器波特率设为115200,可查看连接状态和播放进度
    • 若无声,检查:
      • 引脚是否接错(BCLK/LRC/DATA必须对应)
      • MAX98357供电是否正常(5V电压是否稳定)
      • 音频URL是否可访问(可先用浏览器测试能否下载)
      • 音频文件是否确实为16000Hz单声道PCM(用ffmpeg再次验证)

优化说明

  • 增大dma_buf_countdma_buf_len可减少网络波动导致的卡顿(但会增加延迟)
  • 单声道使用I2S_CHANNEL_FMT_ONLY_LEFTI2S_CHANNEL_FMT_ONLY_RIGHT均可(MAX98357会自动混合输出)
  • 若音频文件头部不是44字节(非标准WAV),需根据实际头部长度修改wavHeader数组大小和读取长度

上传代码前确保已安装ESP32开发板支持(在Arduino IDE的「工具→开发板→ Boards Manager」中搜索"ESP32"安装)。

相关推荐
Android系统攻城狮2 天前
Android内核进阶之周期更新PCM状态snd_pcm_period_elapsed:用法实例(九十二)
android·pcm·android内核·音频进阶
顾北川_野3 天前
播放PCM音频增益低+单独增强PCM解码的方案
音视频·pcm
Android系统攻城狮3 天前
Android内核进阶之写pcm数据到硬件snd_pcm_lib_write:用法实例(八十九)
pcm·android内核·音频进阶·pcm硬件参数·alsa音频
Android系统攻城狮4 天前
Android内核进阶之获取DMA地址snd_pcm_sgbuf_get_addr:用法实例(九十一)
android·pcm·android内核·音频进阶·pcm硬件参数
#做一个清醒的人1 个月前
【electron6】Web Audio + AudioWorklet PCM 实时采集噪音和模拟调试
前端·javascript·electron·pcm
长沙红胖子Qt1 个月前
FFmpeg开发笔记(十二):ffmpeg音频处理、采集麦克风音频录音为WAV
ffmpeg·pcm·wav·录音·麦克风
骄傲的心别枯萎2 个月前
RV1126 NO.30:RV1126多线程获取音频AI的PCM数据
linux·ffmpeg·音视频·pcm·视频编解码
小狮子安度因2 个月前
ffplay播放pcm
pcm
froxy2 个月前
音频中的PDM、PCM概念解读
音视频·pcm