簡介
上一篇我們利用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上電后錄音一次,且沒法實現控制,后面我們將給他加按鈕,顯示屏,以及完善錄音播放器的相關功能,感興趣的可以關注一波走起哦。需要完整代碼可評論區留言!