ESP32 + INMP441 麥克風:WiFi 即時音訊串流
用 ESP32 NodeMCU-32S 搭配 INMP441 數位麥克風,透過 I2S 擷取音訊並以 WebSocket 即時串流到瀏覽器播放。教學涵蓋 I2S 協定、位元轉換完整指南與完整程式碼。
想讓 ESP32 變成一個 WiFi 麥克風,在瀏覽器上即時聽到聲音嗎?這篇教學帶你用 NodeMCU-32S 搭配 INMP441 數位麥克風模組,透過 WebSocket 將音訊即時串流到網頁瀏覽器播放。不管是要做嬰兒監聽器、遠端環境監控,還是 WiFi 對講機的前置作業,這都是一個很好的起點。
認識 INMP441 數位麥克風模組
為什麼選 INMP441?
之前的 聲音感測模組教學 中,我們用的是類比麥克風,它只能偵測「有沒有聲音」和「聲音大不大」,沒辦法真正錄下聲音內容。
INMP441 就不一樣了,它是一顆數位 MEMS 麥克風,內建 24-bit ADC(類比數位轉換器),直接輸出數位音訊資料,不需要額外的音效卡或編解碼器。透過 I2S(Inter-IC Sound)數位音訊介面跟 ESP32 溝通,音質比類比麥克風好很多。
INMP441 模組規格
| 項目 | 規格 |
|---|---|
| 工作電壓 | 1.8V ~ 3.3V |
| 訊噪比(SNR) | 61 dBA |
| 靈敏度 | -26 dBFS |
| 頻率響應 | 60 Hz ~ 15 kHz |
| 輸出介面 | I2S(24-bit) |
| 收音方向 | 全指向(Omnidirectional) |
| 工作電流 | 1.4 mA |
模組接腳說明
INMP441 模組通常有 6 個接腳:
| 接腳 | 說明 |
|---|---|
| VDD | 電源輸入,接 3.3V(不要接 5V!) |
| GND | 接地 |
| SCK | Serial Clock,I2S 時脈訊號 |
| WS | Word Select,也叫 LRCLK,用來區分左右聲道 |
| SD | Serial Data,I2S 音訊資料輸出 |
| L/R | 聲道選擇:接 GND 為左聲道,接 VDD 為右聲道 |
⚠️ 注意: INMP441 的收音孔在電路板底部(背面那個小圓孔),不是正面。安裝時要確保底部朝向聲源,如果焊排針的話,通常要把排針焊在「反面」,讓收音孔朝上。

什麼是 I2S?
I2S(Inter-IC Sound)是一種專門用來傳輸數位音訊的通訊協定,由 Philips(飛利浦)制定。跟 SPI、I2C 一樣是同步序列通訊,但它是專門為音訊設計的。
I2S 使用三條訊號線:
- SCK(Serial Clock / BCLK):位元時脈,每個時脈週期傳送一個位元的資料
- WS(Word Select / LRCLK):字選擇,用來切換左聲道和右聲道。WS = 0 時傳左聲道、WS = 1 時傳右聲道
- SD(Serial Data):實際的音訊資料線
ESP32 內建兩組 I2S 控制器(I2S_NUM_0 和 I2S_NUM_1),可以同時處理音訊輸入和輸出。本篇我們只用到輸入(RX),從 INMP441 讀取聲音。
硬體準備
所需材料
| 材料 | 數量 | 說明 |
|---|---|---|
| ESP32 NodeMCU-32S 開發板 | 1 | 其他 ESP32 開發板也適用 |
| INMP441 麥克風模組 | 1 | I2S 數位麥克風 |
| 麵包板 | 1 | |
| 公對母杜邦線 | 5 條 | |
| Micro USB 傳輸線 | 1 | 供電與燒錄程式 |



接線
| INMP441 | NodeMCU-32S | 說明 |
|---|---|---|
| VDD | 3V3 | 電源(務必接 3.3V) |
| GND | GND | 接地 |
| SCK | GPIO 26 | I2S 時脈 |
| WS | GPIO 22 | I2S 字選擇 |
| SD | GPIO 21 | I2S 資料 |
| L/R | GND | 這次選擇左聲道 |
💡 腳位可以自由更換,只要在程式裡對應修改就好。不過建議避開 GPIO 6~11(連接內部 Flash)和 GPIO 34~39(只能輸入,不能輸出時脈)。
接線完成後,確認 INMP441 的收音孔(底部小圓孔)是朝上的,不要被麵包板擋住。

軟體架構說明
整個專案的運作流程如下:
INMP441 麥克風
│ (I2S 數位音訊)
▼
ESP32 NodeMCU-32S
│ 1. 透過 I2S 讀取音訊(32-bit → 16-bit 轉換)
│ 2. 架設 WiFi WebSocket 伺服器
│ 3. 將音訊資料以二進位封包送出
│ (WebSocket)
▼
瀏覽器(手機 / 電腦)
│ 1. 連線到 ESP32 的 WebSocket
│ 2. 用 Web Audio API 即時播放音訊
▼
🔊 你的喇叭
ESP32 同時扮演兩個角色:
- HTTP 伺服器:提供網頁介面(HTML + JavaScript)
- WebSocket 伺服器:即時傳送音訊二進位資料
需要安裝的函式庫
在 Arduino IDE 中,到「管理函式庫」搜尋並安裝以下函式庫:
| 函式庫名稱 | 作者 | 用途 |
|---|---|---|
| WebSocketsServer | Markus Sattler | WebSocket 伺服器 |
| ArduinoJson | Benoit Blanchon | (選用)JSON 格式處理 |
ESP32 的 I2S 驅動和 WiFi 函式庫是內建的,不需要額外安裝。
💡 搜尋「WebSockets」時,請認明作者是 Markus Sattler(函式庫名稱為 WebSockets),不要裝錯。

完整程式碼
ESP32 端程式(Arduino Sketch)
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <driver/i2s.h>
// ========== WiFi 設定 ==========
const char* ssid = "你的WiFi名稱";
const char* password = "你的WiFi密碼";
// ========== I2S 腳位設定 ==========
#define I2S_SCK 26 // Serial Clock (BCLK)
#define I2S_WS 22 // Word Select (LRCLK)
#define I2S_SD 21 // Serial Data (DOUT)
#define I2S_PORT I2S_NUM_0
// ========== 音訊參數 ==========
#define SAMPLE_RATE 16000 // 取樣率 16kHz(人聲足夠)
#define BUFFER_LEN 1024 // 每次讀取的樣本數
// 緩衝區
int32_t rawSamples[BUFFER_LEN]; // I2S 讀出的原始 32-bit 資料
int16_t samples16[BUFFER_LEN]; // 轉換後的 16-bit 資料
// 伺服器
WebServer server(80); // HTTP 伺服器(埠 80)
WebSocketsServer webSocket(81); // WebSocket 伺服器(埠 81)
bool clientConnected = false; // 是否有客戶端連線
// ========== 網頁 HTML ==========
const char indexHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 即時音訊串流</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #e94560;
}
.status {
font-size: 1rem;
margin-bottom: 1.5rem;
color: #aaa;
}
.status.connected { color: #4ecca3; }
.status.error { color: #e94560; }
button {
padding: 1rem 2.5rem;
font-size: 1.1rem;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s;
margin: 0.5rem;
}
#btnStart {
background: #4ecca3;
color: #1a1a2e;
}
#btnStart:hover { background: #3db890; }
#btnStop {
background: #e94560;
color: #fff;
display: none;
}
#btnStop:hover { background: #d63851; }
.volume-bar {
width: 300px;
height: 20px;
background: #333;
border-radius: 10px;
margin: 1.5rem auto;
overflow: hidden;
}
.volume-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #4ecca3, #e94560);
transition: width 0.1s;
border-radius: 10px;
}
.info {
font-size: 0.8rem;
color: #666;
margin-top: 2rem;
}
</style>
</head>
<body>
<div class="container">
<h1>🎙️ ESP32 即時音訊串流</h1>
<div id="status" class="status">點擊按鈕開始收聽</div>
<div class="volume-bar">
<div id="volumeFill" class="volume-fill"></div>
</div>
<button id="btnStart" onclick="startAudio()">▶ 開始收聽</button>
<button id="btnStop" onclick="stopAudio()">⏹ 停止</button>
<div class="info">
取樣率:16kHz | 位元深度:16-bit | 單聲道| 傑森創工
</div>
</div>
<script>
let ws = null;
let audioCtx = null;
let isPlaying = false;
const SAMPLE_RATE = 16000;
function startAudio() {
// 建立 AudioContext(瀏覽器要求必須在使用者操作後才能建立)
audioCtx = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: SAMPLE_RATE
});
// 建立 WebSocket 連線
// 自動使用目前頁面的 IP,不用手動填
const host = window.location.hostname;
ws = new WebSocket(`ws://${host}:81`);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
isPlaying = true;
document.getElementById('status').textContent = '🟢 已連線,收聽中...';
document.getElementById('status').className = 'status connected';
document.getElementById('btnStart').style.display = 'none';
document.getElementById('btnStop').style.display = 'inline-block';
};
ws.onmessage = (event) => {
if (!isPlaying || !audioCtx) return;
// 將收到的二進位資料轉成 16-bit 整數陣列
const int16Array = new Int16Array(event.data);
const numSamples = int16Array.length;
// 建立 AudioBuffer
const audioBuffer = audioCtx.createBuffer(1, numSamples, SAMPLE_RATE);
const channelData = audioBuffer.getChannelData(0);
// 將 int16 轉成 float(-1.0 ~ 1.0)
let sumSquares = 0;
for (let i = 0; i < numSamples; i++) {
channelData[i] = int16Array[i] / 32768.0;
sumSquares += channelData[i] * channelData[i];
}
// 計算音量(RMS)並更新音量條
const rms = Math.sqrt(sumSquares / numSamples);
const volumePercent = Math.min(rms * 500, 100); // 放大顯示
document.getElementById('volumeFill').style.width = volumePercent + '%';
// 播放音訊
const source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioCtx.destination);
source.start();
};
ws.onerror = (err) => {
document.getElementById('status').textContent = '❌ 連線錯誤';
document.getElementById('status').className = 'status error';
console.error('WebSocket error:', err);
};
ws.onclose = () => {
document.getElementById('status').textContent = '⚪ 連線已中斷';
document.getElementById('status').className = 'status';
document.getElementById('btnStart').style.display = 'inline-block';
document.getElementById('btnStop').style.display = 'none';
isPlaying = false;
};
}
function stopAudio() {
isPlaying = false;
if (ws) {
ws.close();
ws = null;
}
if (audioCtx) {
audioCtx.close();
audioCtx = null;
}
document.getElementById('status').textContent = '點擊按鈕開始收聽';
document.getElementById('status').className = 'status';
document.getElementById('btnStart').style.display = 'inline-block';
document.getElementById('btnStop').style.display = 'none';
document.getElementById('volumeFill').style.width = '0%';
}
</script>
</body>
</html>
)rawliteral";
// ========== I2S 初始化 ==========
void i2sInit() {
// I2S 設定
const i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), // 主模式 + 接收
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // INMP441 輸出 32-bit
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 只用左聲道(L/R 接 GND)
.communication_format = I2S_COMM_FORMAT_STAND_I2S, // 標準 I2S 格式
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中斷等級 1
.dma_buf_count = 8, // DMA 緩衝區數量
.dma_buf_len = BUFFER_LEN, // 每個緩衝區的樣本數
.use_apll = false, // 不使用 APLL
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
// I2S 腳位設定
const i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK, // BCLK
.ws_io_num = I2S_WS, // LRCLK
.data_out_num = I2S_PIN_NO_CHANGE, // 不用輸出(沒有喇叭)
.data_in_num = I2S_SD // 資料輸入
};
// 安裝 I2S 驅動
esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("I2S 驅動安裝失敗: %d\n", err);
return;
}
// 設定 I2S 腳位
err = i2s_set_pin(I2S_PORT, &pin_config);
if (err != ESP_OK) {
Serial.printf("I2S 腳位設定失敗: %d\n", err);
return;
}
Serial.println("I2S 初始化完成");
}
// ========== WebSocket 事件處理 ==========
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
switch (type) {
case WStype_DISCONNECTED:
Serial.printf("[%u] 已斷線\n", num);
clientConnected = false;
break;
case WStype_CONNECTED:
Serial.printf("[%u] 已連線\n", num);
clientConnected = true;
break;
default:
break;
}
}
// ========== 讀取 I2S 並轉換為 16-bit ==========
void readAndSendAudio() {
if (!clientConnected) return;
size_t bytesRead = 0;
// 從 I2S 讀取 32-bit 原始資料
esp_err_t err = i2s_read(I2S_PORT, rawSamples, sizeof(rawSamples), &bytesRead, portMAX_DELAY);
if (err != ESP_OK) return;
int samplesRead = bytesRead / sizeof(int32_t);
// 將 32-bit 轉換成 16-bit
// INMP441 的有效資料在高位元,右移 16 位取出
for (int i = 0; i < samplesRead; i++) {
samples16[i] = (int16_t)(rawSamples[i] >> 16);
}
// 透過 WebSocket 發送 16-bit 音訊資料
webSocket.broadcastBIN((uint8_t*)samples16, samplesRead * sizeof(int16_t));
}
// ========== setup() ==========
void setup() {
Serial.begin(115200);
Serial.println("\n=== ESP32 INMP441 音訊串流 ===");
// 連接 WiFi
WiFi.begin(ssid, password);
Serial.print("正在連接 WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi 已連線,IP 位址:");
Serial.println(WiFi.localIP());
// 初始化 I2S
i2sInit();
// 設定 HTTP 伺服器
server.on("/", []() {
server.send_P(200, "text/html", indexHtml);
});
server.begin();
Serial.println("HTTP 伺服器已啟動(埠 80)");
// 設定 WebSocket 伺服器
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("WebSocket 伺服器已啟動(埠 81)");
Serial.println("\n請用瀏覽器開啟以下網址:");
Serial.print("http://");
Serial.println(WiFi.localIP());
}
// ========== loop() ==========
void loop() {
server.handleClient();
webSocket.loop();
readAndSendAudio();
}


程式碼解析
I2S 設定重點
程式裡有幾個關鍵設定值得特別說明:
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT
雖然 INMP441 是 24-bit 的 ADC,但 ESP32 的 I2S 驅動只支援 16-bit 或 32-bit。我們必須設成 32-bit 來讀,再自己從 32-bit 資料中取出有效位元。
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT
因為我們只接了一顆 INMP441,而且 L/R 腳接 GND(左聲道),所以設成 ONLY_LEFT。如果你的 L/R 接 VDD(右聲道),這裡要改成 ONLY_RIGHT。
.communication_format = I2S_COMM_FORMAT_STAND_I2S
使用標準 I2S 格式。有些舊版的 Arduino ESP32 核心可能要用 I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,如果編譯時出現錯誤,換成舊寫法試試。
dma_buf_count 和 dma_buf_len
DMA(Direct Memory Access)是讓 I2S 硬體自動將資料搬到記憶體,不需要 CPU 介入。dma_buf_count = 8 表示有 8 個緩衝區輪流使用,dma_buf_len = 1024 是每個緩衝區存放的樣本數。這兩個值的設定會影響延遲和穩定度:
- 值設太小:資料來不及處理,容易掉資料(聽起來會斷斷續續)
- 值設太大:延遲增加(說話後要等比較久才聽到)
目前的設定在 16kHz 取樣率下,每個緩衝區約 64ms 的音訊,是一個蠻均衡的值。
32-bit 轉 16-bit 的關鍵
這是整個專案最容易出錯的地方:
samples16[i] = (int16_t)(rawSamples[i] >> 16);
INMP441 輸出的 32-bit 資料中,有效的 24-bit 音訊放在高位元(MSB),最低的 8 位元是補零的。我們右移 16 位,等於取最高的 16 位元作為 16-bit 音訊。
為什麼不右移 8 位取完整的 24-bit?因為 WebSocket 傳輸和瀏覽器端處理 16-bit PCM 比較方便,而且 16-bit 的音質對語音來說已經綽綽有餘了(CD 音質就是 16-bit)。
⚠️ 傑森經驗: 有些 INMP441 模組的有效位元可能稍有不同,如果你聽到的聲音很小聲或很大聲帶破音,可以試著調整右移的位數。把>> 16改成>> 14或>> 11試試看,找出最適合你模組的值。
WebSocket 二進位傳輸
webSocket.broadcastBIN((uint8_t*)samples16, samplesRead * sizeof(int16_t));
broadcastBIN() 會把音訊資料以二進位格式發送給所有已連線的客戶端。相比用文字格式(例如 Base64 編碼),二進位傳輸的效率更高、延遲更低。
瀏覽器端 Web Audio API
瀏覽器收到二進位資料後,透過 Web Audio API 播放:
- 將收到的
ArrayBuffer轉成Int16Array - 建立
AudioBuffer,把 int16 值轉成 float(-1.0 ~ 1.0 範圍) - 用
BufferSource排入播放佇列
這種做法的好處是不需要任何瀏覽器外掛,現代瀏覽器(Chrome、Firefox、Safari、Edge)都原生支援。
⚠️ 注意: 瀏覽器基於安全考量,必須在使用者點擊按鈕後才能建立 AudioContext。所以我們設計了「開始收聽」按鈕,不是一開頁面就自動播放。燒錄與測試
Arduino IDE 設定
如果你對ESP32還不熟,建議先看我們之前的入門教學哦!

測試
- 確認你的電腦或手機跟 ESP32 連到同一個 WiFi 網路
- 打開瀏覽器,輸入序列埠監控視窗顯示的 IP 位址(例如
http://192.168.1.100) - 點擊「▶ 開始收聽」按鈕
- 對著 INMP441 說話,應該可以從電腦喇叭聽到聲音
- 網頁上的音量條會隨著音量變化
常見問題排解
聽到的聲音全是雜訊
原因 1:L/R 腳位設定不一致 如果 INMP441 的 L/R 接了 GND(左聲道),程式裡的 channel_format 一定要設成 I2S_CHANNEL_FMT_ONLY_LEFT。反過來也一樣,L/R 接 VDD 就要用 ONLY_RIGHT。設錯的話會讀到空資料,聽起來就是雜訊。
原因 2:位元轉換的偏移量不對 試著把 >> 16 改成其他值(>> 14、>> 11),找出適合你模組的偏移量。
原因 3:接線有問題 確認 SCK、WS、SD 三條線沒有接錯或接觸不良。I2S 對時序很敏感,鬆動的杜邦線可能導致資料錯誤。
聲音很小聲
可以在轉換時加上增益(放大):
// 加上 2 倍增益
int32_t amplified = (rawSamples[i] >> 16) * 2;
// 防止溢位
if (amplified > 32767) amplified = 32767;
if (amplified < -32768) amplified = -32768;
samples16[i] = (int16_t)amplified;
聲音斷斷續續
可能是 WiFi 頻寬不夠或有干擾。嘗試以下方式:
- 把取樣率從 16000 降到 8000
- 減小
BUFFER_LEN(例如改成 512) - 確認 ESP32 跟路由器的距離不要太遠
瀏覽器無法播放
- 確認是用
http://而不是https://(WebSocket 在非加密連線下用ws://) - 試試換用 Chrome 瀏覽器
- 確認有先點擊「開始收聽」按鈕
延伸應用
這個基礎的音訊串流專案可以再往很多方向發展:
- 嬰兒監聽器:加上音量閾值偵測,當音量超過設定值時透過 LINE Notify 發通知
- WiFi 對講機:加上 MAX98357A 擴大器模組和喇叭,搭配兩塊 ESP32 做雙向通訊
- 影音串流:搭配 ESP32-CAM,同時串流影像和聲音(這會在下一篇教學中介紹)
- 語音辨識:錄製音訊後傳送到雲端 API(Google Speech-to-Text)做語音轉文字
- 環境音量監控:搭配 OLED 顯示即時分貝值,並透過 MQTT 回傳到 Home Assistant
結語
INMP441 是一個 CP 值很高的數位麥克風模組,搭配 ESP32 的 I2S 介面和 WiFi 能力,就能做出即時音訊串流的應用。這篇教學涵蓋了 I2S 的基礎概念、32-bit 轉 16-bit 的位元處理、WebSocket 伺服器架設,以及瀏覽器端的 Web Audio API 播放,希望能幫你打好音訊處理的基礎。
下一篇我們會把這個麥克風串流跟 ESP32-CAM 結合,做出影音同步串流的效果,敬請期待!



