:::spoiler 麥克風測試 ```--C/C++ #include <driver/i2s.h> // 引入 I2S 數位音訊介面函式庫 // INMP441 I2S 腳位定義 #define I2S_WS 25 // I2S Word Select (LRCLK) 腳位,用於標示左右聲道或字元邊界 #define I2S_BCLK 26 // I2S Bit Clock 腳位,用於同步每個資料位元 #define I2S_SD 32 // I2S Serial Data (資料輸入) 腳位,實際音訊資料線 #define I2S_PORT I2S_NUM_0 // 使用 ESP32 的 I2S 埠 0 #define SAMPLE_RATE 16000 // 音訊取樣率:每秒取樣 16000 次,這是語音常用的頻率 #define BUFFER_LEN 64 // 每次讀取/處理的音訊樣本數量 (緩衝區大小) void setup() { Serial.begin(115200); // 初始化序列埠,用於顯示音量數值 // I2S 初始化:設定 ESP32 如何接收音訊 const i2s_config_t i2s_config = { // 設定為 I2S 主模式 (ESP32 提供時脈) 和 接收模式 (從麥克風接收) .mode = (i2s_mode_t) //填入(I2S_MODE_MASTER | I2S_MODE_RX)或(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_MSB,//I2S通訊格式,通常是MSB最高有效位元優先 .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中斷分配旗標,設定中斷優先級 .dma_buf_count = 8, // DMA 緩衝區的數量,DMA 讓音訊資料高效傳輸,不佔用 CPU .dma_buf_len = 64, // 每個 DMA 緩衝區的長度 (樣本數) .use_apll = false, // 不使用 APLL (音訊專用 PLL),節省資源 .tx_desc_auto_clear = false, // 接收模式下,傳輸描述符不自動清除 (不相關) .fixed_mclk = 0 // 不固定 MCLK 頻率,由 I2S 驅動自動計算 }; // 不輸出的腳位填I2S_PIN_NO_CHANGE const i2s_pin_config_t pin_config = { .bck_io_num = , // BCLK 腳位 .ws_io_num = , // WS/LRCLK 腳位 .data_out_num = , // 接收模式下,沒有資料輸出,設為不變 .data_in_num = // SDATA 腳位,用於接收麥克風的資料 }; i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); // 安裝 I2S 驅動程式 i2s_set_pin(I2S_PORT, &pin_config); // 設定 I2S 腳位 i2s_zero_dma_buffer(I2S_PORT); // 清空 DMA 緩衝區,準備接收新資料\ Serial.println("🎙 麥克風收音測試啟動..."); } void loop() { int16_t buffer[BUFFER_LEN]; // 建立一個緩衝區來存放讀取到的音訊樣本 size_t bytesRead; // 記錄實際讀取到的位元組數 // 從 I2S 介面讀取音訊資料 (麥克風收音) // portMAX_DELAY 表示程式會一直等待,直到讀取到足夠的資料 i2s_read(I2S_PORT, &buffer, sizeof(buffer), &bytesRead, portMAX_DELAY); // 計算音量 (簡單的平均絕對值,用於顯示) uint32_t sum = 0; for (int i = 0; i < BUFFER_LEN; i++) { sum += abs(buffer[i]); // 累加每個樣本的絕對值 (絕對值代表聲音的強度) } int avgVolume = sum / BUFFER_LEN; // 計算平均音量 Serial.print("🎚 音量:"); // 序列埠輸出音量 Serial.println(avgVolume); delay(50); // 短暫延遲 } ``` ::: :::spoiler 喇叭測試 ```--C/C++ #include <driver/i2s.h> // 引入 I2S 數位音訊介面函式庫 // I2S 設定 (喇叭輸出腳位) #define I2S_DOUT 27 // I2S Data Out (資料輸出) 腳位,實際音訊資料線 #define I2S_BCLK 26 // I2S Bit Clock 腳位 #define I2S_LRC 25 // I2S Left/Right Clock (LRCLK) 腳位 #define SAMPLE_RATE 16000 // 音訊取樣率,需與傳送端一致 #define BUFFER_LEN 64 // 每次寫入的音訊樣本數量 (緩衝區大小) // 簡單的正弦波數據,用於測試喇叭播放 // 實際對講機中,這裡會是接收到的音訊數據 int16_t test_tone[BUFFER_LEN]; void setupI2S() { // 將 I2S 初始化獨立成一個函式,方便呼叫 const i2s_config_t i2s_config = { //設定為 I2S 主模式 (ESP32 提供時脈) 和 傳輸模式 (輸出到喇叭) .mode = (i2s_mode_t) //填入(I2S_MODE_MASTER | I2S_MODE_RX)或(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_MSB, // I2S 通訊格式 .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中斷分配旗標 .dma_buf_count = 8, // DMA 緩衝區數量 .dma_buf_len = 64, // 每個 DMA 緩衝區的長度 .use_apll = false, // 不使用 APLL .tx_desc_auto_clear = true, // 傳輸模式下,傳輸描述符自動清除,以便持續傳輸數據 .fixed_mclk = 0 // 不固定 MCLK 頻率 }; // 不輸出的腳位填I2S_PIN_NO_CHANGE const i2s_pin_config_t pin_config = { .bck_io_num = , // BCLK 腳位 .ws_io_num = , // WS/LRCLK 腳位 .data_out_num = , // SDATA 腳位,用於輸出資料到喇叭 .data_in_num = // 傳輸模式下,沒有資料輸入,設為不變 }; i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); // 安裝 I2S 驅動程式 i2s_set_pin(I2S_NUM_0, &pin_config); // 設定 I2S 腳位 i2s_zero_dma_buffer(I2S_NUM_0); // 清空 DMA 緩衝區,準備寫入新資料 } void setup() { Serial.begin(115200); setupI2S(); // 呼叫 I2S 設定函式 // 生成一個簡單的正弦波測試音 for (int i = 0; i < BUFFER_LEN; i++) { test_tone[i] = (int16_t)(sin(i * 2 * PI / BUFFER_LEN * 5) * 10000); // 5個週期,振幅10000 } Serial.println("🔊 喇叭播放測試啟動..."); } void loop() { size_t bytesWritten; // 將測試音數據寫入 I2S 輸出到喇叭 i2s_write(I2S_NUM_0, test_tone, sizeof(test_tone), &bytesWritten, portMAX_DELAY); delay(10); // 稍微延遲,避免播放過快 } ``` ::: :::spoiler ESP-NOW發送 ```--C/C++ #include <WiFi.h> #include <esp_now.h> // 按鈕腳位 #define BUTTON_PIN 33 // 模擬音訊資料長度(int16_t 單位) #define BUFFER_LEN 64 // 接收端 MAC 地址(請依實際修改) uint8_t peerMac[] = {0xA4, 0xE5, 0x7C, 0xD3, 0xB4, 0x5C}; void setup() { Serial.begin(115200); delay(1000); // 設定按鈕腳位 pinMode(BUTTON_PIN, INPUT_PULLUP); // 初始化 WiFi 為 STA 模式(ESP-NOW 需要) WiFi.mode(WIFI_STA); delay(100); // 等待 WiFi 模式穩定 // 初始化 ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗"); while (true); } // 設定對等端(接收端) esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, peerMac, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("❌ 加入 ESP-NOW 對等端失敗"); while (true); } Serial.print("📡 傳送端 MAC 地址:"); Serial.println(WiFi.macAddress()); Serial.println("🚀 傳送端啟動完成,等待按鈕觸發..."); } void loop() { int16_t buffer[BUFFER_LEN]; // 模擬音訊資料(這裡用簡單的正弦波或隨機值) for (int i = 0; i < BUFFER_LEN; i++) { buffer[i] = random(-1000, 1000); // 模擬音訊樣本 } // 音量計算(平均絕對值) uint32_t sum = 0; for (int i = 0; i < BUFFER_LEN; i++) { sum += abs(buffer[i]); } int avgVolume = sum / BUFFER_LEN; Serial.print("🎚 模擬音量:"); Serial.print(avgVolume); // 按鈕觸發傳送 if (digitalRead(BUTTON_PIN) == LOW) { esp_err_t result = esp_now_send(peerMac, (uint8_t*)buffer, sizeof(buffer)); if (result == ESP_OK) { Serial.println(" → 傳送成功"); } else { Serial.println(" → 傳送失敗"); } } else { Serial.println("(未傳送)"); } delay(100); // 控制傳送頻率 } ``` ::: :::spoiler 接收 ```--C/C++ #include <WiFi.h> #include <esp_now.h> // ==== 音量處理參數 ==== #define NOISE_THRESHOLD 30 // 音量低於此值視為靜音 #define FILTER_TAP_COUNT 2 // 移動平均濾波器抽頭數 // 接收資料的 callback void onDataReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) { int16_t *samples = (int16_t *)data; int sampleCount = len / sizeof(int16_t); // 計算平均音量 uint32_t sum = 0; for (int i = 0; i < sampleCount; i++) { sum += abs(samples[i]); } int avgVolume = sum / sampleCount; // 閾值靜音處理 if (avgVolume < NOISE_THRESHOLD) { Serial.println("🔇 音量過低,視為靜音"); return; } // 移動平均濾波器(簡單實作) for (int i = 0; i < sampleCount; i++) { long sum_filter = 0; int count_filter = 0; for (int j = 0; j < FILTER_TAP_COUNT; j++) { if (i - j >= 0) { sum_filter += samples[i - j]; count_filter++; } } samples[i] = (int16_t)(sum_filter / count_filter); } // 印出處理後的音量資訊 Serial.print("📥 接收到 "); Serial.print(len); Serial.print(" bytes,平均音量:"); Serial.println(avgVolume); } void setup() { Serial.begin(115200); delay(100); // 設定為 STA 模式 WiFi.mode(WIFI_STA); WiFi.disconnect(); // 確保不連接 AP delay(100); Serial.print("📡 接收端 MAC 地址:"); Serial.println(WiFi.macAddress()); // 初始化 ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗"); return; } // 註冊接收 callback esp_now_register_recv_cb(onDataReceive); Serial.println("🎧 接收端啟動,等待資料傳入..."); } void loop() { } ``` ::: 音量增益 ```c // ===== 新增:音量增益處理 ===== float gain = 2.0; // 設定增益係數,例如 2.0 代表音量加倍 for (int i = 0; i < sampleCount; i++) { // 確保不會超過 16 位元整數的最大值 long amplified_sample = (long)samples[i] * gain; if (amplified_sample > 32767) { samples[i] = 32767; } else if (amplified_sample < -32768) { samples[i] = -32768; } else { samples[i] = (int16_t)amplified_sample; } } ``` ```cpp #include <WiFi.h> #include <esp_now.h> #include <driver/i2s.h> // 引入 I2S 數位音訊介面函式庫 // ==== 音量處理參數 ==== #define NOISE_THRESHOLD 20 // 音量低於此值視為靜音 #define FILTER_TAP_COUNT 2 // 移動平均濾波器抽頭數 #define I2S_DOUT 27 // I2S Data Out (資料輸出) 腳位,實際音訊資料線 #define I2S_BCLK 26 // I2S Bit Clock 腳位 #define I2S_LRC 25 // I2S Left/Right Clock (LRCLK) 腳位 #define SAMPLE_RATE 16000 // 音訊取樣率,需與傳送端一致 #define BUFFER_LEN 64 // 每次寫入的音訊樣本數量 (緩衝區大小) void setupI2S() { // 將 I2S 初始化獨立成一個函式,方便呼叫 const i2s_config_t i2s_config = { //設定為 I2S 主模式 (ESP32 提供時脈) 和 傳輸模式 (輸出到喇叭) .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), //填入(I2S_MODE_MASTER | I2S_MODE_RX)或(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_MSB, // I2S 通訊格式 .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中斷分配旗標 .dma_buf_count = 8, // DMA 緩衝區數量 .dma_buf_len = 64, // 每個 DMA 緩衝區的長度 .use_apll = false, // 不使用 APLL .tx_desc_auto_clear = true, // 傳輸模式下,傳輸描述符自動清除,以便持續傳輸數據 .fixed_mclk = 0 // 不固定 MCLK 頻率 }; // 不輸出的腳位填I2S_PIN_NO_CHANGE const i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCLK, // BCLK 腳位 .ws_io_num = I2S_LRC, // WS/LRCLK 腳位 .data_out_num = I2S_DOUT, // SDATA 腳位,用於輸出資料到喇叭 .data_in_num = I2S_PIN_NO_CHANGE // 傳輸模式下,沒有資料輸入,設為不變 }; i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); // 安裝 I2S 驅動程式 i2s_set_pin(I2S_NUM_0, &pin_config); // 設定 I2S 腳位 i2s_zero_dma_buffer(I2S_NUM_0); // 清空 DMA 緩衝區,準備寫入新資料 } // 接收資料的 callback void onDataReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) { int16_t *samples = (int16_t *)data; int sampleCount = len / sizeof(int16_t); // 計算平均音量 uint32_t sum = 0; for (int i = 0; i < sampleCount; i++) { sum += abs(samples[i]); } int avgVolume = sum / sampleCount; // 閾值靜音處理 if (avgVolume < NOISE_THRESHOLD) { Serial.println("🔇 音量過低,視為靜音"); return; } // 移動平均濾波器(簡單實作) for (int i = 0; i < sampleCount; i++) { long sum_filter = 0; int count_filter = 0; for (int j = 0; j < FILTER_TAP_COUNT; j++) { if (i - j >= 0) { sum_filter += samples[i - j]; count_filter++; } } samples[i] = (int16_t)(sum_filter / count_filter); } // ===== 新增:音量增益處理 ===== float gain = 4.0; // 設定增益係數,例如 2.0 代表音量加倍 for (int i = 0; i < sampleCount; i++) { // 確保不會超過 16 位元整數的最大值 long amplified_sample = (long)samples[i] * gain; if (amplified_sample > 32767) { samples[i] = 32767; } else if (amplified_sample < -32768) { samples[i] = -32768; } else { samples[i] = (int16_t)amplified_sample; } } // 印出處理後的音量資訊 Serial.print("📥 接收到 "); Serial.print(len); Serial.print(" bytes,平均音量:"); Serial.println(avgVolume); size_t bytesWritten; // 將測試音數據寫入 I2S 輸出到喇叭 i2s_write(I2S_NUM_0, samples, len, &bytesWritten, portMAX_DELAY); // delayMicroseconds(1000); // 稍微延遲,避免播放過快 } void setup() { Serial.begin(115200); delay(100); // 設定為 STA 模式 WiFi.mode(WIFI_STA); WiFi.disconnect(); // 確保不連接 AP delay(100); Serial.print("📡 接收端 MAC 地址:"); Serial.println(WiFi.macAddress()); // 初始化 ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗"); return; } // 註冊接收 callback esp_now_register_recv_cb(onDataReceive); setupI2S(); Serial.println("🎧 接收端啟動,等待資料傳入..."); } void loop() { } ```