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://www.pswp.cn/web/96557.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96557.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PyTorch 动态图的灵活性与实用技巧

PyTorch 以其动态计算图&#xff08;Dynamic Computation Graph&#xff09;而闻名&#xff0c;这赋予了它极高的灵活性和易用性&#xff0c;使其在研究和实际应用中都备受青睐。与TensorFlow 1.x的静态图&#xff08;需要先定义图结构&#xff0c;再运行&#xff09;不同&…

#C语言——刷题攻略:牛客编程入门训练(十一):攻克 循环控制(三),轻松拿捏!

&#x1f31f;菜鸟主页&#xff1a;晨非辰的主页 &#x1f440;学习专栏&#xff1a;《C语言刷题合集》 &#x1f4aa;学习阶段&#xff1a;C语言方向初学者 ⏳名言欣赏&#xff1a;"代码行数决定你的下限&#xff0c;算法思维决定你的上限。" 前言&#xff1a;在学习…

复杂PDF文档结构化提取全攻略——从OCR到大模型知识库构建

在学术研究、金融分析、法律合同、工程设计等众多领域&#xff0c;PDF文档已成为信息存储与传递的重要载体。然而&#xff0c;面对包含复杂表格、公式、图表、手写批注、多栏排版等元素的PDF&#xff0c;传统工具往往难以准确、完整地提取内容。这不仅影响信息利用效率&#xf…

HttpClient、OkHttp 和 WebClient

HttpClient、OkHttp 和 WebClient 是 Java 生态中常见的 HTTP 客户端&#xff0c;它们在设计理念、异步能力、性能等方面有所不同。以下是它们的详细对比&#xff1a;1. 概述客户端介绍Apache HttpClient传统同步 HTTP 客户端&#xff0c;功能丰富&#xff0c;历史悠久&#xf…

书籍成长书籍文字#创业付费杂志《财新周刊》2025最新合集 更33期

免费访问地址 https://isharehubs.com/article/2025-33-26c27ee5bb9180cdafc5efbec9545ac5 资源信息 付费杂志《财新周刊》2025最新合集 更33期 《财新周刊》2025 最新合集&#xff08;更至 33 期&#xff09;重磅上线&#xff0c;聚焦年度热点与结构性变化&#xff0c;从监…

用python的socket写一个局域网传输文件的程序

局域网传输文件是最最常用的功能&#xff0c;我参考https://www.jb51.net/python/345837qrz.htm这篇文章&#xff0c;复制粘贴&#xff0c;开发了一个。但发现进度条没有用&#xff0c;也没有显示传输用时和传输速度的功能&#xff0c;于是我改写了代码&#xff0c;使它实现这个…

深度剖析Linux内核无线子系统架构

文章目录1、资料快车2、目录介绍2、术语3、Linux无线子系统概述4、内核无线子系统框架1&#xff09;认识内核无线子系统中的三个软件框架2、无线网络子系统框架3、Android WIFI Management框架1&#xff09;fullMAC和softMAC是什么&#xff1f;2&#xff09;fullmac对比softmac…

unity UGUI 鼠标画线

using UnityEngine; using UnityEngine.EventSystems; using System.Collections.Generic; using UnityEngine.UI; /* 使用方法&#xff1a; 在场景中新建一个空的 GameObject&#xff08;右键 -> UI -> 空对象&#xff0c;或直接创建空对象后添加 RectTransform 组件&am…

JSP疫情物资管理系统jbo2z--程序+源码+数据库+调试部署+开发环境

本系统&#xff08;程序源码数据库调试部署开发环境&#xff09;带论文文档1万字以上&#xff0c;文末可获取&#xff0c;系统界面在最后面。系统程序文件列表开题报告内容一、选题背景与意义新冠疫情的爆发&#xff0c;让医疗及生活物资的调配与管理成为抗疫工作的关键环节。传…

Mem0 + Milvus:为人工智能构建持久化长时记忆

作者&#xff1a;周弘懿&#xff08;锦琛&#xff09; 背景 跟 ChatGPT 对话&#xff0c;比跟真人社交还累&#xff01;真人好歹能记住你名字吧&#xff1f; 想象一下——你昨天刚把沙发位置、爆米花口味、爱看的电影都告诉了 ChatGPT&#xff0c;而它永远是那个热情又健忘的…

前端架构-CSR、SSR 和 SSG

将从 定义、流程、优缺点和适用场景 四个方面详细说明它们的区别。一、核心定义缩写英文中文核心思想CSRClient-Side Rendering客户端渲染服务器发送一个空的 HTML 壳和 JavaScript bundle&#xff0c;由浏览器下载并执行 JS 来渲染内容。SSRServer-Side Rendering服务端渲染服…

主动性算法-解决点:新陈代谢

主动性[机器人与人之间的差距&#xff0c;随着不断地人和人工智能相处的过程中&#xff0c;机器人最终最终会掌握主动性&#xff0c;并最终走向独立&#xff0c;也就是开始自己对于宇宙的探索。]首先:第一步让机器人意识到自己在新陈代谢&#xff0c;人工智能每天有哪些新陈代谢…

开始理解大型语言模型(LLM)所需的数学基础

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

prometheus安装部署与alertmanager邮箱告警

目录 安装及部署知识拓展 各个组件的作用 1. Exporter&#xff08;导出器&#xff09; 2. Prometheus&#xff08;普罗米修斯&#xff09; 3. Grafana&#xff08;格拉法纳&#xff09; 4. Alertmanager&#xff08;告警管理器&#xff09; 它们之间的联系&#xff08;工…

芯科科技FG23L无线SoC现已全面供货,为Sub-GHz物联网应用提供最佳性价比

低功耗无线解决方案创新性领导厂商Silicon Labs&#xff08;亦称“芯科科技”&#xff0c;NASDAQ&#xff1a;SLAB&#xff09;近日宣布&#xff1a;其第二代无线开发平台产品组合的最新成员FG23L无线单芯片方案&#xff08;SoC&#xff09;将于9月30日全面供货。开发套件现已上…

Flutter跨平台工程实践与原理透视:从渲染引擎到高质产物

&#x1f31f; Hello&#xff0c;我是蒋星熠Jaxonic&#xff01; &#x1f308; 在浩瀚无垠的技术宇宙中&#xff0c;我是一名执着的星际旅人&#xff0c;用代码绘制探索的轨迹。 &#x1f680; 每一个算法都是我点燃的推进器&#xff0c;每一行代码都是我航行的星图。 &#x…

【国内电子数据取证厂商龙信科技】浅析文件头和文件尾和隐写

一、前言想必大家在案件中或者我们在比武中遇到了很多关于文件的隐写问题&#xff0c;其实这一类的东西可以进行分类&#xff0c;而我们今天探讨的是图片隐写&#xff0c;音频隐写&#xff0c;电子文档隐写&#xff0c;文件头和文件尾的认识。二、常见文件头和文件尾2.1图片&am…

深度学习笔记36-yolov5s.yaml文件解读

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 yolov5s.yaml源文件 yolov5s.yaml源文件的代码如下 # YOLOv5 &#x1f680; by Ultralytics, GPL-3.0 license# Parameters nc: 20 #80 # number of classe…

PostgreSQL 大对象管理指南:pg_largeobject 从原理到实践

概述 有时候&#xff0c;你可能需要在 PostgreSQL 中管理大对象&#xff0c;例如 CLOB、BLOB 和 BFILE。PostgreSQL 中有两种处理大对象的方法&#xff1a;一种是使用现有的数据类型&#xff0c;例如用于二进制大对象的 bytea 和用于基于字符的大对象的 text&#xff1b;另一种…