ESP32 I2S音频总线学习笔记(七):制作一个录音播放器
简介
上一篇我们利用I2S输出DIY了一个蓝牙音箱简单玩了一下,本篇我们继续来看代码。前面几篇文章我们分别介绍了I2S输入,I2S输出,以及WAV文件格式的相关内容,那我们就可以根据所学到的,制作一个录音机,具体效果是使用I2S协议进行录音并将其存储在SD卡里,而且我们还可以将存储的内容直接播放出来,这样就制作出了一个录音播放器。在之前采用I2S输出的时候,是使用软件生成的正弦波音频来进行音频播放,本文我们将直接使用从麦克风采集到的音频,存储在SD卡里实现录音并播放。这样就把前面学到的结合在一起了,没看过往期相关文章的小伙伴可以点击下方链接查看。
往期相关文章:
ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础
ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据
ESP32 I2S音频总线学习笔记(三):I2S音频输出
ESP32 I2S音频总线学习笔记(四):INMP441采集音频并实时播放
ESP32 I2S音频总线学习笔记(五):将inmp441采集到的音频发送至网络
ESP32 I2S音频总线学习笔记(六):DIY蓝牙音箱教程
【ESP32|音频】一文读懂WAV音频文件格式【详解】
主要硬件
ESP32主控:
INMP441全向麦克风模块:
PCM5102A 立体声DAC模块 :
SD卡模块:
硬件接线
ESP32和麦克风INMP441:
ESP32 | INMP441 |
---|---|
D13 | SCK |
D12 | WS |
D14 | SD |
3.3V | VDD |
GND | GND |
ESP32和PCM5102A:
ESP32 | PCM5102A |
---|---|
- | VCC |
3.3V | 3.3V |
GND | GND |
GND | FLT、DMP、SCL (这里SCL悬空可能会有干扰,所以接地) |
D27 | BCK |
D25 | DIN |
D26 | LCK |
GND | FMT |
3.3V | XMT |
ESP32和SD模块接线:
ESP32 | SD模块 |
---|---|
D5 | CS |
D18 | SCK |
D23 | MOSI |
D19 | MISO |
5V | VCC |
GND | GND |
i2s输入实现录音
采集音频样本
首先是包含必要的头文件,这里因为使用到了SD卡,所以要包含对应的库。
#include <SD.h>
#include <driver/i2s.h>
然后是对SD相关初始化:
// SD卡引脚配置
#define SD_CS_PIN 5// 初始化SD卡if (!SD.begin(SD_CS_PIN)) {Serial.println("SD卡初始化失败");while (1);}Serial.println("SD卡初始化成功");
麦克风i2s输入的相关初始化,具体初始化步骤可以查看:ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据
这里直接给出麦克风i2s初始化代码:
// 配置 I2S0 用于麦克风采集
#define I2S_MIC_NUM I2S_NUM_0
#define I2S_MIC_BCK 13 // 位时钟引脚(BCK)用于麦克风
#define I2S_MIC_WS 12 // 字选择引脚(WS)用于麦克风
#define I2S_MIC_SD 14 // 数据输入引脚(SD)用于麦克风void setupI2SMic() {// 初始化I2S输入(麦克风)i2s_config_t mic_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位采样深度.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道左通道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE, };i2s_pin_config_t mic_pin_config = {.bck_io_num = I2S_MIC_BCK, // 麦克风位时钟引脚.ws_io_num = I2S_MIC_WS, // 麦克风字选择引脚.data_out_num = -1,.data_in_num = I2S_MIC_SD // 麦克风数据输入引脚};// 安装I2S驱动并配置引脚(麦克风)if (i2s_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL) != ESP_OK ||i2s_set_pin(I2S_MIC_NUM, &mic_pin_config) != ESP_OK) { Serial.println("麦克风I2S驱动安装失败");while (1);}
}
为了方便观察i2s是否初始化成功,在setup函数添加i2s初始化调试信息:
Serial.println("I2S初始化成功");delay(1000);
初始化完成后,我们可以先从I2S读取麦克风数据,调用esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);
那这里读取数据,我们要读多久呢,比如录音30秒吧,那就在30秒内,持续采集音频样本。在这里我们配置采样深度为 I2S_BITS_PER_SAMPLE_16BIT
,所以每个样本是2字节,所以我们定义一个2字节的缓存区数组buffer
来存储读取的音频样本,数组长度为BUFFER_SIZE
=1024,即每次处理1024个样本。然后采样率的话我们上面初始化配置的是44100Hz,SAMPLE_RATE
乘以录音时间 RECORD_TIME
=30s,得到音频总样本数total_samples
(注意这里只是预估),当前采样音频总样本数小于目标音频总样本数时,持续采集音频样本,同样这里和之前一样,需要进行增益调整,这里就不解释了。
// 音频采样参数
#define SAMPLE_RATE 44100
#define BUFFER_SIZE 1024 // 缓冲区大小
#define RECORD_TIME 30 // 录音时长(秒)size_t bytes_read;
int16_t buffer[BUFFER_SIZE];
uint32_t total_samples = 0;void loop() {while (total_samples < SAMPLE_RATE * RECORD_TIME) {// 从I2S读取数据(麦克风)i2s_read(I2S_NUM_0, buffer, BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);// 增益调整for (int i = 0; i < bytes_read / sizeof(int16_t); i++) {buffer[i] = buffer[i] * 20; // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767; if (buffer[i] < -32768) buffer[i] = -32768;} }
}
写入SD卡
因为我们需要录音,所以还要将采集到的样本,写入SD卡里。这个实现步骤,在【ESP32|音频】一文读懂WAV音频文件格式【详解】 这篇文章中有提及到,里面介绍了如何使用ESP32将WAV文件写入SD卡,所以我们将从麦克风采集到的音频样本保存为WAV文件格式以进行存储。
首先需要定义WAV文件头结构
struct WavHeader {char riff[4] = {'R','I','F','F'};uint32_t chunkSize;char wave[4] = {'W','A','V','E'};char fmt[4] = {'f','m','t',' '};uint32_t fmtChunkSize = 16;uint16_t audioFormat = 1;uint16_t numChannels = 1;uint32_t sampleRate = SAMPLE_RATE;uint32_t byteRate = SAMPLE_RATE * 2;uint16_t blockAlign = 2;uint16_t bitsPerSample = 16;char data[4] = {'d','a','t','a'};uint32_t dataSize;
};
创建WAV文件用来存储音频样本:
// 创建WAV文件File file = SD.open("/audio.wav", FILE_WRITE);if (!file) {Serial.println("文件打开失败");return;}
写入WAV文件头:
// 写入WAV文件头WavHeader header;header.dataSize = RECORD_TIME * SAMPLE_RATE * 2;header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));
录音并写入SD卡 :
这里写入SD卡还是使用前面介绍的size_t write(const uint8_t *buf, size_t size)
函数,这时候的total_samples是实际读取到的音频总样本数,它等于实际读取到的总字节数除以单个样本字节数。
// 处理数据,将16位音频数据写入SD卡file.write((uint8_t*)buffer, bytes_read);total_samples += bytes_read / sizeof(int16_t);
更新WAV文件头:
// 更新WAV文件头file.seek(0);header.dataSize = total_samples * sizeof(int16_t);header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));file.close();Serial.println("录音完成,文件已保存至SD卡");
仅录音的完整代码如下:
#include <SD.h>
#include <driver/i2s.h>// SD卡引脚配置
#define SD_CS_PIN 5// 配置 I2S0 用于麦克风采集
#define I2S_MIC_NUM I2S_NUM_0
#define I2S_MIC_BCK 13 // 位时钟引脚(BCK)用于麦克风
#define I2S_MIC_WS 12 // 字选择引脚(WS)用于麦克风
#define I2S_MIC_SD 14 // 数据输入引脚(SD)用于麦克风// 音频采样参数
#define SAMPLE_RATE 44100
#define BUFFER_SIZE 1024 // 缓冲区大小
#define RECORD_TIME 30 // 录音时长(秒)
#define WAV_HEADER_SIZE 44 // WAV文件头的大小size_t bytes_read;
int16_t buffer[BUFFER_SIZE];
uint32_t total_samples = 0;// WAV文件头结构
struct WavHeader {char riff[4] = {'R','I','F','F'};uint32_t chunkSize;char wave[4] = {'W','A','V','E'};char fmt[4] = {'f','m','t',' '};uint32_t fmtChunkSize = 16;uint16_t audioFormat = 1;uint16_t numChannels = 1;uint32_t sampleRate = SAMPLE_RATE;uint32_t byteRate = SAMPLE_RATE * 2;uint16_t blockAlign = 2;uint16_t bitsPerSample = 16;char data[4] = {'d','a','t','a'};uint32_t dataSize;
};void setupI2SMic() {// 初始化I2S输入(麦克风)i2s_config_t mic_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位采样深度.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道左通道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE, };i2s_pin_config_t mic_pin_config = {.bck_io_num = I2S_MIC_BCK, // 麦克风位时钟引脚.ws_io_num = I2S_MIC_WS, // 麦克风字选择引脚.data_out_num = -1,.data_in_num = I2S_MIC_SD // 麦克风数据输入引脚};// 安装I2S驱动并配置引脚(麦克风)if (i2s_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL) != ESP_OK ||i2s_set_pin(I2S_MIC_NUM, &mic_pin_config) != ESP_OK) { Serial.println("麦克风I2S驱动安装失败");while (1);}
}void setup() {Serial.begin(115200);// 初始化SD卡if (!SD.begin(SD_CS_PIN)) {Serial.println("SD卡初始化失败");while (1);}Serial.println("SD卡初始化成功");setupI2SMic(); Serial.println("I2S初始化成功");delay(1000);
}void loop() {// 创建WAV文件File file = SD.open("/audio.wav", FILE_WRITE);if (!file) {Serial.println("文件打开失败");return;}// 写入WAV文件头WavHeader header;header.dataSize = RECORD_TIME * SAMPLE_RATE * 2;header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));// 录音并写入SD卡 Serial.println("开始录音...");while (total_samples < SAMPLE_RATE * RECORD_TIME) {// 从I2S读取数据(麦克风)i2s_read(I2S_NUM_0, buffer, BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);// 增益调整for (int i = 0; i < bytes_read / sizeof(int16_t); i++) {buffer[i] = buffer[i] * 20; // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767; if (buffer[i] < -32768) buffer[i] = -32768;}// 处理数据,将16位音频数据写入SD卡file.write((uint8_t*)buffer, bytes_read);total_samples += bytes_read / sizeof(int16_t); }// 更新WAV文件头file.seek(0);header.dataSize = total_samples * sizeof(int16_t);header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));file.close();Serial.println("录音完成,文件已保存至SD卡"); // 程序完成,进入无限循环while (1);
}
i2s输出实现播放
录音完写入SD卡之后,如果我们要知道录音的内容,需要读卡器去读取,这样就比较麻烦,能不能录音完写入SD卡后,进行播放呢?这就要用到我们的i2s dac输出了。
实现播放的话有两种,一种录音的时候实时播放我们正在说话的内容,同时保存音频到SD卡,我称为实时录音;另一种是录音后进行播放。根据不同功能实现,我们可以有四种组合:
仅录音 | 录音后播放 |
---|---|
实时录音 | 实时录音且播放 |
仅录音:参考上面i2s输入实现录音。
录音后播放:
如果要在录音后进行播放SD卡的音频文件的话,我们只需在录音完成后将SD卡文件打开进行相关操作。PCM5102A i2s输出的相关初始化,具体初始化步骤可以查看:ESP32 I2S音频总线学习笔记(三):I2S音频输出 这篇文章里使用外部I2S进行音频输出的部分
#include <driver/i2s.h>
// 配置 I2S1 用于 DAC 输出
#define I2S_DAC_NUM I2S_NUM_1
#define I2S_DAC_BCK 27 // 位时钟引脚(BCK)用于PCM5102A
#define I2S_DAC_WS 26 // 字选择引脚(WS)用于PCM5102A
#define I2S_DAC_DIN 25 // 数据输出引脚(SD)用于PCM5102Avoid setupI2SDac() {// 初始化I2S输出(PCM5102A)i2s_config_t dac_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位采样深度.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道左通道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE,.use_apll = false};i2s_pin_config_t dac_pin_config = {.bck_io_num = I2S_DAC_BCK, // PCM5102A位时钟引脚.ws_io_num = I2S_DAC_WS, // PCM5102A字选择引脚.data_out_num = I2S_DAC_DIN, // PCM5102A数据输出引脚.data_in_num = I2S_PIN_NO_CHANGE};// 安装I2S驱动并配置引脚(PCM5102A)if (i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL) != ESP_OK ||i2s_set_pin(I2S_DAC_NUM, &dac_pin_config) != ESP_OK) {Serial.println("PCM5102A I2S驱动安装失败");while (1);}
}void setup() {Serial.begin(115200); setupI2SDac();Serial.println("I2S初始化成功");delay(1000);}
在我们将录音文件保存至SD卡后,将其打开进行播放,还是参考ESP32 I2S音频总线学习笔记(三):I2S音频输出
File audioFile = SD.open("/audio.wav"); // 打开SD卡上的WAV文件if (!audioFile) {Serial.println("无法打开文件");return;}byte wavHeader[WAV_HEADER_SIZE];audioFile.read(wavHeader, WAV_HEADER_SIZE);
while ((bytes_read = audioFile.read((uint8_t*)buffer, BUFFER_SIZE)) > 0) {// 将音频数据通过I2S传输到PCM5102Asize_t bytesWritten;i2s_write(I2S_NUM_1, buffer, bytes_read, &bytesWritten, portMAX_DELAY);}Serial.println("播放完成");i2s_zero_dma_buffer(I2S_NUM_1);audioFile.close(); // 关闭文件delay(1000); // 播放完成后延迟1秒
录音后播放完整代码:
#include <SD.h>
#include <driver/i2s.h>// SD卡引脚配置
#define SD_CS_PIN 5// 配置 I2S0 用于麦克风采集
#define I2S_MIC_NUM I2S_NUM_0
#define I2S_MIC_BCK 13 // 位时钟引脚(BCK)用于麦克风
#define I2S_MIC_WS 12 // 字选择引脚(WS)用于麦克风
#define I2S_MIC_SD 14 // 数据输入引脚(SD)用于麦克风// 配置 I2S1 用于 DAC 输出
#define I2S_DAC_NUM I2S_NUM_1
#define I2S_DAC_BCK 27 // 位时钟引脚(BCK)用于PCM5102A
#define I2S_DAC_WS 26 // 字选择引脚(WS)用于PCM5102A
#define I2S_DAC_DIN 25 // 数据输出引脚(SD)用于PCM5102A// 音频采样参数
#define SAMPLE_RATE 44100
#define BUFFER_SIZE 1024 // 缓冲区大小
#define RECORD_TIME 30 // 录音时长(秒)
#define WAV_HEADER_SIZE 44 // WAV文件头的大小size_t bytes_read;
int16_t buffer[BUFFER_SIZE];
uint32_t total_samples = 0;// WAV文件头结构
struct WavHeader {char riff[4] = {'R','I','F','F'};uint32_t chunkSize;char wave[4] = {'W','A','V','E'};char fmt[4] = {'f','m','t',' '};uint32_t fmtChunkSize = 16;uint16_t audioFormat = 1;uint16_t numChannels = 1;uint32_t sampleRate = SAMPLE_RATE;uint32_t byteRate = SAMPLE_RATE * 2;uint16_t blockAlign = 2;uint16_t bitsPerSample = 16;char data[4] = {'d','a','t','a'};uint32_t dataSize;
};void setupI2SMic() {// 初始化I2S输入(麦克风)i2s_config_t mic_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位采样深度.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道左通道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE, };i2s_pin_config_t mic_pin_config = {.bck_io_num = I2S_MIC_BCK, // 麦克风位时钟引脚.ws_io_num = I2S_MIC_WS, // 麦克风字选择引脚.data_out_num = -1,.data_in_num = I2S_MIC_SD // 麦克风数据输入引脚};// 安装I2S驱动并配置引脚(麦克风)if (i2s_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL) != ESP_OK ||i2s_set_pin(I2S_MIC_NUM, &mic_pin_config) != ESP_OK) {Serial.println("麦克风I2S驱动安装失败");while (1);}
}void setupI2SDac() {// 初始化I2S输出(PCM5102A)i2s_config_t dac_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16位采样深度.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道左通道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE,.use_apll = false};i2s_pin_config_t dac_pin_config = {.bck_io_num = I2S_DAC_BCK, // PCM5102A位时钟引脚.ws_io_num = I2S_DAC_WS, // PCM5102A字选择引脚.data_out_num = I2S_DAC_DIN, // PCM5102A数据输出引脚.data_in_num = I2S_PIN_NO_CHANGE};// 安装I2S驱动并配置引脚(PCM5102A)if (i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL) != ESP_OK ||i2s_set_pin(I2S_DAC_NUM, &dac_pin_config) != ESP_OK) {Serial.println("PCM5102A I2S驱动安装失败");while (1);}
}void setup() {Serial.begin(115200);// 初始化SD卡if (!SD.begin(SD_CS_PIN)) {Serial.println("SD卡初始化失败");while (1);}Serial.println("SD卡初始化成功");setupI2SMic();setupI2SDac();Serial.println("I2S初始化成功");delay(1000);}void loop() {// 创建WAV文件File file = SD.open("/audio.wav", FILE_WRITE);if (!file) {Serial.println("文件打开失败");return;}// 写入WAV文件头WavHeader header;header.dataSize = RECORD_TIME * SAMPLE_RATE * 2;header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));// 录音并写入SD卡 Serial.println("开始录音...");while (total_samples < SAMPLE_RATE * RECORD_TIME) {// 从I2S读取数据(麦克风)i2s_read(I2S_NUM_0, buffer, BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);// 增益调整for (int i = 0; i < bytes_read / sizeof(int16_t); i++) {buffer[i] = buffer[i] * 20; // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}// 处理数据,将16位音频数据写入SD卡file.write((uint8_t*)buffer, bytes_read);total_samples += bytes_read / sizeof(int16_t); }// 更新WAV文件头file.seek(0);header.dataSize = total_samples * sizeof(int16_t);header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));file.close();Serial.println("录音完成,文件已保存至SD卡");File audioFile = SD.open("/audio.wav"); // 打开SD卡上的WAV文件if (!audioFile) {Serial.println("无法打开文件");return;}byte wavHeader[WAV_HEADER_SIZE];audioFile.read(wavHeader, WAV_HEADER_SIZE);
while ((bytes_read = audioFile.read((uint8_t*)buffer, BUFFER_SIZE)) > 0) {// 将音频数据通过I2S传输到PCM5102Asize_t bytesWritten;i2s_write(I2S_NUM_1, buffer, bytes_read, &bytesWritten, portMAX_DELAY);}Serial.println("播放完成");i2s_zero_dma_buffer(I2S_NUM_1);audioFile.close(); // 关闭文件delay(1000); // 播放完成后延迟1秒// 程序完成,进入无限循环while (1);
}
实时录音:
在录音的过程进行播放,其实只需要添加一条代码,即前面介绍的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);
部分代码:
void loop() {// 创建WAV文件File file = SD.open("/audio.wav", FILE_WRITE);if (!file) {Serial.println("文件打开失败");return;}// 写入WAV文件头WavHeader header;header.dataSize = RECORD_TIME * SAMPLE_RATE * 2;header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));// 录音并写入SD卡 Serial.println("开始录音...");while (total_samples < SAMPLE_RATE * RECORD_TIME) {// 从I2S读取数据(麦克风)i2s_read(I2S_NUM_0, buffer, BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);// 增益调整for (int i = 0; i < bytes_read / sizeof(int16_t); i++) {buffer[i] = buffer[i] * 20; // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}// 处理数据,将16位音频数据写入SD卡file.write((uint8_t*)buffer, bytes_read);total_samples += bytes_read / sizeof(int16_t);// 实时播放录音(通过PCM5102A)i2s_write(I2S_NUM_1, buffer, bytes_read, &bytes_read, portMAX_DELAY);}// 更新WAV文件头file.seek(0);header.dataSize = total_samples * sizeof(int16_t);header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));file.close();Serial.println("录音完成,文件已保存至SD卡");i2s_zero_dma_buffer(I2S_NUM_1);// 停止I2S驱动//i2s_driver_uninstall(I2S_NUM_0);// i2s_driver_uninstall(I2S_NUM_1);// 程序完成,进入无限循环while (1);
}
实时录音且播放:
在录音的过程进行播放,并且结束后自动播放一次,还是和录音后播放一样的代码,同时实时录音添加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);
部分代码:
void loop() {// 创建WAV文件File file = SD.open("/audio.wav", FILE_WRITE);if (!file) {Serial.println("文件打开失败");return;}// 写入WAV文件头WavHeader header;header.dataSize = RECORD_TIME * SAMPLE_RATE * 2;header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));// 录音并写入SD卡 Serial.println("开始录音...");while (total_samples < SAMPLE_RATE * RECORD_TIME) {// 从I2S读取数据(麦克风)i2s_read(I2S_NUM_0, buffer, BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);// 增益调整for (int i = 0; i < bytes_read / sizeof(int16_t); i++) {buffer[i] = buffer[i] * 20; // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}// 处理数据,将16位音频数据写入SD卡file.write((uint8_t*)buffer, bytes_read);total_samples += bytes_read / sizeof(int16_t);// 实时播放录音(通过PCM5102A)i2s_write(I2S_NUM_1, buffer, bytes_read, &bytes_read, portMAX_DELAY);}// 更新WAV文件头file.seek(0);header.dataSize = total_samples * sizeof(int16_t);header.chunkSize = sizeof(WavHeader) - 8 + header.dataSize;file.write((uint8_t*)&header, sizeof(header));file.close();Serial.println("录音完成,文件已保存至SD卡");File audioFile = SD.open("/audio.wav"); // 打开SD卡上的WAV文件if (!audioFile) { Serial.println("无法打开文件");return;}byte wavHeader[WAV_HEADER_SIZE];audioFile.read(wavHeader, WAV_HEADER_SIZE);
while ((bytes_read = audioFile.read((uint8_t*)buffer, BUFFER_SIZE)) > 0) {// 将音频数据通过I2S传输到PCM5102Asize_t bytesWritten;i2s_write(I2S_NUM_1, buffer, bytes_read, &bytesWritten, portMAX_DELAY);}Serial.println("播放完成");i2s_zero_dma_buffer(I2S_NUM_1);audioFile.close(); // 关闭文件delay(1000); // 播放完成后延迟1秒// 停止I2S驱动//i2s_driver_uninstall(I2S_NUM_0);// i2s_driver_uninstall(I2S_NUM_1); // 程序完成,进入无限循环while (1);
}
实际现象
打开串口监视器,可以看到相关初始化成功后开始录音,录音完成后会进行播放。
使用读卡器读取U盘里面的内容,也可以看到录音后的WAV音频文件。
注意事项
- 如果出现SD卡初始化失败的时候,有几个解决方法,一是需要重启sd卡模块,可以是断开给sd模块的供电然后再上电,或者拔插一下SD卡(亲测有用);二可以给SD卡模块外部供电,同时和ESP32共地,我自己实测可以,但是还是会有初始化失败出现,这个只能减小失败的概率。三是换一张SD卡,我自己测是换了一张卡可以大大减小初始化失败的概率,猜测是SD卡读取不稳定导致,建议用质量好一点的SD卡。还有一种原因可能是接线不稳定导致的。当然以上是我个人猜测,如果你们也遇到这个问题然后知道答案的可以评论区告诉下我~
- 本篇播放使用PCM5102A模块,需要接耳机或者AUX接功放板才能听到,你也可以使用MAX98357模块。使用这个模块接线也更简单了,只需要5根连接线即可。
总结
通过上面的步骤,我们已经实现录音播放功能了,但是缺点是这种方法只能在ESP32上电后录音一次,且没法实现控制,后面我们将给他加按钮,显示屏,以及完善录音播放器的相关功能,感兴趣的可以关注一波走起哦。需要完整代码可评论区留言!