你睡著時只有兩件事在跑:一個 AAC 編碼器,和一個 20Hz 的特徵迴圈,後者只留下 10Hz 的音量包絡。就這樣。夜間不跑任何機器學習、不上傳任何東西,也不做任何偵測判斷——半夜對著不完整的資料做的決定,是不可逆的。早上 App 把錄音走過一次:包絡挑出有聲音的區段,Apple 的聲音分類器只讀這些區段,存活下來的視窗才成為事件,附上音檔、音量與嚴重度。這一頁的每個數字都來自出貨原始碼。
以下參數取自 App 原始碼(AudioPipeline、Screeners、EnvelopeGate、BatchProcessor、Theme、CLOUD_SYNC_SPEC)。模擬資料都有標示。
設計鐵律是:夜間階段必須在電池上撐過 iOS 八小時的背景排程。所以夜間只做加減乘除——整流、濾波、累加——凡是「事後看得更清楚」的判斷,全部留到早上;那時整份檔案就在硬碟上,門檻設錯可以重跑一次。
AVAudioEngine tap,4800 frame 一批(48kHz 約 100ms)。工作階段用 .playAndRecord + .measurement,關掉系統 AGC 與 EQ,量測才乾淨。一個無聲 player node 讓工作階段在來電、鬧鐘之後仍然活著。
50ms 一框,每秒 20 框。每框產出四個數字:整體 RMS、鼾聲帶 RMS、人聲帶 RMS、零交越率。兩條四階 Biquad 並行——鼾聲帶 40–400Hz,人聲帶 250–3000Hz。
每兩框取較大值,得到 10Hz 的音量軌——峰值不能被平均洗掉。每 30 秒落地一次。十小時約 1.2MB。
錄音時已算好的包絡被掃過一次,找出活躍區段:門檻 = 第 20 百分位底噪 × 2.5,每段前後外擴 2 秒,相距不到 5 秒就併成一段。採相對底噪,旅館的吵冷氣與安靜臥室才能同時成立。
SNAudioStreamAnalyzer 搭配 Apple 內建分類器,只餵活躍區段,並傳入真實檔案 frame 位置。鼾聲類 = {snoring, snort};說夢話類 = {speech, whispering, shout, singing, babbling}。
同類視窗間隔 ≤ 2 秒就合併,短於 0.5 秒丟棄。存活的每一筆裁出前後各 1 秒的音檔、正規化到 0.9 峰值,並算出峰值 dB、平均位準與嚴重度。
整晚錄音的任務已完成,預設刪除——留下來的是事件片段。轉文字是按需的:你點哪一段才辨識那一段。
一排是一小時,一格是十秒。方格亮度是固定刻度上的音量,所以不同夜晚的排可以並排比較——多數睡眠 App 把每晚各自正規化到當晚最大值,結果每個夜晚看起來都一樣。在織帶上拖動,讀出管線在那一刻看到什麼。夜晚是模擬的;參數、色票與映射都是 App 的。
這裡的音量是 dBFS(數位滿刻度)。App 另外呈現推估的 dB SPL,那取決於裝置與手機擺放位置——夜與夜之間一致,但不是校準過的噪音計。
這是把五分鐘的分析變成幾秒鐘的那個判斷。包絡很便宜、而且已經在硬碟上;閘門用它決定哪些部分值得跑那個昂貴的 pass。拖動三個控制項,看活躍占比怎麼變。第 20 百分位底噪由資料自己決定、不由你決定——這正是相對門檻的意義。
包絡以 10Hz 繪製,依畫布寬度做 max pooling。高亮區段就是 SNAudioStreamAnalyzer 實際收到的內容,並帶著真實 frame 位置,所以視窗時間戳不需要換算。
靈敏度設定只是一個 1 到 10 的數字,它把最低信心門檻從 0.70 拉到 0.25——嚴到寬,預設 5 對應 0.50。更細緻的判斷是:要往分類器輸出的第幾名看。旅館的冷氣會產生一個恆定類別、整晚霸佔第一名,把鼾聲擠到第二名。只讀第一名會丟掉整夜;App 掃 top-5,取第一個達到門檻的目標類別。
模擬視窗,信心值落在真實分類器的輸出範圍內。合併與最短長度用的是出貨規則:同類 2 秒內合併,短於 0.5 秒丟棄。
| 1 | white noise | 0.71 | — |
| 2 | mechanical fan | 0.63 | — |
| 3 | snoring | 0.58 | ← target ✓ |
| 4 | breathing | 0.31 | — |
| 5 | speech | 0.12 | — |
這是開著風扇的房間裡真實的 top-5。第一個達到門檻的目標類別勝出,不管它排第幾。
鼾聲是低頻主導,講話是中頻主導,所以夜間迴圈把兩條帶分開保留。它同時保留一個主導性判準:鼾聲要 low > mid × 2,人聲要 mid > low × 0.35。以真實 zh-TW 語音過管線量測,語音框只有約 23% 達到 low > mid × 2——這就是講話不會被記成鼾聲的原因。這些粗篩驅動工程模式的量測面板、也塑造了整個設計;但它們在夜間產出的事件是算完就丟的,因為真正的偵測器是早晨的分類器。
| rawRMS | 未濾波 RMS | 現場波形、織帶、包絡 |
| lowRMS | 40–400Hz RMS | 鼾聲粗篩、主導性判準 |
| midRMS | 250–3000Hz RMS | 人聲粗篩、主導性判準 |
| zcr | 零交越次數 ÷ 框長 | 像不像人聲,0.006–0.35 |
| 自適應底噪 | EMA,α = 0.002,只在未觸發時更新——事件中更新會把門檻追高,事件被自己吃掉。 |
| 觸發門檻 | max(底噪 × 倍率, 0.0009)。絕對下限約 −61 dBFS,死寂房間不會被自己的底噪抖動觸發。 |
| 遲滯 | 釋放門檻 = 觸發 × 0.6,且需維持 1.2 秒——鼾聲一陣一陣,中間有換氣空隙。 |
| 啟動 | 鼾聲需連續 4 框(200ms),人聲需 6 框(300ms)。 |
| 事件長度 | 鼾聲 0.5–30 秒,說夢話 0.4–20 秒。結尾取最後一個高於釋放門檻的框,不是等待結束的那一刻。 |
| 持續噪音防護 | 一次鼾聲事件不可能持續 8 秒——換氣會結束它。連續 8 秒沒結束就判定是風扇或冷氣:底噪學習到觀測峰值的 90%,該段丟棄。 |
| 靈敏度映射 | 1 → 5.0×,10 → 1.5×,線性內插;預設 5 得到高於底噪 3.4×。 |
| 平台 | iPhone 與 iPad,iOS 17.6 或以上 |
| 音訊工作階段 | .playAndRecord、mode .measurement——關閉 AGC 與 EQ |
| Tap buffer | 4800 frame,48kHz 約 100ms |
| 整晚錄音 | AAC m4a,裝置原生取樣率,quality medium,約 200MB/晚 |
| 分框 | 50ms 一框,20 fps |
| 濾波帶 | 鼾聲 40–400Hz、人聲 250–3000Hz,各為四階 Biquad |
| 包絡 | 10Hz,每 2 框取 max,每 30 秒落地,10 小時約 1.2MB |
| 原始環形緩衝 | 45 秒音訊,供抽取片段 |
| 靜音閘門 | 第 20 百分位底噪 × 2.5,外擴 2s,合併 5s,活躍 70% 即退回整檔 |
| 分類器 | SNClassifySoundRequest,identifier .version1,裝置端 |
| 信心門檻範圍 | 0.70(靈敏度 1)到 0.25(靈敏度 10),預設 0.50 |
| 類別集合 | snoring, snort / speech, whispering, shout, singing, babbling |
| 事件規則 | 同類 2 秒內合併,短於 0.5 秒丟棄 |
| 音檔片段 | 事件前後各 1 秒,正規化到 0.9 峰值 |
| 嚴重度 | 0–100:0.6 × 響度(−50 → −10 dBFS)+ 0.4 × 持續時間(0 → 20s) |
| 轉文字 | zh-TW,requiresOnDeviceRecognition = true,30 秒超時,按需執行 |
| 新引擎 | 若已安裝則優先用 iOS 26 SpeechTranscriber,全程裝置端 |
| 同步 | CloudKit 私有資料庫,自訂 SessionsZone,CKSyncEngine + CKOperation |
| 網路呼叫 | 整個專案沒有任何一處 |
| 主題 | 29 套預設,每套含深色與淺色兩份配色 |
| 織帶方格 | 一格 10 秒,預設絕對刻度 |
| 時間軸縮放 | 第 95 百分位正規化,單一巨響不會把整夜壓扁 |
沒有我們的伺服器,也沒有帳號系統。同步用的是你自己 Apple ID 之下的 CloudKit 私有資料庫:metadata 走 CKSyncEngine,資產走直接操作——這樣音檔片段才能做到「你按播放才抓」。整晚錄音永不上傳,它在分析後於本機刪除。離線不是錯誤狀態:iCloud 不可用時,App 就是它原本那個純本機 App,而網路狀態永遠不能阻擋開始錄音。
| session.json — 事件、時間、狀態 | 數十 KB | 雲端永遠有,本機永遠留 |
| envelope.bin — 10Hz 整晚包絡 | 約 1.2MB | 雲端永遠有,本機永遠留 |
| clips/evt-* — 事件音檔 | 數 MB | 雲端有;本機副本可卸載,播放時再抓 |
| audio.m4a — 整晚原始錄音 | 約 200MB | 永不上雲,分析完即刪 |
| 帳號、email、識別碼 | — | 不收集,沒有東西可送 |
| 分析工具、廣告、崩潰回報 SDK | — | 完全沒有 |
一筆 CloudKit 記錄當錄音鎖,用 compare-and-swap 搶,並以每 5 分鐘一次的心跳對抗 15 分鐘租約——裝置沒電或當機時,永遠沒機會執行「釋放」,所以沒有心跳的鎖一次意外就永久死鎖。而且這個鎖是禮貌、不是正確性閘門:檢查失敗或離線時照常開錄。兩段重疊的 session 就是兩個 UUID,兩筆都留。
MoyiSnore 不是醫療器材。它記錄與量測聲音,不偵測呼吸中止或任何疾病。一份客觀、有時間戳的紀錄適合帶去給醫師參考——但不能取代醫師。
手機無法分辨聽到的是誰的鼾聲。同床時,請把報告讀成「這個房間」的紀錄。
音量取決於距離,以及手機放在什麼東西上。每晚放在同一個位置,才讓夜與夜之間可以比較。
App 用的是 Apple 內建的聲音分類器,不是專門用鼾聲訓練的模型。我們對此誠實:每個事件都保留它的類別、信心值與音檔,任何一筆你都能自己核對。
電風扇、路過的車、身旁的人翻身。持續噪音防護與信心門檻能濾掉大部分,但不是全部。
文獻用於設計依據;App 不作任何臨床聲明。