ESP32 I2S音频总线学习笔记(三):I2S音频输出
前面文章我们介绍了通过I2S读取INMP441音频数据,是关于I2S协议的数据读取,本篇我们介绍关于I2S协议的数据输出,通过I2S输出数据,即通过I2S进行音频输出。对于ESP32可以有两种I2S音频输出的方式,一种是 使用外部I2S进行音频输出,另一种是 使用片上DAC进行I2S音频输出。对于这两种方式,这里结合上篇文章对WAV文件格式的介绍,分别通过I2S协议播放不同音频,一个是我们自己放进
简介
前面文章我们介绍了通过I2S读取INMP441音频数据,是关于I2S协议的数据读取,本篇我们介绍关于I2S协议的数据输出,通过I2S输出数据,即通过I2S进行音频输出。对于ESP32可以有两种I2S音频输出的方式,一种是 使用外部I2S进行音频输出,另一种是 使用片上DAC进行I2S音频输出。对于这两种方式,这里结合上篇文章对WAV文件格式的介绍,分别通过I2S协议播放不同音频,一个是我们自己放进去SD卡里的WAV音频,另一个是我们上篇文章生成的1秒正弦波音频。没有看过对WAV格式介绍这篇文章的或者对WAV格式不了解的可以点击下面文章,
往期相关文章:
ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础
ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据
准备
- 主要硬件
根据我们不同方式的音频输出我们需要以下硬件:
| 硬件名称 | 数量 |
|---|---|
| ESP32 | 1 |
| SD卡模块 | 1 |
| PCM5102A立体声DAC模块 (可替换为其他DAC模块如MAX98357) |
1 |
| TDA2030A功放模块(可替换为其他功放模块) | 1 |
| 4欧3瓦喇叭 | 1 |
| 耳机或音频功放板 (用于PCM5102A音频输出) | 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)和喇叭的原因。



硬件接线

因为TDA2030A模拟功放的工作电压是6-12V,而ESP32的电压只有5V,所以需要进行外部供电,同样也可以接其他功放板进行音频输出。因为我们配置了两个内置DAC,所以TDA2030A的音频输入端IN既可以接D25,也可以接D26。
软件实现
I2S初始化
使用片上DAC进行I2S音频输出,和使用外部I2S进行音频输出,对I2S的初始化的整体内容是差不多的,只不过有一些区别。区别如下:
- 开启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都有音频输出。
- 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)
- 配置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音频输出介绍后,下篇文章我们将介绍将两者结合起来进行一个应用,感兴趣的可以关注收藏一下。需要资料代码的可以评论留言。
更多推荐



所有评论(0)