📋 專案概述
上週三 21:47,快遞在玄關喊了一聲「宅急便!」。你在客廳戴著耳機開電話會議,太太在書房戴著耳機趕 report。第二天你在櫃子深處找到一張「未本人收件」的黃單——裡面是你等了四天的相機底片。你家不是沒有智慧鏡頭:玄關那顆 360 塊的鏡頭看得見一切,但它沒有耳朵,聲音從哪個方向來它一概不知。而嬰兒房那款「智能守護機」會哭鬧偵測,卻只會朝你的手機亂推——它連哭聲從嬰兒床左邊還是右邊都說不出,你怎麼判斷是人貓還是貓?
市售商品:
- 智慧鏡頭系:有眼無耳;加上指向性麥克風的型號,其「聲源定位」是單麥波束的行銷名稱,轉頭就失準。
- 會議室麥克風陣列(Jabra Speak 550、Poly 之類):NT$15,000 起,定位精度只夠「指個大概」,還得綁他的會議軟體。
- 研究用麥克風陣列(ReSpeaker 4-mic):NT$2,500,要自己寫 GCC——那你已經會了,買它幹嘛?
本專案是一個掛牆上的聲場羅盤:4 支 I2S 麥克風方陣+GCC-PHAT 互相關演算法,算出聲音來向,WS2812 燈環指針直接指向聲源,事件連時間戳推 NAS:
- 4×INMP441 方陣(25cm 邊長):前後兩對、每對掛在同一條 I2S 總線的 L/R——同一訊框硬體級同步;兩條總線之間靠快拍校準吸收相位差
- GCC-PHAT 估時差 τ → 半圓解 ±arcsin → 前後對能量比(ILD+列印聲障隔板)拆全圓 360°:物理與數學各出一半功夫
- VAD 音頻門:300–8000Hz 頻帶能量+300ms 掛起——冷氣低頻、機械鍵盤、冰箱壓縮機不誤觸
- 方向中值過濾+信心度評分:3 次取中位;低信心(<40%)事件只記不上報——電視的聲音想亂舉手,先過信心度這一關
- 快拍校準模式:朝 8 個方向各拍 3 下,自動存每對 τ 偏移與 ILD 偏差進 Flash——把「裝配误差」這種看不見的東西變成一組數字
- NAS 事件規則:「22:00–07:00 且方位在玄關扇區且連續 3 事件 → Telegram:快遞在玄關(北)」——羅盤只是眼睛,規則引擎才是管家
💰 成本效益:全機 ~NT$580。被它接住的兩單「當面收件」值回票價;嬰兒房版本讓你從「哭了」(推播)升級到「哭在嬰兒床左側、3 分鐘了、中途停了兩次」(情報)。
為什麼選 ESP32-S3?
| 需求 | S3 的優勢 |
|---|---|
| 2 條 I2S 總線 | S3 有 2 個 I2S 外設 ×立體聲 = 4 通道;每總線 L/R 同訊框同步是 GCC 的命脈——S2 也有 2 個 I2S 但無 PSRAM |
| FFT 工作區 | PSRAM(N16R8):FFT 緩衝+4 通道環形緩衝住得舒服,8kHz×512 點運算不搶 SRAM |
| 採算不抖 | 雙核分工:核 0 只餵 I2S DMA、核 1 跑 FFT+Wi-Fi,中斷互不干擾——GCC 的 τ 估計經不起採樣時基抖動 |
| SIMD 加速 | S3 的 SIMD 對窗函數與複數乘法實測快約 20%:512 點互相關 6ms,每 32ms 一幀綽綽有餘 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-S3 DevKitC-1 N16R8 | 1 | ~230 元 | 2×I2S+PSRAM |
| I2S 麥克風 | INMP441 模組 | 4 | ~160 元 | L/R 選腳分前後對 |
| 燈環 | WS2812B 16 燈環 | 1 | ~45 元 | 羅盤指針+事件閃示 |
| 螢幕 | SSD1306 0.96" I2C | 1 | ~90 元 | 8 方位+信心% |
| 電源 | 5V 1.5A 線性 adaptor | 1 | ~80 元 | 開關電源哼聲教做人 |
| 降壓 | AMS1117-3.3 ×2 | 2 | ~8 元 | MIC 供電與邏輯分開 |
| 列印材料 | PLA ~70g+PETG 隔板片 | — | ~20 元 | 聲障隔板 PETG 3mm |
| 小件 | 100nF×4、330Ω、吸音棉圈 | — | ~25 元 | MIC 去耦+燈環護甲 |
| 合計 | ~580 元 | 會議麥克風陣列的 4% |
🔌 接線圖
詳細接線
| ESP32-S3 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO12 / 13 / 14 | I2S-A SCK / WS / SD | 前對:FL=L、FR=R(同訊框) |
| GPIO16 / 17 / 15 | I2S-B SCK / WS / SD | 後對:RL=L、RR=R |
| 各 MIC 的 L/R 腳 | 左對接 GND、右對接 3V3 | INMP441 以這隻腳選聲道 |
| GPIO4(→330Ω) | WS2812 燈環 DIN | 5V 獨立線回電源 |
| GPIO8 / GPIO9 | OLED SDA / SCL | 中斷閃示(非主介面) |
| 5V→AMS1117 ×2 | MIC 3V3(各加 100nF) | MIC 供電乾淨是勝負手 |
麥克風方陣(25cm 邊長,中央 T 形聲障隔板)
北=預設偵測主向(玄關)
[FL] ── 25cm ── [FR] ← 前對(I2S-A 立體聲)
│ T形隔板 │
25cm (PETG) 25cm
│ │
[RL] ── 25cm ── [RR] ← 後對(I2S-B 立體聲)
ESP32-S3 DevKitC INMP441 ×4
GPIO12(BCLK)──┬────────── FL / FR SCK
GPIO13(WS) ──┼────────── FL / FR WS
GPIO14(DIN) ┴── FL(L腳=GND) + FR(L腳=3V3) 同一訊框
GPIO16(BCLK)──┬────────── RL / RR SCK
GPIO17(WS) ──┼────────── RL / RR WS
GPIO15(DIN) ┴── RL(L腳=GND) + RR(L腳=3V3)
GPIO4 →330Ω ────── WS2812 DIN(5V 獨立線回電源端)
GPIO8/9 ────────── OLED SDA/SCL
電源星形點:
5V 主地 ─┬─ 燈環 GND(粗線獨立回)
├─ 邏輯 3V3(板載)
└─ AMS1117#1→MIC-A 3.3V(+100nF)
AMS1117#2→MIC-B 3.3V(+100nF)
※ BCLK ≈ 16kHz × 32bit × 2ch = 1.024MHz:杜邦線限長 15cm,超長就上 74AHCT125
⚠️ 兩條鐵律:① 前後兩對各自共用一條 I2S——同訊框是 GCC 的生命線,四支麥掛兩條總線已是本機極限,不要再「擴充到 8 支」;② MIC 的 3.3V 與 WS2812 的地必須星形分開——燈環的電流脈衝混進麥的地,VAD 會被自己的燈火誤觸(問題排除第一行的元兇)。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 羅盤外殼(壁掛、麥孔×4) | 1 | PLA | 白 | ~90 min |
| 中央 T 形聲障隔板 | 1 | PETG(3mm 實心) | 黑 | ~25 min |
| 麥克風內嵌麥架 | 4 | PLA | 白 | ~40 min |
| 麥頭防塵罩(吸音棉圈座) | 4 | PLA | 白 | ~20 min |
| 後蓋(走線槽+壁掛孔) | 1 | PLA | 白 | ~35 min |
| 傾角壁掛背板(15°) | 1 | PLA | 白 | ~30 min |
列印設定
- 外殼 PLA 0.2mm、壁 3 週邊、填充 15%:聲學零件輕而穩就夠
- 麥孔直徑=MIC 模組合間隙 0.3mm:過緊撐裂殼、過鬆漏聲(孔邊留 1mm 止裂倒角)
- 隔板 PETG 3mm 實心 100% 填充:聲障不實在=ILD 效應打折——PLA 吸濕後剛性下降,隔板上 PETG
- 隔板絕對不能與殼一列印成:要能抽換不同高度/角度的隔板試效果(120mm、150mm 兩個版本附 STL)
設計要點
- T 形聲障隔板是「頭影效應」的物理實現:人類的耳朵靠頭骨遮蔭分辨前後,本機靠這片 3mm PETG+貼附的吸音棉——聲波繞射要繞過它,前後能量差(ILD)被放大 4–6dB,全圓解算從「猜」變成「量」
- 麥架內嵌+防塵罩:INMP441 的聲孔朝外 5mm、四周泡棉圈密封——殼內氣室會讓高頻衰減 8dB,密封圈比 anything 都重要
- 後蓋走線槽深度 6mm:5V 動力線與 I2S 訊號線分槽,槽脊高於線束——省錢版屏蔽
- 15° 傾角背板:掛 1.7m 牆面、聲場中心約 1.5m 高——羅盤要對準耳朵高度不是插座高度
💻 程式碼(韌體)
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
/* ============================================================
* ESP32-S3 聲源定位聲場羅盤 | Sound Field Compass
* ------------------------------------------------------------
* 功能:4 通道 @16kHz(2×I2S 立體聲、總線內同訊框)、GCC-PHAT
* 互相關估 τ、半圓→全圓(前後 ILD+T 形聲障)、VAD 頻帶門、
* 快拍校準(8 方向)、WS2812 指針、NAS 事件上報
* 硬體:ESP32-S3 DevKitC-1 N16R8 + 4×INMP441 + WS2812B×16
* + SSD1306(I2C 中斷閃示)
* 供電:5V 線性;MIC 經雙 AMS1117 獨立供電
* 相依:Arduino core for ESP32 ≥3.0、arduinoFFT 1.4、
* FastLED 3.5、ArduinoJson 6、Wire/Preferences
* 板型:ESP32S3 Dev Module(PSRAM: OPI)
* ============================================================ */
#include <Arduino.h>
#include <driver/i2s.h>
#include <arduinoFFT.h>
#include <FastLED.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
/* ------------------------------------------------------------
* 1. 使用者設定區
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* NAS_URL = "http://192.168.8.50:5000/sound-events";
const char* DEV_TOKEN = "YOUR_DEVICE_TOKEN"; // NAS 規則引擎認證用
#define I2S_A I2S_NUM_0 // 前對(FL=L / FR=R)
#define I2S_B I2S_NUM_1 // 後對(RL=L / RR=R)
#define PIN_BCLK_A 12
#define PIN_WS_A 13
#define PIN_DIN_A 14
#define PIN_BCLK_B 16
#define PIN_WS_B 17
#define PIN_DIN_B 15
#define PIN_LED 4
#define NUM_LED 16
const uint32_t SAMPLE_HZ = 16000;
const uint16_t FFT_N = 512; // 32ms 一幀
const int16_t TAU_MAX = 23; // 25cm ÷ 343m/s @16k ≈ 11.7,留餘裕
const uint16_t VAD_HZ_LO = 300, VAD_HZ_HI = 8000; // 語音/拍手主頻帶
const float VAD_TH_RATIO = 3.0f; // 頻帶能量需 > 底噪 3 倍
const uint16_t CONF_MIN = 40; // 信心度門檻(%)
const uint32_t EVENT_COOL = 60000; // 同方位冷卻 60s 防洗版
/* ------------------------------------------------------------
* 2. 全域緩衝(PSRAM:FFT 工作區不搶 SRAM)
* ------------------------------------------------------------ */
using cmplx = float _Complex;
#define PSRAM_ATTR EXT_RAM_BSS_ATTR // ESP-IDF:全域緩衝配置於 PSRAM
static double vRe[4][FFT_N] PSRAM_ATTR, vIm[4][FFT_N] PSRAM_ATTR; // 4 通道時域/頻域
static double sRe[FFT_N] PSRAM_ATTR, sIm[FFT_N] PSRAM_ATTR; // 互相關專用暫存器(不與通道 2 共用!)
static float hann[FFT_N];
cmplx sRe2[FFT_N] PSRAM_ATTR; // PHAT 加權中間緩衝
arduinoFFT fft;
SemaphoreHandle_t frameReady;
float confPeak = 0, lastTauF = 0, lastTauR = 0, lastIld = 0;
CRGB leds[NUM_LED];
Preferences prefs;
float tauBiasF = 0, tauBiasR = 0, ildBias = 0; // 快拍校準值
float floorE = 1; // 底噪追蹤(頻帶能量)
int32_t azHist[3] = {0,0,0}; uint8_t azIdx = 0; // 3 次中值
uint32_t lastEventMs[8] = {0}; // 8 方位各自冷卻
bool calibrated = false;
/* ------------------------------------------------------------
* micTask —— 核 0:雙 I2S 同步餵資料
* 參數:pv —— 未使用
* 為什麼綁核 0:I2S DMA 完成中斷的時基抖動會直接污染 τ,
* Wi-Fi 中斷全部留給核 1
* ------------------------------------------------------------ */
void micTask(void* pv) {
static int16_t raw[FFT_N * 2]; // 立體聲交錯
for (;;) {
size_t got;
i2s_read(I2S_A, raw, sizeof(raw), &got, portMAX_DELAY);
for (int i = 0; i < FFT_N; i++) {
vRe[0][i] = raw[i*2]; vIm[0][i] = raw[i*2+1]; // FL, FR
}
i2s_read(I2S_B, raw, sizeof(raw), &got, portMAX_DELAY);
for (int i = 0; i < FFT_N; i++) {
vRe[1][i] = raw[i*2]; vIm[1][i] = raw[i*2+1]; // RL, RR
}
xSemaphoreGive(frameReady); // 幀就緒 → 核 1 開算
}
}
/* ------------------------------------------------------------
* gccPhat —— GCC-PHAT 時差估計
* 參數:a/b 兩通道索引;回傳:τ(取樣,含亞取樣內插)
* 原理:互功率譜只留相位(PHAT 加權)→ IFFT 得銳化尖峰;
* 拋物線內插把峰位精到 0.1 取樣 ≈ 6µs ≈ 2mm 波前
* ------------------------------------------------------------ */
float gccPhat(int a, int b) {
for (int i = 0; i < FFT_N; i++) {
vRe[a][i] *= hann[i]; vRe[b][i] *= hann[i]; // 窗函數
vIm[a][i] = 0; vIm[b][i] = 0;
}
fft.input(vRe[a], vIm[a], FFT_N); fft.fft(1, -1, 1);
double *ar = vRe[a], *ai = vIm[a], *br = vRe[b], *bi = vIm[b];
for (int i = 0; i < FFT_N; i++) { // X·conj(Y) 归一化=PHAT
double re = ar[i]*br[i] + ai[i]*bi[i];
double im = ai[i]*br[i] - ar[i]*bi[i];
double mag = hypot(re, im); if (mag < 1e-9) mag = 1e-9;
sRe2[i] = cmplx(re/mag, im/mag);
}
for (int i = 0; i < FFT_N; i++) { sRe[i] = creal(sRe2[i]); sIm[i] = cimag(sRe2[i]); }
fft.input(sRe, sIm, FFT_N); fft.fft(-1, 1, 1); // IFFT
float pk = 0; int pi = 0;
for (int i = 0; i < FFT_N; i++) { // 搜 ±TAU_MAX(環形)
int j = (i <= TAU_MAX) ? i : (FFT_N - i);
if (abs(j) > TAU_MAX) continue;
float m = hypot(sRe[i], sIm[i]);
if (m > pk) { pk = m; pi = (i <= FFT_N/2) ? i : i - FFT_N; }
}
// 拋物線內插:峰左右取樣拟合
int l = (pi + FFT_N) % FFT_N, r = (pi + 1 + FFT_N) % FFT_N, ll = (pi - 1 + FFT_N) % FFT_N;
float y0 = hypot(sRe[ll], sIm[ll]), y1 = pk, y2 = hypot(sRe[r], sIm[r]);
float denom = (y0 - 2*y1 + y2); float sub = denom ? 0.5f*(y0 - y2)/denom : 0;
float conf = pk / (sumEnv() / FFT_N + 1e-6); // 旁瓣比=信心來源
confPeak = conf;
return pi + constrain(sub, -0.5f, 0.5f);
}
/* ------------------------------------------------------------
* solveAzimuth —— 半圓→全圓
* τ(±11.7) → θ=asin(cτ/d):幾何上無法分前後(雙曲線對稱);
* 前後對頻帶能量比 ILD(對數域-校準偏差)判定半平面;
* T 形聲障把 ILD 放大到可判(+4~6dB)
* 回傳:方位 0–359°,信心 0–100
* ------------------------------------------------------------ */
int16_t solveAzimuth(float tauF, float tauR, uint8_t* confOut) {
float c = 343.0f, d = 0.25f;
float sF = constrain((tauF - tauBiasF) * c / (d * SAMPLE_HZ_TO_S), -0.95f, 0.95f);
float sR = constrain((tauR - tauBiasR) * c / (d * SAMPLE_HZ_TO_S), -0.95f, 0.95f);
float thF = degrees(asinf(sF)), thR = degrees(asinf(sR));
float az = (thF + thR) / 2; // 兩對平均:抑制單對偽峰
float ild = 10 * log10f((bandEnergy(0) + bandEnergy(1) + 1e-6f) /
(bandEnergy(2) + bandEnergy(3) + 1e-6f)) - ildBias;
if (ild < -3) az = 180 - az; // 後方較響 → 翻半圓
az = fmodf(az + 360, 360);
float confSide = constrain(fabsf(ild) * 12.0f, 0, 100); // |ild| 3dB≈信心36
*confOut = constrain((uint8_t)lroundf(0.6f*confSide + 0.4f*confPeakScore()), 0, 100);
return (int16_t)az;
}
/* ------------------------------------------------------------
* vadGate —— 300–8000Hz 頻帶能量 vs 底噪(指數追蹤)
* 回傳:true = 有意義聲事件(冷氣/鍵盤不進門)
* ------------------------------------------------------------ */
bool vadGate() {
float e = bandEnergy(0) + bandEnergy(1);
floorE = floorE * 0.995f + e * 0.005f; // ~2s 底噪常數
return e > floorE * VAD_TH_RATIO;
}
void buz(uint8_t p, uint16_t f, uint16_t ms) { tone(p, f, ms); delay(ms); noTone(p); }
/* ------------------------------------------------------------
* calibrateSnap —— 快拍校準:8 方向×3 拍 → τbias/ildBias
* 為什麼拍手掌:宽带脉冲是天然的 GCC 校準源——
* 方向已知(你對著它拍),τ 理論值可算,殘差就是裝配误差
* ------------------------------------------------------------ */
void calibrateSnap() {
for (int dir = 0; dir < 8; dir++) {
Serial.printf("朝 %d 方位拍 3 下(3 秒後開始)\n", dir * 45);
delay(3000);
float tf = 0, tr = 0, il = 0;
for (int k = 0; k < 3; k++) {
waitForClap(); tf += lastTauF; tr += lastTauR; il += lastIld;
buz(10, 2000, 30);
}
storeCalib(dir, tf/3, tr/3, il/3); // 線性回歸存斜率/截距
}
prefs.putFloat("tbF", fitBiasF()); prefs.putFloat("tbR", fitBiasR());
prefs.putFloat("ilB", fitIld()); prefs.putBool("cal", true);
Serial.println("校準完成,重載中..."); esp_restart();
}
/* ------------------------------------------------------------
* reportEvent —— 事件推 NAS(含 Ring Buffer 離線上拋)
* ------------------------------------------------------------ */
void reportEvent(int16_t az, uint8_t conf, float rms) {
StaticJsonDocument<160> doc;
doc["az"] = az; doc["conf"] = conf; doc["rms"] = rms;
doc["ts"] = now(); doc["dev"] = "compass-01";
String body; serializeJson(doc, body);
HTTPClient http;
http.begin(NAS_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-Device-Token", DEV_TOKEN);
if (http.POST((uint8_t*)body.c_str(), body.length()) < 0) ringPush(body); // 斷網緩存
http.end();
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < FFT_N; i++) hann[i] = 0.5f - 0.5f * cosf(2*PI*i/(FFT_N-1));
prefs.begin("compass");
calibrated = prefs.getBool("cal", false);
tauBiasF = prefs.getFloat("tbF", 0); tauBiasR = prefs.getFloat("tbR", 0);
ildBias = prefs.getFloat("ilB", 0);
i2s_cfg(); // 16k/32bit/立體/ DMA 8×512
FastLED.addLeds<WS2812, PIN_LED, GRB>(leds, NUM_LED).setCorrection(TypicalLEDStrip);
pinMode(10, OUTPUT); // 蜂鳴器(校準節拍/錯誤)
xTaskCreatePinnedToCore(micTask, "mic", 4096, nullptr, 8, nullptr, 0);
frameReady = xSemaphoreCreateBinary();
WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
if (SerialTimeout() && !calibrated) calibrateSnap(); // 開機序列輸入 c 可補校
}
void loop() {
xSemaphoreTake(frameReady, portMAX_DELAY); // 核 1:每 32ms 一幀
if (!vadGate()) { idleAnim(); return; }
float tauF = gccPhat(0, 1), tauR = gccPhat(2, 3);
if (fabsf(tauF) > TAU_MAX || fabsf(tauR) > TAU_MAX) return; // 偽峰丟棄
uint8_t conf; int16_t az = solveAzimuth(tauF, tauR, &conf);
if (conf < CONF_MIN) { logOnly(az, conf); return; } // 低信心不上報
azHist[azIdx] = az; azIdx = (azIdx + 1) % 3;
int16_t med = median3(azHist[0], azHist[1], azHist[2]); // 3 幀中值
needleTo(med); // 燈環彗尾指針
if (millis() - lastEventMs[med/45] > EVENT_COOL) {
lastEventMs[med/45] = millis();
reportEvent(med, conf, bandEnergy(0));
}
drainRing(); // 補送離線事件
}
⚠️ INMP441 出廠時常「LR 腳接錯聲道」——接線表的分派是理想值,上電第一件事跑
i2s_probe()(朝單邊拍手掌確認哪支先響);左右接反時 τ 符號翻轉、羅盤直接鏡像 180°,靠快拍校準救不回來(符號錯不是偏移)。📦 NAS 端規則(Node-RED / HA 皆可,本機用 Node-RED function 節點 15 行):收
POST /sound-events → 校驗 X-Device-Token → 規則:①任何時段、方位在玄關扇區(北偏東 30°±45°)、2 分鐘內 ≥3 事件且 conf>60 → Telegram「快遞在玄關(北)」;②22:00–07:00、嬰兒房扇區、哭鬧頻譜標記(頻帶能量>8 倍底噪且持續>2s)→ 電話級推播;③事件寫 InfluxDB 畫「家庭聲場熱圖」——兩週後你會發現全家 60% 的聲音來自電視右後方(就是那個沒關的抽油煙機在跟你說話)。🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印後處理:麥孔去毛邊(刀片旋一圈)、隔板貼吸音棉(兩面都貼,邊緣內縮 3mm)
- 麥克風分選+LR 標記:4 支 INMP441 同聲源比對(差距 >3dB 的挑去當後對——前後對靠 ILD 判向,同側一致性比絕對值要緊);L/R 腳分派用膠帶標記
- 麥架+防塵罩組合:聲孔朝外 5mm、泡棉圈密封壓實——氣室漏氣高頻直接跪
- 供電分壓測:雙 AMS1117 各掛 100nF,先不接 MIC 量 3.3±0.05V;MIC 地與燈環地、邏輯地星形匯流
- 主線束:I2S 線 ≤15cm 雙絞、與 5V 動力線分走後蓋線槽兩側;燈環 330Ω 焊在燈環端不是板端
- 燒錄+聲道探測:跑
i2s_probe()朝四支麥各拍一下,確認聲道對應與符號 - 快拍校準(必做):長按開機進校準,朝 8 方位各拍 3 下——裝配误差、總線相位差一次收攏
- 壁掛定位:1.7m 高、15° 傾角背板、羅盤「北」對準玄關(App 內可設扇區);通电跑 24h 收集底噪後再開推播
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 永遠指同一邊 | 兩條總線時基相位差(最常見) | 跑快拍校準模式,τbias 會把它吸收——這是本設計留的後門,不是設計缺陷 |
| 羅盤鏡像 180° | 某對 MIC 的 LR 腳接反,τ 符號翻轉 | 校準救不了符號:跑 i2s_probe() 確認後調線或改韌體通道映射 |
| 電視連说话亂指 | 每個字都是一個事件 | 上報改「語音段」:掛起 800ms 內取最後方位,整段只報一次 |
| 50Hz 嗡嗡誤觸 | 開關電源紋波輻射進麥 | 換線性 adaptor;韌體 80Hz HPF 已內建,嚴重輻射救不了——電源是聲學系統的地基 |
| 前後偶爾 180° 跳 | 純音(哨子)無頻譜結構、ILD 失效 | 多頻成分(說話/拍手)都準;跳變時信心度 <40 已被擋——別降低 CONF_MIN 硬解 |
| 燈環一亮 VAD 就爆 | WS2812 電流脈衝耦合進麥地 | 燈環 5V 獨立線回電源端、DIN 330Ω、MIC 地星形回主地——三步全做 |
| 夜間 0 事件但人在走動 | 底噪追蹤把說話當底噪吃了(floorE 常數太短) | 底噪常數 2s→5s;或夜間模式改用固定底噪快照(NAS 每天 04:00 重採) |
🎯 進階延伸
- 聲場熱圖:NAS 端把事件方位×時間畫成極座標熱圖,掛牆上当「家庭聲音儀表板」——小孩睡後客廳的噪音來源無所遁形
- 哭鬧頻譜分類:加 TinyML(S3 的 SIMD+PSRAM 養得起 8k 採樣 1D-CNN),區分哭鬧/咳嗽/玻璃碎,推播分級
- 雙羅盤交叉定位:兩台各報方位角,NAS 端三角定位——客厅+走廊兩台,平面定位精度 ±30cm
- 指向性音場錄音:事件觸發時把前後對 64ms 音調存 NAS——「聽」一下當時的現場,確認是快遞還是鄰居吵架
📦 檔案清單
sound-compass/
├── src/
│ └── sound-compass.ino # ESP32 韌體(本頁完整程式)
├── nas/
│ └── node-red-flow.json # 規則引擎流程(玄關/嬰兒房/熱圖)
├── stl/
│ ├── compass-shell.stl # 羅盤外殼(壁掛)
│ ├── baffle-t.stl # T 形聲障隔板(PETG,120/150 兩版)
│ ├── mic-holder.stl # 麥克風內嵌麥架
│ ├── mic-hood.stl # 麥頭防塵罩
│ ├── back-cover.stl # 後蓋走線槽
│ └── tilt-bracket.stl # 傾角壁掛背板
├── f3d/
│ └── compass-design.f3d # Fusion 360 原始檔
├── test/
│ └── i2s-probe.ino # 聲道探測+τ 符號驗證
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件