简介

前面文章我们介绍了通过I2S读取INMP441音频数据,是关于I2S协议的数据读取,本篇我们介绍关于I2S协议的数据输出,通过I2S输出数据,即通过I2S进行音频输出。对于ESP32可以有两种I2S音频输出的方式,一种是 使用外部I2S进行音频输出,另一种是 使用片上DAC进行I2S音频输出。对于这两种方式,这里结合上篇文章对WAV文件格式的介绍,分别通过I2S协议播放不同音频,一个是我们自己放进去SD卡里的WAV音频,另一个是我们上篇文章生成的1秒正弦波音频。没有看过对WAV格式介绍这篇文章的或者对WAV格式不了解的可以点击下面文章,

【ESP32|音频】一文读懂WAV音频文件格式【详解】

往期相关文章:

ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础

ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据

准备

  1. 主要硬件
    根据我们不同方式的音频输出我们需要以下硬件:
硬件名称 数量
ESP32 1
SD卡模块 1
PCM5102A立体声DAC模块
可替换为其他DAC模块如MAX98357
1
TDA2030A功放模块(可替换为其他功放模块 1
4欧3瓦喇叭 1
耳机或音频功放板 (用于PCM5102A音频输出 1

在这里插入图片描述

  1. 软件

格式工厂,用于转换和生成WAV音频文件,以下介绍生成过程,会使用格式工厂生成WAV音频的可以略过,但要注意我们要生成的音频参数为:44100Hz,双声道。
在这里插入图片描述

点击转换为WAV格式:

在这里插入图片描述
添加原始文件,我这里是.MP3格式:

在这里插入图片描述

添加文件后点击输出配置,按箭头数字顺序点击和配置参数:
在这里插入图片描述

点击 开始
在这里插入图片描述
开始后完成
在这里插入图片描述

名字太长了改个名字:(不要问我为什么不一开始改。。
在这里插入图片描述

使用外部I2S进行音频输出

硬件接线

在这里插入图片描述

软件实现

上面生成了WAV音频后,把音频放入SD卡里,我们使用ESP32和PCM5102A来实现播放SD卡里的音频。根据我们前面几篇文章对I2S底层API函数及其每个函数内参数的介绍,我们可以按照以下步骤进行I2S的初始化。首先安装I2S驱动,然后配置I2S引脚

安装I2S驱动

安装I2S驱动首先包括对I2S接口的初始化(I2S传输模式、采样率,采样深度等参数)。

初始化I2S接口

因为我们要通过I2S输出音频,所以I2S传输模式.mode设置为I2S_MODE_MASTER | I2S_MODE_TX
,这里注意.channel_format我们用的是I2S_CHANNEL_FMT_RIGHT_LEFT,双声道输出, 因为前面我们生成WAV音频文件设置属性的时候是选择2 立体声,这里大家可以自己试一下如果改成I2S_CHANNEL_FMT_ONLY_RIGHT 只右声道输出会有什么效果。

 i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),  // I2S传输模式
        .sample_rate = SAMPLE_RATE,  // 设置采样率
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,  // 16位音频数据
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,  // 双声道输出
        .communication_format = I2S_COMM_FORMAT_I2S,  // I2S通信协议
        .intr_alloc_flags = 0,  // 无中断
        .dma_buf_count = 8,  // DMA缓冲区数目
        .dma_buf_len = BUFFER_SIZE,  // 每个DMA缓冲区大小
        .use_apll = false
    };

配置I2S引脚

这里我们定义I2S BCK、WS、DIN引脚分别为esp32 的 27 26 25引脚

#define BCK_PIN      27     // I2S BCK引脚
#define WS_PIN       26     // I2S WS引脚
#define DIN_PIN      25     // I2S SD引脚

// 配置I2S引脚
    i2s_pin_config_t pin_config = {
        .bck_io_num = BCK_PIN,  // BCK引脚
        .ws_io_num = WS_PIN,    // WS引脚
        .data_out_num = DIN_PIN,  // DIN引脚
        .data_in_num = I2S_PIN_NO_CHANGE  // 不使用I2S数据输入
    };

安装和配置I2S

配置好以上参数后调用加载I2S驱动函数esp_err_t i2s_driver_install(i2s_port_t i2s_num, const i2s_config_t *i2s_config, int queue_size, void *i2s_queue) 和设置I2S引脚函数 esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin)

// 安装I2S驱动并配置引脚
    if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK ||
        i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) {
        Serial.println("I2S驱动安装失败");
        while (1);
    }
    Serial.println("I2S初始化成功");
}

前面介绍I2S读取INMP441音频数据的时候我们是将初始化I2S接口和配置I2S引脚分别封装在一个i2s_install()和i2s_setpin()函数里,但如果有两个I2S需要配置的话,这样就显的比较麻烦。这里直接将对I2S的初始化封装在一个新的函数setupI2S_PCM5102A(),里面同时包含了安装I2S驱动和配置I2S引脚。

#define SAMPLE_RATE  44100  // 采样率 44.1 kHz

//初始化I2S(用于PCM5102A)

void setupI2S_PCM5102A()
{
  // 初始化I2S接口
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),  // I2S传输模式
        .sample_rate = SAMPLE_RATE,  // 设置采样率
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,  // 16位音频数据
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,  // 单声道输出
        .communication_format = I2S_COMM_FORMAT_I2S,  // I2S通信协议
        .intr_alloc_flags = 0,  // 无中断
        .dma_buf_count = 8,  // DMA缓冲区数目
        .dma_buf_len = BUFFER_SIZE,  // 每个DMA缓冲区大小
        .use_apll = false
    };

    // 配置I2S引脚
    i2s_pin_config_t pin_config = {
        .bck_io_num = BCK_PIN,  // BCK引脚
        .ws_io_num = WS_PIN,    // WS引脚
        .data_out_num = SD_PIN,  // DIN引脚
        .data_in_num = I2S_PIN_NO_CHANGE  // 不使用I2S数据输入
    };

    // 安装I2S驱动并配置引脚
    if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK ||
        i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) {
        Serial.println("I2S驱动安装失败");
        while (1);
    }
    Serial.println("I2S初始化成功");

}

打开WAV音频并播放

初始化I2S后 , 对SD卡进行初始化

 // 初始化SD卡
    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD卡初始化失败");
        while (1);
    }
    Serial.println("SD卡初始化成功");

并打开SD卡里的WAV文件,这里我们循环播放。所以以下代码都放在loop函数里面。

File audioFile = SD.open("/步步 - 五月天.wav");  // 打开SD卡上的WAV文件
    if (!audioFile) {
        Serial.println("无法打开文件");
        return;
    }

从前面的文章我们知道WAV文件由WAV文件头和WAV数据部分组成,我们需要读取SD卡里面的WAV音频并播放,就需要跳过WAV文件头部分,这里我们使用audioFile.read(wavHeader, WAV_HEADER_SIZE)读取文件头,当读取WAV文件头时,文件指针会自动移动到头部之后,也就是音频数据的起始位置。所以,后续的读取操作会直接从数据部分开始,相当于“跳过了文件头”。 也可以使用 audioFile.seek(sizeof(WAV_HEADER_SIZE))直接跳过WAV文件头直接到数据部分。同时这里对WAV文件的有效性进行了判断,前面文章我们介绍过了,因为WAV是基于RIFF格式的,所以这里判断文件头的前4个字节是否为RIFF。

#define WAV_HEADER_SIZE 44  // WAV文件头的大小

// 读取WAV文件头(前44字节)
    byte wavHeader[WAV_HEADER_SIZE];
    audioFile.read(wavHeader, WAV_HEADER_SIZE);
    
    // 确保是有效的WAV文件
    if (wavHeader[0] != 'R' || wavHeader[1] != 'I' || wavHeader[2] != 'F' || wavHeader[3] != 'F') {
        Serial.println("无效的WAV文件");
        audioFile.close();
        return;
    }
//audioFile.seek(sizeof(WAV_HEADER_SIZE));

发送I2S数据

跳过文件头到数据部分后,使用 size_t read(uint8_t* buf, size_t size) 读取WAV音频,跟之前一样,需要进行强制类型转换。每个样本大小是2字节,所以一次读BUFFER_SIZE * 2 字节大小的数据。同时这个函数会返回实际读取的字节数,类型是 size_t,所以定义一个 size_t 类型的变量 bytesRead,用于存储从文件中实际读取的字节数,如果大于零表示成功读取WAV音频数据,然后将读取的数据通过 i2s_write 函数发送到 PCM5102A 模块。只要 audioFile.read() 返回的字节数大于 0,说明还有数据可读,每次循环读取2048字节。

这里i2s_write 函数是我们这系列第一篇文章提到的,用于向 I2S 接口写入音频数据,函数原型是esp_err_t i2s_write(i2s_port_t i2s_num, const void *src, size_t size, size_t *bytes_written, TickType_t ticks_to_wait),其中 bytesWritten用于存储 i2s_write() 实际写入 I2S 硬件的字节数,bytesRead是要写入的字节数,即 audioFile.read() 返回的实际读取到的字节数,buffer也是从 WAV 文件读取的音频数据缓存区。bytesWritten 应该等于 bytesRead。

音频播放完成后需要关闭文件并加上 i2s_zero_dma_buffer(I2S_NUM_0); (或者 i2s_driver_uninstall(I2S_NUM_0) ),区别是使用后者会重启ESP32。

#define BUFFER_SIZE  1024   // 每次读取的音频数据大小

Serial.println("开始播放音频...");

    // 播放WAV文件的音频数据
    size_t bytesRead;
    int16_t buffer[BUFFER_SIZE];
    while ((bytesRead = audioFile.read((uint8_t *)buffer, BUFFER_SIZE * sizeof(int16_t))) > 0) {
        // 将音频数据通过I2S传输到PCM5102A
        size_t bytesWritten;
        i2s_write(I2S_NUM_0, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
    }

    Serial.println("音频播放完成");
    i2s_zero_dma_buffer(I2S_NUM_0);   
    //i2s_driver_uninstall(I2S_NUM_0);
    file.close();  // 关闭文件
    delay(1000);  // 播放完成后延迟1秒

将以上代码整合如下:

#include <SPIFFS.h>
#include <SD.h>
#include <driver/i2s.h>
#include <FS.h>
#include <SD.h>

#define SD_CS_PIN    5      // SD卡的CS引脚
#define BCK_PIN      27     // I2S BCK引脚
#define WS_PIN       26    // I2S WS引脚
#define DIN_PIN      25     // I2S DIN引脚

#define SAMPLE_RATE  44100  // 采样率 44.1 kHz
#define BUFFER_SIZE  1024   // 每次读取的音频数据大小
#define WAV_HEADER_SIZE 44  // WAV文件头的大小


//初始化I2S(用于PCM5102A)

void setupI2S_PCM5102A()
{
  // 初始化I2S接口
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),  // I2S传输模式
        .sample_rate = SAMPLE_RATE,  // 设置采样率
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,  // 16位音频数据
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,  // 双声道输出
        .communication_format = I2S_COMM_FORMAT_I2S,  // I2S通信协议
        .intr_alloc_flags = 0,  // 无中断
        .dma_buf_count = 8,  // DMA缓冲区数目
        .dma_buf_len = BUFFER_SIZE,  // 每个DMA缓冲区大小
        .use_apll = false
    };

    // 配置I2S引脚
    i2s_pin_config_t pin_config = {
        .bck_io_num = BCK_PIN,  // BCK引脚
        .ws_io_num = WS_PIN,    // WS引脚
        .data_out_num = DIN_PIN,  // DIN引脚
        .data_in_num = I2S_PIN_NO_CHANGE  // 不使用I2S数据输入
    };

    // 安装I2S驱动并配置引脚
    if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK ||
        i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) {
        Serial.println("I2S驱动安装失败");
        while (1);
    }
    Serial.println("I2S初始化成功");

}

void setup() {
    Serial.begin(115200);
    setupI2S_PCM5102A();

    // 初始化SD卡
    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD卡初始化失败");
        while (1);
    }
    Serial.println("SD卡初始化成功");

    
}

void loop() {
    File audioFile = SD.open("/步步 - 五月天.wav");  // 打开SD卡上的WAV文件
    if (!audioFile) {
        Serial.println("无法打开文件");
        return;
    }

    // 读取WAV文件头(前44字节)
    byte wavHeader[WAV_HEADER_SIZE];
    audioFile.read(wavHeader, WAV_HEADER_SIZE);
    
    // 确保是有效的WAV文件
    if (wavHeader[0] != 'R' || wavHeader[1] != 'I' || wavHeader[2] != 'F' || wavHeader[3] != 'F') {
        Serial.println("无效的WAV文件");
        audioFile.close();
        return;
    }
//audioFile.seek(sizeof(WAV_HEADER_SIZE));
    Serial.println("开始播放音频...");

    // 播放WAV文件的音频数据
    size_t bytesRead;
    int16_t buffer[BUFFER_SIZE];
    while ((bytesRead = audioFile.read((uint8_t *)&buffer, BUFFER_SIZE * sizeof(int16_t))) > 0) {
        // 将音频数据通过I2S传输到PCM5102A
        size_t bytesWritten;
        i2s_write(I2S_NUM_0, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
    }

    Serial.println("音频播放完成");
    i2s_zero_dma_buffer(I2S_NUM_0);
    //i2s_driver_uninstall(I2S_NUM_0);
    audioFile.close();  // 关闭文件
    delay(1000);  // 播放完成后延迟1秒
}


上传以上代码到ESP32并按照硬件接线,通过PCM5102A就可以输出SD卡音频信号,此时需要一个耳机插入PCM5102A的耳机孔,或者可以使用AUX接个音频功放板,就可以听到SD卡里的音乐了。

使用片上DAC进行I2S音频输出

使用ESP32 内建DAC进行I2S 音频输出,我们的目的是使用ESP32循环播放存储在SD里的上篇文章生成的1秒正弦波WAV音频文件,播放方式是使用模拟功放TDA2030A。

查看乐鑫官方编程指南有这么一句话:

在这里插入图片描述

在第一篇文章中在提到I2S通信格式的时候也有提到过DAC模式(下图1)。ESP32的片上DAC引脚是GPIO25和GPIO26(官方手册),可用于输出模拟音频信号。在ESP32中,I2S接口可以配置为DAC模式,直接将数字音频数据传输到片上DAC,片上DAC再将数字信号转换为模拟信号,并通过GPIO25或GPIO26进行输出。这种方式不需要额外的外部DAC芯片就可以输出音频数据,但是要注意的是,只有I2S0可以配置为使用内置 DAC 来输出音频数据,I2S1是不支持的(下图3)。而且通过片上DAC输出的模拟信号一般声音很小,需要进行音频放大,这也是为什么我们需要一个模拟功放(TDA2030A)和喇叭的原因。

图1
在这里插入图片描述

在这里插入图片描述

硬件接线

在这里插入图片描述
因为TDA2030A模拟功放的工作电压是6-12V,而ESP32的电压只有5V,所以需要进行外部供电,同样也可以接其他功放板进行音频输出。因为我们配置了两个内置DAC,所以TDA2030A的音频输入端IN既可以接D25,也可以接D26。

软件实现

I2S初始化

使用片上DAC进行I2S音频输出,和使用外部I2S进行音频输出,对I2S的初始化的整体内容是差不多的,只不过有一些区别。区别如下:

  1. 开启I2S 内置DAC模式 , 使用ESP_ERR_T i2s_set_dac_mode(I2S_DAC_MODE_T DAC_MODE )
    在这里插入图片描述
    这个函数前面没有介绍,I2S_DAC_MODE_T 同样是一个枚举类型,我们来看一下它有哪些参数:
/**
 * @brief I2S DAC mode for i2s_set_dac_mode.
 *
 * @note Built-in DAC functions are only supported on I2S0 for current ESP32 chip.
 */
typedef enum {
    I2S_DAC_CHANNEL_DISABLE  = 0,    /*!< Disable I2S built-in DAC signals*/
    I2S_DAC_CHANNEL_RIGHT_EN = 1,    /*!< Enable I2S built-in DAC right channel, maps to DAC channel 1 on GPIO25*/
    I2S_DAC_CHANNEL_LEFT_EN  = 2,    /*!< Enable I2S built-in DAC left  channel, maps to DAC channel 2 on GPIO26*/
    I2S_DAC_CHANNEL_BOTH_EN  = 0x3,  /*!< Enable both of the I2S built-in DAC channels.*/
    I2S_DAC_CHANNEL_MAX      = 0x4,  /*!< I2S built-in DAC mode max index*/
} i2s_dac_mode_t;

每个的意思如下,

I2S_DAC_CHANNEL_DISABLE 不使用内置i2s dac模式
I2S_DAC_CHANNEL_RIGHT_EN 使用i2s内置DAC右通道,映射到GPIO25上的DAC通道1
I2S_DAC_CHANNEL_LEFT_EN 使用i2s内置DAC左通道,映射到GPIO26上的DAC通道2
I2S_DAC_CHANNEL_BOTH_EN 同时启用两个内置DAC通道。
I2S_DAC_CHANNEL_MAX I2S内置DAC模式最大索引

当使用 I2S_DAC_CHANNEL_RIGHT_EN 只有GPIO25有音频输出;
使用 I2S_DAC_CHANNEL_LEFT_EN 只有GPIO26有音频输出;
使用 I2S_DAC_CHANNEL_BOTH_EN GPIO25和GPIO26都有音频输出

这里我们使用 I2S_DAC_CHANNEL_BOTH_EN ,同时配置两个DAC I2S输出,所以ESP32的D25 D26都有音频输出。

  1. I2S工作模式mode需添加I2S_MODE_DAC_BUILT_IN
    在这里插入图片描述
    I2S_MODE_DAC_BUILT_IN 也是我们第一篇文章介绍过的枚举类型 i2s_mode_t 里 其中一个参数,如下:
/**
 * @brief I2S Mode
 */
typedef enum {
    I2S_MODE_MASTER       = (0x1 << 0),       /*!< Master mode*/
    I2S_MODE_SLAVE        = (0x1 << 1),       /*!< Slave mode*/
    I2S_MODE_TX           = (0x1 << 2),       /*!< TX mode*/
    I2S_MODE_RX           = (0x1 << 3),       /*!< RX mode*/
#if SOC_I2S_SUPPORTS_DAC
    //built-in DAC functions are only supported on I2S0 for ESP32 chip.
    I2S_MODE_DAC_BUILT_IN = (0x1 << 4),       /*!< Output I2S data to built-in DAC, no matter the data format is 16bit or 32 bit, the DAC module will only take the 8bits from MSB*/
#endif // SOC_I2S_SUPPORTS_DAC
#if SOC_I2S_SUPPORTS_ADC
    I2S_MODE_ADC_BUILT_IN = (0x1 << 5),       /*!< Input I2S data from built-in ADC, each data can be 12-bit width at most*/
#endif // SOC_I2S_SUPPORTS_ADC
    // PDM functions are only supported on I2S0 (all chips).
    I2S_MODE_PDM          = (0x1 << 6),       /*!< I2S PDM mode*/
} i2s_mode_t;

/**


在本次使用内置DAC模块进行I2S音频输出时,.mode 应该包含I2S_MODE_DAC_BUILT_IN, 设置为.mode =(i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN)

  1. 配置I2S引脚 引脚设置为空 i2s_set_pin(I2S_NUM_0, NULL) 则初始化两个内置DAC,可通过ESP32的D25和D26输出音频。

在这里插入图片描述

综上我们可以知道, i2s_set_dac_mode和i2s_set_pin的区别是前者可以选择开启默认的哪个DAC通道(GPIO25或GPIO26),后者只能同时开启两个默认的DAC通道。

以下是乐鑫官方使用片上DAC进行I2S音频输出的应用示例:

在这里插入图片描述

根据上面介绍,配置I2S初始化如下,类似上面使用外部I2S音频输出的方式,起名为setupI2SDac();因为我们的音频采样率是8000Hz, 所以这里SAMPLE_RATE 定义为 8000

#define SAMPLE_RATE 8000  

void setupI2SDac() {
i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = I2S_COMM_FORMAT_STAND_MSB,
    .intr_alloc_flags = 0,
    .dma_buf_count = 16,
    .dma_buf_len = 60,
    .use_apll = false
  };

if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK || i2s_set_pin(I2S_NUM_0, NULL) != ESP_OK || i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN) != ESP_OK ) {
        Serial.println("I2S驱动安装失败");
        while (1);
    }
    Serial.println("I2S初始化成功");
 }

生成WAV音频并播放

接下来的步骤和使用外部I2S音频输出类似。这里生成了1秒正弦波WAV的同时进行循环播放。关于如何生成WAV音频文件及WAV文件头的相关参数如何填写,这篇文章【ESP32|音频】一文读懂WAV音频文件格式【详解】已经介绍,这里就不作解释了。

初始化SD卡:

#define SD_CS_PIN 5         // SD卡片选引脚

// 初始化 SD 卡
    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD卡初始化失败!");
        return;
    }
    Serial.println("SD卡初始化成功。");

生成WAV音频:

#define PI 3.1415926535     // π 值
#define WAV_HEADER_SIZE 44  // WAV文件头的大小

// 定义 WAV 头部结构体
struct WavHeader {
    char     riff[4] = {'R', 'I', 'F', 'F'};    // "RIFF"
    uint32_t chunkSize;                         // 文件大小 - 8
    char     wave[4] = {'W', 'A', 'V', 'E'};    // "WAVE"
    char     fmt[4] = {'f', 'm', 't', ' '};     // "fmt "
    uint32_t fmtChunkSize = 16;                 // fmt 块大小 (16 for PCM)
    uint16_t audioFormat = 1;                   // 音频格式 (1 = PCM)
    uint16_t numChannels = 1;                   // 声道数 (1 = 单声道)
    uint32_t sampleRate = SAMPLE_RATE;          // 采样率 (8000 Hz)
    uint32_t byteRate = SAMPLE_RATE * 2;        // 字节率 (sampleRate * numChannels * bitsPerSample / 8)
    uint16_t blockAlign = 2;                    // 块对齐 (numChannels * bitsPerSample / 8)
    uint16_t bitsPerSample = 16;                // 每样本位数 (16 bits)
    char     data[4] = {'d', 'a', 't', 'a'};    // "data"
    uint32_t dataSize;                          // 数据块大小
};

// 创建并打开文件
    File file = SD.open("/test.wav", FILE_WRITE);
    if (!file) {
        Serial.println("无法创建文件!");
        return;
    }

    // 计算音频数据大小
    const int numSamples = SAMPLE_RATE * 1;     // 1 秒的样本数
    const int bytesPerSample = 2;               // 16 位,每个样本 2 字节
    uint32_t dataSize = numSamples * bytesPerSample; // 数据大小:16000 字节
    uint32_t chunkSize = 36 + dataSize;         // 文件总大小 - 8:16036 字节
 

    // 写入 WAV 头部
    file.write(WavHeader, 44);

    // 生成并写入音频数据(440 Hz 正弦波)
    const int frequency = 440;
    for (int i = 0; i < numSamples; i++) {
        float time = (float)i / SAMPLE_RATE;
        int16_t sample = (int16_t)(32767.0 * sin(2.0 * PI * frequency * time));
        file.write((uint8_t*)&sample, 2);
    }

    // 关闭文件
    file.close();
    Serial.println("WAV文件写入完成。");

发送I2S数据

播放WAV音频,同理这个步骤和使用外部I2S进行音频输出一样,循环播放正弦波音乐,以下代码放在loop循环里面:

 File file = SD.open("/test.wav");  // 打开SD卡上的WAV文件
    if (!file) {
        Serial.println("无法打开文件");
        return;
    }
     byte wavHeader[WAV_HEADER_SIZE];
    file.read(wavHeader, WAV_HEADER_SIZE);
    
    // 确保是有效的WAV文件
    if (wavHeader[0] != 'R' || wavHeader[1] != 'I' || wavHeader[2] != 'F' || wavHeader[3] != 'F') {
        Serial.println("无效的WAV文件");
        file.close();
        return;
    }

    Serial.println("开始播放音频...");

    // 播放WAV文件的音频数据
    size_t bytesRead;
    uint8_t buffer[BUFFER_SIZE];
    while ((bytesRead = file.read(buffer, BUFFER_SIZE)) > 0) {
        // 将音频数据通过I2S传输到ESP32片上DAC
        size_t bytesWritten;
        i2s_write(I2S_NUM_0, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
    }

    Serial.println("音频播放完成");
    i2s_zero_dma_buffer(I2S_NUM_0);
    file.close();  // 关闭文件
    delay(1000);  // 播放完成后延迟1秒

将以上代码整合:

#include <SD.h>
#include <SPI.h>
#include <driver/i2s.h>
// 定义常量
#define SD_CS_PIN 5         // SD卡片选引脚
#define SAMPLE_RATE 8000    // 采样率(8000 Hz)
#define PI 3.1415926535     // π 值

#define BUFFER_SIZE  1024   // 每次读取的音频数据大小
#define WAV_HEADER_SIZE 44  // WAV文件头的大小
// WAV文件头部(44字节)
struct WavHeader {
    char     riff[4] = {'R', 'I', 'F', 'F'};    // "RIFF"
    uint32_t chunkSize;                         // 文件大小 - 8
    char     wave[4] = {'W', 'A', 'V', 'E'};    // "WAVE"
    char     fmt[4] = {'f', 'm', 't', ' '};     // "fmt "
    uint32_t fmtChunkSize = 16;                 // fmt 块大小 (16 for PCM)
    uint16_t audioFormat = 1;                   // 音频格式 (1 = PCM)
    uint16_t numChannels = 1;                   // 声道数 (1 = 单声道)
    uint32_t sampleRate = SAMPLE_RATE;          // 采样率 (8000 Hz)
    uint32_t byteRate = SAMPLE_RATE * 2;        // 字节率 (sampleRate * numChannels * bitsPerSample / 8)
    uint16_t blockAlign = 2;                    // 块对齐 (numChannels * bitsPerSample / 8)
    uint16_t bitsPerSample = 16;                // 每样本位数 (16 bits)
    char     data[4] = {'d', 'a', 't', 'a'};    // "data"
    uint32_t dataSize;                          // 数据块大小
};

void setupI2SDac() {
  
    i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = I2S_COMM_FORMAT_STAND_MSB,
    .intr_alloc_flags = 0,
    .dma_buf_count = 16,
    .dma_buf_len = 60,
    .use_apll = false
  };


	if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK ||
      	  i2s_set_pin(I2S_NUM_0, NULL) != ESP_OK || i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN) != ESP_OK ) {
      	  Serial.println("I2S驱动安装失败");
      	  while (1);
 	   }
 	   Serial.println("I2S初始化成功");
}

void setup() {
    Serial.begin(115200);
    setupI2SDac();

    // 初始化 SD 卡
    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD卡初始化失败!");
        return;
    }
    Serial.println("SD卡初始化成功。");


    // 创建并打开文件
    File wavFile = SD.open("/test.wav", FILE_WRITE);
    if (!wavFile) {
        Serial.println("无法创建文件!");
        return;
    }

    // 计算音频数据大小
    const int numSamples = SAMPLE_RATE * 1;     // 1 秒的样本数
    const int bytesPerSample = 2;               // 16 位,每个样本 2 字节
    uint32_t dataSize = numSamples * bytesPerSample; // 数据大小:16000 字节
    uint32_t chunkSize = 36 + dataSize;         // 文件总大小 - 8:16036 字节
 
 // 创建并初始化 WAV 头部
    WavHeader header;
    header.chunkSize = chunkSize;               // 设置 chunkSize
    header.dataSize = dataSize;                 // 设置 dataSize

    // 写入 WAV 头部
    wavFile.write((uint8_t*)&header, sizeof(header));
    // 生成并写入音频数据(440 Hz 正弦波)
    const int frequency = 440;
    for (int i = 0; i < numSamples; i++) {
        float time = (float)i / SAMPLE_RATE;
        int16_t sample = (int16_t)(32767.0 * sin(2.0 * PI * frequency * time));
        wavFile.write((uint8_t*)&sample, 2);
    }

    // 关闭文件
    wavFile.close();
    Serial.println("WAV文件写入完成。");

    
}

void loop() {
  File wavFile  = SD.open("/test.wav");  // 打开SD卡上的WAV文件
    if (!wavFile ) {
        Serial.println("无法打开文件");
        return;
    }
     byte wavHeader[WAV_HEADER_SIZE];
    wavFile.read(wavHeader, WAV_HEADER_SIZE);
    
    // 确保是有效的WAV文件
    if (wavHeader[0] != 'R' || wavHeader[1] != 'I' || wavHeader[2] != 'F' || wavHeader[3] != 'F') {
        Serial.println("无效的WAV文件");
        wavFile.close();
        return;
    }

    Serial.println("开始播放音频...");

    // 播放WAV文件的音频数据
    size_t bytesRead;
    uint8_t buffer[BUFFER_SIZE];
    while ((bytesRead = wavFile .read(buffer, BUFFER_SIZE)) > 0) {
        // 将音频数据通过I2S传输到ESP32片上DAC
        size_t bytesWritten;
        i2s_write(I2S_NUM_0, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
    }

    Serial.println("音频播放完成");
    i2s_zero_dma_buffer(I2S_NUM_0);
    wavFile .close();  // 关闭文件
    delay(1000);  // 播放完成后延迟1秒
}

上传代码后就可以从扬声器里听到循环播放的正弦波音频,其声音干净透彻,听起来类似于“滴滴”声。

总结

以上我们介绍了使用ESP32进行I2S音频输出的内容,可以使用外部I2S音频输出,也可以直接使用片上DAC进行I2S音频输出,两者在代码上只有细微区别,使用片上DAC进行I2S音频输出在进行初始化引脚的时候不太一样,除此之外都是一样的初始化步骤和流程,这个步骤在前面介绍过I2S音频输入部分的内容后,就比较清晰了,有相关疑问可参考往期相关文章。对I2S音频输入和I2S音频输出介绍后,下篇文章我们将介绍将两者结合起来进行一个应用,感兴趣的可以关注收藏一下。需要资料代码的可以评论留言。

Logo

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

更多推荐