簡介
前面兩期文章我們介紹了I2S的讀取和寫入,一個是通過INMP441麥克風模塊采集音頻,一個是通過PCM5102A模塊播放音頻,那如果我們將兩者結合起來,將麥克風采集到的音頻通過PCM5102A播放,是不是就可以做一個擴音器了呢,本篇將介紹一個INMP441采集音頻并實時播放的應用。
往期相關文章:
ESP32 I2S音頻總線學習筆記(一):初識I2S通信與配置基礎
ESP32 I2S音頻總線學習筆記(二):I2S讀取INMP441音頻數據
ESP32 I2S音頻總線學習筆記(三):I2S音頻輸出
主要硬件
INMP441全向麥克風模塊:
PCM5102A 立體聲DAC模塊 :
硬件接線
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 |
軟件實現
前面兩篇文章我們詳細介紹了I2S讀取和I2S輸出的初始化步驟,所以本篇我們就不過多介紹了。我們的目的是實現INMP441采集音頻并通過PCM5102A實時播放(可替換為其他DAC模塊如MAX98357),使用的協議是I2S,所以首先要分別配置麥克風I2S初始化和音頻播放模塊I2S初始化,前者我們起名為setupI2SMic( )
,后者起名為setupI2SDac( )
;因為我們是循環采集音頻,所以音頻處理邏輯部分放在loop( )函數,首先搭建起整體框架,注意必不可少的是包含I2S驅動頭文件:
#include <driver/i2s.h>void setup() {Serial.begin(115200);setupI2SMic();setupI2SDac();
}void loop()
{/*音頻處理邏輯部分...待補充*/
}
搭建完整體框架后,我們再來完善setupI2SMic( )和setupI2SDac( )里面的內容。
INMP441讀取 I2S初始化
setupI2SMic( )里面的I2S初始化步驟如何配置,可參考往期第二篇文章。這里因為使用到了兩個I2S,一個用于麥克風采集,一個用于播放音頻,所以我們將I2S0用于麥克風,I2S1用于音頻的實時播放。
// 配置 I2S0 用于麥克風采集
#define I2S_MIC_NUM I2S_NUM_0
#define I2S_MIC_BCK 13
#define I2S_MIC_WS 12
#define I2S_MIC_SD 14#define SAMPLE_RATE 44100void setupI2SMic() {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,.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 緩沖區數量.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_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL);i2s_set_pin(I2S_MIC_NUM, &mic_pin_config);
}
PCM5102A輸出 I2S初始化
// 配置 I2S1 用于 DAC 輸出
#define I2S_DAC_NUM I2S_NUM_1
#define I2S_DAC_BCK 27
#define I2S_DAC_WS 26
#define I2S_DAC_DIN 25void setupI2SDac() {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,.channel_format = I2S_CHANNEL_FMT_RIGHT_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 dac_pin_config = {.bck_io_num = I2S_DAC_BCK,.ws_io_num = I2S_DAC_WS,.data_out_num = I2S_DAC_DIN,.data_in_num = -1};i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL);i2s_set_pin(I2S_DAC_NUM, &dac_pin_config);
}
音頻處理邏輯部分
音頻處理邏輯部分主要步驟是從麥克風采集數據,然后播放音頻數據,需要用到前面講過的兩個函數:esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);
和 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(){/*音頻處理邏輯部分 */// 從麥克風采集數據// 增益處理,放大音量并限制范圍// 單聲道轉立體聲// 播放音頻數據
}
首先定義兩個音頻緩沖區,buffer 用于單聲道輸入,stereo_buffer 用于立體聲輸出。BUFFER_SIZE定義為單聲道緩沖區大小1024,每次讀取1024個樣本,雙聲道緩存區的樣本數是單聲道緩存區樣本數的兩倍,所以為BUFFER_SIZE * 2。
#define BUFFER_SIZE 1024int16_t buffer[BUFFER_SIZE]; // 單聲道緩沖區
int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立體聲緩沖區
然后從麥克風采集數據:
i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);
將采集到的音頻數據進行增益處理,這里實測增益因子20~50比較合適,太大了采集到的音頻聲音會大,但是近距離說話的時候會容易出現破音,對遠處的采集聲音較好。溢出保護是因為16 位音頻范圍為 -32768 到 32767,放大后若超出此范圍就會導致失真。
for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20; // 增益因子為20,可以根據需要調整// 增加溢出保護if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}
放大音頻數據后將單聲道音頻數據轉化為立體聲 音頻數據,單聲道只有一個聲道的數據,立體聲需要左右兩個聲道,這里將單聲道樣本復制到左右聲道。
for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i]; // 左聲道stereo_buffer[2 * i + 1] = buffer[i]; // 右聲道}
通過 I2S1 接口將立體聲數據發送到PCM5102A播放。
i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);
音頻處理邏輯部分的代碼如下,這個步驟可以總結為:1. 采集單聲道音頻 → 2. 放大音量并限制范圍 → 3. 轉換為立體聲 → 4. 播放
void loop() {int16_t buffer[BUFFER_SIZE]; // 單聲道緩沖區int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立體聲緩沖區size_t bytesRead, bytesWritten;// 從麥克風采集數據i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);// 增益處理,放大音量并限制范圍for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20; // 增益因子為20,可以根據需要調整// 增加溢出保護if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}// 單聲道轉立體聲for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i]; // 左聲道stereo_buffer[2 * i + 1] = buffer[i]; // 右聲道}// 播放音頻數據i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);
}
全部整合后的代碼如下:
#include <driver/i2s.h>// 配置 I2S0 用于麥克風采集
#define I2S_MIC_NUM I2S_NUM_0
#define I2S_MIC_BCK 13
#define I2S_MIC_WS 12
#define I2S_MIC_SD 14// 配置 I2S1 用于 DAC 輸出
#define I2S_DAC_NUM I2S_NUM_1
#define I2S_DAC_BCK 27
#define I2S_DAC_WS 26
#define I2S_DAC_DIN 25#define SAMPLE_RATE 44100
#define BUFFER_SIZE 1024void setupI2SMic() {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,.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 緩沖區數量.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_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL);i2s_set_pin(I2S_MIC_NUM, &mic_pin_config);
}void setupI2SDac() {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,.channel_format = I2S_CHANNEL_FMT_RIGHT_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 dac_pin_config = {.bck_io_num = I2S_DAC_BCK,.ws_io_num = I2S_DAC_WS,.data_out_num = I2S_DAC_DIN,.data_in_num = -1};i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL);i2s_set_pin(I2S_DAC_NUM, &dac_pin_config);
}void setup() {Serial.begin(115200);setupI2SMic();setupI2SDac();
}void loop() {int16_t buffer[BUFFER_SIZE]; // 單聲道緩沖區int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立體聲緩沖區size_t bytesRead, bytesWritten;// 從麥克風采集數據i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);// 增益處理,放大音量并限制范圍for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20; // 增益因子為20,可以根據需要調整// 增加溢出保護if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;// }// 單聲道轉立體聲for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i]; // 左聲道stereo_buffer[2 * i + 1] = buffer[i]; // 右聲道}// 播放音頻數據i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);
}
功能擴展
添加兩個按鈕,用于控制實時播放的音量。主要實現思路是通過按鈕去控制增益因子的增加或減少。
首先創建一個數組用來存儲增益因子,這里我們選擇放大的倍數為20, 30, 40, 50, 60, 70, 80可選,然后numGains用于計算元素個數,方便我們后續的判斷。并設置默認增益為20。
// 增益因子數組
const int gainFactors[] = {20, 30, 40, 50, 60, 70, 80};
const int numGains = sizeof(gainFactors) / sizeof(gainFactors[0]);
int currentGainIndex = 0; // 默認增益為 20
整體思路是:如果按鍵1按下,currentGainIndex++, 如果按鍵2按下,currentGainIndex- -
考慮到按鍵抖動的情況,代碼如下:
// 按鍵引腳
#define BUTTON_UP_PIN 33 // 增益增加按鍵(GPIO 33)
#define BUTTON_DOWN_PIN 32 // 增益減少按鍵(GPIO 32)// 按鍵去抖變量
unsigned long lastUpDebounceTime = 0;
unsigned long lastDownDebounceTime = 0;
const unsigned long debounceDelay = 50;void KeyUp() {// 檢查增益增加按鍵bool upReading= digitalRead(BUTTON_UP_PIN);if (upReading!= lastUpButtonState) {lastUpDebounceTime = millis();}if ((millis() - lastUpDebounceTime) > debounceDelay) {if (upReading== LOW && currentGainIndex < numGains - 1) { // 按下且未達最大增益currentGainIndex++;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));delay(200); // 防止快速切換}}lastUpButtonState = upReading;
}void KeyDn() {// 檢查增益減少按鍵bool downReading = digitalRead(BUTTON_DOWN_PIN);if (downReading != lastDownButtonState) {lastDownDebounceTime = millis();}if ((millis() - lastDownDebounceTime) > debounceDelay) {if (downReading == LOW && currentGainIndex > 0) { // 按下且未達最小增益currentGainIndex--;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));delay(200); // 防止快速切換}}lastDownButtonState = downReading;}
增加增益因子和減少增益因子的去抖機制相同,以增加為例,機械按鍵在按下或釋放的時候會有產生短暫的電平抖動,通過去抖機制,等待一段時間(這里的debounceDelay)可以確保狀態穩定,避免誤判為多次按鍵事件。millis() 這個函數會返回程序啟動后經過的毫秒數。lastUpDebounceTime = millis();
標記按鍵狀態變化的時間點,millis() - lastUpDebounceTime
是當前時間與按鍵狀態變化時間的差值,表示從上次狀態變化以來經過的毫秒數, 用于判斷按鍵狀態是否穩定足夠長時間,如果時間差值大于 debounceDelay,說明按鍵狀態已經穩定,其效果類似于delay(50) (但是一個是阻塞等待,一個是非阻塞等待)。
主要代碼是這兩個, 其中numGains
是增益因子數組元素個數,因為數組是從零開始的,所以numGains個數減1就是表示數組最大索引數, 如果沒達到最大索引,即還未達最大增益,currentGainIndex++。
if (upReading== LOW && currentGainIndex < numGains - 1) { // 按下且未達最大增益currentGainIndex++;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));delay(200); // 防止快速切換}
currentGainIndex > 0表示還未到最小索引,所以currentGainIndex- -
if (downReading == LOW && currentGainIndex > 0) { // 按下且未達最小增益currentGainIndex--;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));delay(200); // 防止快速切換}
按鍵是否按下是通過判斷upReading和downReading是否變為LOW
實現。
為了方便控制音量,這里我還嘗試了使用旋轉編碼器來控制音量,主要代碼如下:
#include <ESP32Encoder.h>
// 編碼器引腳
#define MODE_DT_PIN 32 // A 相
#define MODE_CLK_PIN 35 // B 相#define MODE_STEP 2 // 每 2 個計數觸發ESP32Encoder modeEncoder;void KeyUp() {if (currentGainIndex < numGains - 1) {currentGainIndex++;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));}
}void KeyDn() {if (currentGainIndex > 0) {currentGainIndex--;Serial.println("增益切換至: " + String(gainFactors[currentGainIndex]));}
}/*------EC11 控制函數------*/
void EC11_Control() {static int lastModeCount = 0;static unsigned long lastRotateTime = 0;int currentModeCount = modeEncoder.getCount();if (abs(currentModeCount - lastModeCount) >= MODE_STEP) {lastRotateTime = millis();if (currentModeCount > lastModeCount) {KeyUp(); // 順時針增加增益Serial.println("向下");} else {KeyDn(); // 逆時針減少增益Serial.println("向上");}modeEncoder.setCount(0); // 重置計數lastModeCount = 0;}// 長時間無操作時輸出停止if (millis() - lastRotateTime > 1000) {//Serial.println("停止旋轉");lastRotateTime = millis(); }
}
理解了按鍵控制增益因子的原理,這里也是一樣的。我們在EC11_Control()調用 KeyUp()和KeyDn()這兩個函數。采用編碼器的時候還需要注意在setup()函數里面添加初始化編碼器的相關代碼。
void setup() {
// 初始化編碼器pinMode(MODE_CLK_PIN, INPUT_PULLUP);pinMode(MODE_DT_PIN, INPUT_PULLUP);modeEncoder.attachHalfQuad(MODE_CLK_PIN, MODE_DT_PIN);//配置編碼器為半四分之一模式,只計數部分狀態變化(每步約 1-2 個計數)modeEncoder.setFilter(50); // 濾波值modeEncoder.setCount(0);}
現象
ESP32驅動inmp441采集音頻并實時播放
注意事項
- 音頻輸出可以用耳機去接收聽到聲音,也可以使用AUX線外接功放板。PCM5102A板子上也有L/R左右聲道接口,也可以杜邦線接到其他功放上(比如上一期的TDA2030A功放模塊)。
- INM441是全向麥克風模塊,把揚聲器和麥克風放在一起使用很容易引起嘯叫,有耳機的話基本不會有這個問題。
- 根據前面嘯叫問題,增益不宜調太大,一是容易引起嘯叫,二是靠近說話容易引起破音,增益大了對遠距離采集聲音較好。