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
英文地址,請注明出處:http://en.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;另一種…