当前位置: 首页 > news >正文

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:

ESP32INMP441
D13SCK
D12WS
D14SD
3.3VVDD
GNDGND

ESP32和PCM5102A:

ESP32PCM5102A
-VCC
3.3V3.3V
GNDGND
GNDFLT、DMP、SCL (这里SCL悬空可能会有干扰,所以接地)
D27BCK
D25DIN
D26LCK
GNDFMT
3.3VXMT

ESP32和SD模块接线:

ESP32SD模块
D5CS
D18SCK
D23MOSI
D19MISO
5VVCC
GNDGND

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音频文件。
在这里插入图片描述

注意事项

  1. 如果出现SD卡初始化失败的时候,有几个解决方法,一是需要重启sd卡模块,可以是断开给sd模块的供电然后再上电,或者拔插一下SD卡(亲测有用);二可以给SD卡模块外部供电,同时和ESP32共地,我自己实测可以,但是还是会有初始化失败出现,这个只能减小失败的概率。三是换一张SD卡,我自己测是换了一张卡可以大大减小初始化失败的概率,猜测是SD卡读取不稳定导致,建议用质量好一点的SD卡。还有一种原因可能是接线不稳定导致的。当然以上是我个人猜测,如果你们也遇到这个问题然后知道答案的可以评论区告诉下我~
  2. 本篇播放使用PCM5102A模块,需要接耳机或者AUX接功放板才能听到,你也可以使用MAX98357模块。使用这个模块接线也更简单了,只需要5根连接线即可。

总结

通过上面的步骤,我们已经实现录音播放功能了,但是缺点是这种方法只能在ESP32上电后录音一次,且没法实现控制,后面我们将给他加按钮,显示屏,以及完善录音播放器的相关功能,感兴趣的可以关注一波走起哦。需要完整代码可评论区留言!


文章转载自:

http://v7jKeEuG.Lsmcx.cn
http://H6pjYQ01.Lsmcx.cn
http://iVUYhlxh.Lsmcx.cn
http://aXCkKzPf.Lsmcx.cn
http://wWYL91Ag.Lsmcx.cn
http://f7qkrX2s.Lsmcx.cn
http://LGQXEKSj.Lsmcx.cn
http://jhH16iLY.Lsmcx.cn
http://YN3JgvfV.Lsmcx.cn
http://k8Q6C0an.Lsmcx.cn
http://dcoCQFxy.Lsmcx.cn
http://NIkk4F7h.Lsmcx.cn
http://HBrdKZqf.Lsmcx.cn
http://ZPK7hYRX.Lsmcx.cn
http://gorTT9dx.Lsmcx.cn
http://8UPm6SAs.Lsmcx.cn
http://fBtA39lZ.Lsmcx.cn
http://ODAg3U7O.Lsmcx.cn
http://aX6I72S9.Lsmcx.cn
http://FJyHqIIL.Lsmcx.cn
http://uvBl2E1W.Lsmcx.cn
http://Pn42mfER.Lsmcx.cn
http://beyBhICA.Lsmcx.cn
http://DSqwqfsO.Lsmcx.cn
http://cpn9jagG.Lsmcx.cn
http://DYYMrxa5.Lsmcx.cn
http://k8MqarGm.Lsmcx.cn
http://xIElAoqp.Lsmcx.cn
http://2qkLNPvW.Lsmcx.cn
http://eDAo0Z26.Lsmcx.cn
http://www.dtcms.com/a/378967.html

相关文章:

  • Shell编程:计算Linux主机用户id总和
  • 【Leetcode】高频SQL基础题--196.删除重复的电子邮箱
  • SpreadJS V18.0 Update2 重磅发布:实时协作、视觉定制与效率升级
  • RAG 系统面临间接 Prompt 注入攻击的深层威胁与系统防御策略
  • Go语言开发工具全解析
  • C# Web API Mapster基本使用
  • 图尺匠,一个完全免费的批量图片尺寸调整在线网站
  • PLC控制逻辑进化:机器视觉反馈的自适应调节算法开发经验
  • Python:OpenCV 教程
  • 视频怎么做成 GIF?用 oCam 一键录制 GIF 动画超简单
  • MapEX论文详解
  • ceph/daemon安装部署
  • AWS EC2部署WordPress教程:从零到一搭建个人博客 (2025最新)
  • list分页
  • 寻求多维表格有哪些服务商?Teable、飞书、WPS、简道云和Airtable
  • 6-获取磁盘分区信息
  • GRASP 实验室研究 论文解读 | 机器人交互:基于神经网络引导变分推理的快速失配估计
  • 元宇宙与金融创新:虚实融合下的金融服务新形态
  • 【基于协同过滤的校园二手交易平台】
  • Oracle APEX 定型文(快速选取功能)
  • 小说创作中的时间轴体验设计:事序图交互与用户体验优化
  • Liunx执行source /etc/profile 报错, -bash: HISTTIMEFORMAT: readonly variable
  • js的事件循环机制的理解
  • MATLAB基于博弈论-云模型的城市道路塌陷风险评价模型
  • 医保购药平台如何对接互联网医院系统源码?技术难点与解决方案
  • 景观设计师的数字画笔:园林景观设计软件有哪些
  • 配置docker常见问题
  • 华宇TAS应用中间件与瀚高股份两款产品完成兼容互认证
  • 详解 C++11
  • GS1-128 校验码计算方法