← Notes

How the iPhone recording array works

September 19, 2026 · OdiMeet development notes

Alice3REC00:00
Receiving
aligned · offset 0.11 s
AliceAlign the two tracks first.
BenThen see who is louder.
AliceCross-talk keeps both.
Sent ✓
Ben3REC00:00
Sending
Sent ✓
Pair: Wi‑Fi + Bluetooth, six-digit code.

Interviews, meetings, podcast field recordings, consultations — when one of those ends, what you actually want is not an audio file but a summary that answers questions: who said what, what the key points were, which action items came out of it, what was stated as fact, which moments are worth replaying. And that material is often sensitive enough that it should never leave your phone, so the tool has to be privacy-first.

"Who said what" needs speaker diarization first. The mainstream way is algorithmic: slice the audio into short segments, extract a voice-print embedding for each with a model such as an x-vector or ECAPA-TDNN, then cluster similar segments into the same speaker — pyannote.audio, NVIDIA NeMo and WhisperX all work this way; the newer EEND family trains one network to output "who is talking now" end to end. All of them are clever, and all of them share a weakness: at bottom they are guessing. Similar voices, two people talking over each other, someone leaning into the microphone — the guess breaks, and every vendor's documentation says so.

OdiMeet wants to explore a different route: several iPhones as a microphone array, one in front of each speaker, deciding who is talking by physics rather than judgement — whoever is closest is loudest on their own phone. This note is the story of getting that from an idea to something that works on two real recordings, including the two times my own measurements lied to me. So how is it actually done?

Where the usual gadgets put your privacy

The products in this space follow the same recipe. One: you buy a small recording device for a hundred and something, sometimes two hundred dollars — some are badges you clip to your chest. Two: after the meeting, the device or its app turns the audio into text locally. Three: the content goes up to the vendor's servers, where the cloud does the speaker diarization and generates the summary, the mind map, the to-do list. Step three is what makes it convenient, and it is also the price: everything you said, everything the other side said, all of it has been read in full by that company and now lives on their machines.

OdiMeet is built the other way round. Who is talking is decided by the iPhone array; capture, recording, transcription, alignment and merging all happen inside those few iPhones. Only when you want a summary do you choose to send the already speaker-labelled transcript to Google Gemini — with your own key, straight to Google, nothing of ours in between. We have no servers; strictly speaking, apart from you, only Gemini ever sees that text, and only at the moment you ask for a summary. If you'd rather skip even that, export the labelled transcript and hand it to a local model on your own computer — then not a single word leaves your devices.

Physics instead of a model

Distance doesn't fail the way voice-prints do. A phone 40 cm from its speaker and 1.5–2 m from everyone else hears its own person ten to twenty decibels louder — the inverse-square law, doing the work a neural network can't do reliably.

So the pipeline is deliberately dumb: align the tracks, keep only the voice band (300–3400 Hz), measure each track's energy every 200 ms, and hand the moment to the loudest track. If two tracks are both loud and close, both people are talking, and both are kept. Then each speaker is transcribed from their own microphone — the closest, cleanest one — and the pieces are merged by time.

Two phones, one start

The phones pair over Multipeer Connectivity — Wi‑Fi and Bluetooth, no network, no account — gated by a six-digit code. They ping each other to estimate the clock offset, the host counts down 3‑2‑1, and both start recording 48 kHz WAV at the agreed instant. When the host stops, the guest sends its track back automatically, so the host holds both and can merge them with one tap.

The first real take was 14 seconds: two phones about 1.5 m apart on a table, each 40 cm from its speaker, two people alternating. I pulled the two WAV files off and looked at the numbers before writing another line of app code.

Trap one: I found a bug that didn't exist

The energy numbers were good news: on windows with speech, the two tracks differed by a median 10.6 dB. But the alignment step said the tracks were 2.83 seconds apart. A synchronized start that misses by three seconds is broken, so I went hunting in the clock code — rewrote the calibration, switched to absolute timestamps, added a spin-wait to the exact instant. The next take came back "3.90 seconds off". Worse.

The clock was fine. My aligner was wrong. I was cross-correlating the two tracks' loudness envelopes — and when two people alternate, the envelopes are anti-correlated at the true offset (A loud while B quiet), so the search locks onto some other lag where A's speech happens to line up with B's. The method that works is GCC‑PHAT on the waveforms: the same words reach both microphones a few milliseconds apart, so the phase correlation peaks at the true offset regardless of who is talking. The real number was 88 ms. The synchronized start had been good all along.

Trap two: a suspiciously perfect 0.00 s

Next take, 20 seconds, with names entered this time. The app reported an alignment offset of 0.00 s. Two independent phones do not land within one sample of each other; the previous take had been 88 ms. I looked at every segment separately: 0.000 ms, every time, with a correlation peak 530 times its background. And the per-frequency coherence between the tracks was 0.004. The "perfect" peak had no acoustic content at all.

The cause was a step I'd added for good reasons. Before correlating, I band-passed both tracks by zeroing FFT bins outside 300–3400 Hz — a brick-wall filter. That stamps identical Gibbs ringing onto both tracks at identical sample positions, and the correlator found it and called it a match. Band-limiting inside the cross-spectrum instead, with no filter on the time signals, gave the real offset: 106 ms, with a clean 61× peak.

That one now has a regression test with the structure of the real failure: two independent alternating talkers, only −16 dB of bleed between them, one started 106 ms late. The old code returns 0. The new code returns 106.

Once the alignment was right

Sync-start error, three real takes88 · 106 · 53 ms
Speaker separation, median over speech windows (1.5 m · 1.5 m · 2 m)12.1 · 13.7 · 17.2 dB
Windows attributed with ≥ 6 dB margin93 · 97 · 96 %
Cross-talk windowskept for both

The merged transcript labels each line with the name entered before pairing, and the tester's verdict on the second take was "separated, quite good". Two smaller traps on the way there, for completeness: a merged conversation borrows track A's audio file and session id, so the next "combine" paired A with itself (offset 0.00, separation 0.0 dB — the same tell); and iOS returns the literal string "iPhone" as the device name to apps, so both speakers were called iPhone until we asked for a name up front.

So how good is the algorithm, really? We ran it too — after being schooled by our own data

Earlier I called the mainstream approach a guess, citing other people's documentation. I took a recording, one track only, and ran the route a single phone can run: pyannote segmentation‑3.0, 3D‑Speaker voice-print embeddings, clustering — the whole chain on device through sherpa‑onnx, about 40 MB of models. My first comparison used the array as the answer key, produced a set of numbers, and went live; hours later I learned that the 20‑second take had been one person taking turns toward each phone. The array was fine — it measures position, and all seven turns were right — but for a voice-print algorithm the correct answer was "one speaker", and my conclusion was backwards. One thing learned in passing: distance leaks into voice-prints; the same person at 40 cm and at 1.5 m looks like two people to the algorithm.

So we recorded again: two different people, 2 metres apart, 41 seconds, strict turn-taking with a few deliberate interruptions at the end — this time the answer key is the known order, not either method's output.

The array: alignment error 53 ms; across 28 seconds of speech the two tracks differ by a median 17.2 dB, with 96% of windows decided by a ≥ 6 dB margin; 18 utterances, strictly alternating, 18 of 18; three 200 ms windows flagged as both speaking, all between 30.8 and 31.6 s — exactly where the interruptions were. Moving from 1.5 to 2 metres made separation better, not worse.

The algorithm depends on which track it gets and on whether someone tells it how many people there are:

Told "two speakers" and given the track where both voices are reasonably audible (B, the less sensitive microphone, where the far person is relatively less faint), it reaches 90% — 19 utterances with a single extra cut, an honestly good result. On the other track the far speaker is so faint that the segmentation model drops nearly half the speech, and what remains is clustered by time rather than by person: 37%. And in the room you don't know which kind of track you are holding. More practically: nobody types in a speaker count before a real meeting — left to guess, the algorithm found 1 person on one track and 8 on the other.

The array needs no speaker count and doesn't care how clear anyone's voice is; it only looks at distance. So OdiMeet ships both, in a clear order: Precision Mode with two iPhones is measured; single-phone speaker separation is an estimate for when there is no second phone, off by default and labelled that way.

So, can a three-iPhone array reconstruct the room?

By this point a question suggests itself: if the array knows who is talking, and measures when each phone hears it, can we run it backwards and draw the room — who sits where, next to whom, across from whom?

Physically it is the same data. Sound travels at 343 m/s, so two metres is 5.8 ms, and the phase correlation we already use for alignment resolves well under a millisecond. And a sentence comes out of one mouth: when A talks, A's phone hears it first and B's a little later; when B talks, the other way round. The average of the two lags is the clock offset; half the difference is the extra distance the sound travelled. So I re-measured all three takes utterance by utterance, tapering each span with a Hann window (so the cut edges don't stamp identical clicks on both tracks — a cousin of the earlier trap):

Spread of per-utterance lags for the same speaker± 0.1 ms ≈ 3 cm
Take 1 (1.5 m): extra path to the far phone, recovered1.0 m (expected ≈ 1.1 m)
Take 3 (2 m): the same calculation9.5 m (impossible)

The first two rows are good news: the measurement is stable to three centimetres, and the first take's distance comes out right. The third row is an open puzzle: every one of B's utterances arrives at A's phone exactly 27.7 ms late — far too consistent for noise, far too long for the direct path. Most likely A's phone never got B's direct sound (blocked, or too faint) and the correlation locked onto a strong reflection with a round trip of about 9.5 m. It is also a reminder that geometry may only trust physically possible lags; a two-metre table does not produce 27 ms.

A useful by-product fell out: the single whole-file alignment leans toward whoever talked more — take 3's "53 ms" was really A's value, and the true clock offset is about 66 ms. Harmless for 200 ms attribution windows, but geometry needs the average of both speakers.

And three phones? Two phones yield one distance. With three, each speaker's words give three pairwise lags, which is the distance between each pair of phones — a triangle, and with it who is adjacent to whom and who sits opposite. What distances alone cannot give is left versus right: a mirrored triangle has the same sides. Breaking the mirror needs one directional reference, and an iPhone carries two ready-made: each phone's compass heading (read once at pairing; 5–15° of error is plenty to tell left from right), or stereo recording, available since the iPhone XS.

None of this is in the app yet. It is an exploration — but the raw material is already inside every array recording, with nothing extra to capture. If it holds up, a meeting would end with a transcript and a small top-down sketch: who sat where, who spoke to whom and how much. A product with one microphone cannot draw that picture.

What I'm keeping from this

My aligner passed a synthetic test — one signal delayed by 1.234 s — perfectly, then failed on real audio twice. The synthetic case shared none of the real case's structure: two independent talkers, faint bleed, alternation. Validate an estimator on the case it will actually see.

And an answer that is too good is the instrument talking about itself. Two phones aligned to 0.02 ms, a peak 500 times its background, an offset landing exactly on the search boundary — each of those was the estimator, not the room. The check that settled it was asking what was coherent, and finding nothing was.

This ships as Precision Mode in OdiMeet 1.1: pair two iPhones, record together, tap Combine. With a single phone, optional on-device speaker separation gives you labels marked as estimates. Version 1.0 — recording, importing from any voice recorder, on-device transcription and Gemini summaries with your own key — is in App Store review now.

OdiMeet is free for 90 days, then $2.99 a month, $29.99 a year, or $39.99 once. Follow odimeet.app for the App Store link.

← 開發筆記

iPhone 錄音陣列怎麼做到的

2026 年 9 月 19 日 · OdiMeet 開發筆記

小美3錄音00:00
接收中
已對齊 · 偏移 0.11 s
小美先把兩軌對齊。
阿明再比哪一支大聲。
小美同時講就兩邊都留。
已傳送 ✓
阿明3錄音00:00
傳送中
已傳送 ✓
配對:Wi‑Fi + 藍牙,六位數配對碼。

訪談、開會、podcast 現場錄音、諮詢——這些場合結束之後,你真正想要的不是一個音檔,而是一份能回答問題的摘要:誰說了什麼、重點是什麼、有哪些 action items、哪些是事實、哪些是值得回頭聽的 highlights。而且這些內容常常敏感到不該離開你的手機,所以這個工具必須是隱私優先的。

要做到「誰說了什麼」,就得先做人聲分離(speaker diarization)。主流做法是演算法:先把聲音切成小段,用 x-vector 或 ECAPA-TDNN 這類模型抽出每段的「聲紋」向量,再用聚類把相似的段落歸為同一個人——pyannote.audio、NVIDIA NeMo、WhisperX 走的都是這條路;更新的 EEND 系列則試著讓一個神經網路端到端直接輸出「此刻誰在講」。它們都很聰明,也都有同一個弱點:本質上是在猜——聲音相近、兩人搶話、有人靠近麥克風,猜就會錯,而且每一家的文件都承認這點。

OdiMeet 想探索的是另一條路:用多支 iPhone 組成一組麥克風陣列,每位講者面前一支,用物理而不是判斷來決定誰在講——離誰近,誰的手機就大聲。這篇筆記記錄它從想法變成在兩段真實錄音上能用的東西;中間我踩了三個坑,其中兩個是我自己的量測騙了我。那它到底是怎麼做到的?

先說傳統做法把你的隱私放在哪裡

市面上這類產品的套路差不多:一、要你買一個一百多、甚至兩百多美金的小錄音裝置,有的做成可以夾在胸口的牌子;二、錄完之後,裝置或 app 在本地把聲音辨識成文字;三、然後把內容上傳到他們的伺服器,在雲端做人聲分離、生成會議摘要、心智圖、待辦清單。第三步聽起來很方便,代價是:你在會議裡講了什麼、對方講了什麼、所有的隱私,都被這家廠商完整看過一遍,存在他們那裡。

OdiMeet 反過來。誰在講話,靠 iPhone 陣列來分辨;聲音的採集、錄音、辨識、對齊、合併,全部發生在這幾支 iPhone 裡。只有當你需要進一步的摘要時,才由你自己決定把已經分好講者的逐字稿送給 Google Gemini——用你自己的金鑰,直接連 Google,中間沒有我們。我們沒有伺服器;嚴格說,除了你,只有 Gemini 看得到那份文字,而且只在你按下摘要的時候。如果連這一步都不想要,把分好講者的逐字稿匯出、丟給你自己電腦上的本地模型就行——這樣沒有任何一個字離開你的裝置。

用物理,不用模型

聲紋會錯的地方,距離不會錯:一支手機離自己的講者 40 公分、離其他人 1.5–2 公尺,聽到自己面前的人會大十到二十分貝——反平方律替我們做了神經網路做不穩的事。

所以整條流程刻意做得很笨:先對齊兩軌,只留人聲頻帶(300–3400 Hz),每 200 毫秒量一次各軌的能量,把那個瞬間判給最大聲的一軌。兩軌都大聲又差不多,就是兩個人同時在講,兩邊都保留。然後每位講者用自己那支麥克風轉錄——最近、最乾淨的那支——再按時間合併。

兩支手機,同一個開始

手機之間用 Multipeer Connectivity 配對——Wi‑Fi 加藍牙,不用網路、不用帳號——以六位數配對碼把關。互相 ping 幾次估出時鐘差,主控倒數 3‑2‑1,兩支在約好的瞬間同時開始錄 48 kHz WAV。主控按停止,來賓那支會自動把它的音軌傳回來,主控手上就有兩軌,按一下就能合併。

第一段真實錄音 14 秒:兩支手機在桌上隔約 1.5 公尺,各離講者 40 公分,兩個人輪流講。我把兩個 WAV 拉出來先看數字,再決定要不要往 app 裡寫。

第一個坑:我「發現」了一個不存在的 bug

能量的數字是好消息:有人講話的窗,兩軌相差中位數 10.6 dB。但對齊那一步說兩軌差了 2.83 秒。同步開始差三秒,那就是壞了——於是我鑽進時鐘的程式碼:重寫校時、改成絕對時刻、加上等到精確瞬間的 spin-wait。下一段錄音回來:「差 3.90 秒」。更糟。

時鐘沒問題,是我的對齊器錯了。我拿兩軌的音量包絡做互相關——但兩個人輪流講話時,包絡在真實偏移處是反相關的(A 大聲時 B 正好小聲),演算法就鎖到某個 A 的講話剛好對上 B 講話的假 lag。對的方法是對波形做 GCC‑PHAT:同一句話會進到兩支麥克風,只差幾毫秒,所以相位互相關不管誰在講都會在真實偏移處出現峰值。真實數字是 88 毫秒。同步開始從頭到尾都是好的。

第二個坑:完美得可疑的 0.00 秒

再錄一段,20 秒,這次先填了名字。App 顯示對齊偏移 0.00 秒。兩支獨立的手機不會對到一個 sample 都不差,上一段還是 88 毫秒。我把每個片段分開算:每一段都是 0.000 毫秒,相關峰值是背景的 530 倍。可是兩軌的逐頻率相干性只有 0.004——那個「完美」的峰值裡沒有任何聲學內容。

原因是我為了正確理由加的一步。互相關之前,我先把兩軌都做了帶通:直接把 300–3400 Hz 以外的 FFT bin 歸零,一個硬切濾波器。這會在兩軌相同的取樣位置留下一模一樣的 Gibbs 振鈴,互相關找到它,說「對上了」。改成在互相關頻譜裡限制頻帶、完全不動時域訊號,真實偏移就出來了:106 毫秒,峰值乾淨的 61 倍。

這個坑現在有一個回歸測試,結構照著真實的失敗案例做:兩個獨立、輪流講話的合成講者,彼此只有 −16 dB 的微弱串音,其中一個晚 106 毫秒開始。舊程式回 0,新程式回 106。

對齊對了之後

同步開始的誤差,三段真實錄音88 · 106 · 53 ms
講者分離度,語音窗中位數(1.5 m · 1.5 m · 2 m)12.1 · 13.7 · 17.2 dB
以 ≥ 6 dB 差距判定的窗93 · 97 · 96 %
同時講話的窗兩邊都保留

合併出來的逐字稿,每一句前面是配對前填的名字;第二段錄音測試者的評語是「分離了,相當好」。順帶記兩個小坑:合併結果借用 A 軌的音檔、又帶著同一個 session id,下一次「合併」就把 A 和它自己合併了(偏移 0.00、分離 0.0 dB——同一種徵兆);還有 iOS 給 app 的裝置名稱就是字面上的「iPhone」,兩位講者都叫 iPhone,直到我們改成配對前先問名字。

那演算法到底能做到多好?我們也跑了——先被自己的資料教了一課

前面說主流做法是「猜」,但那是引用別人的文件。我把錄音只取一軌,丟給單支手機能跑的演算法路線:pyannote segmentation‑3.0 分段、3D‑Speaker 聲紋向量、再聚類——整條鏈透過 sherpa‑onnx 在裝置上跑,兩個模型合計約 40 MB。第一次比對我拿陣列當答案卷、寫了一組數字發上去,幾小時後才知道那 20 秒其實是同一個人輪流對著兩支手機講。陣列沒事——它量的是位置,7 句全對——但對聲紋演算法來說正確答案是「一個人」,我的結論剛好反了。順帶學到一件事:距離會滲進聲紋向量,同一個人在 40 cm 和 1.5 m 兩處,演算法看成兩個人。

所以重錄:兩個不同的人,相距 2 公尺,41 秒,嚴格輪流講、最後幾句刻意插話——這次的答案卷是已知的順序,不是任何一方的輸出。

陣列:對齊誤差 53 毫秒;有聲的 28 秒裡,兩軌能量差中位數 17.2 dB,96% 的窗以 ≥ 6 dB 清楚判定;切出 18 句,A/B 嚴格交替,18/18;三個 200 毫秒的窗判為兩人同時講,全落在 30.8–31.6 秒——正是插話的地方。距離從 1.5 拉到 2 公尺,分離度反而更好。

演算法就看它拿到哪一軌、以及有沒有人告訴它幾個人:

告訴它「兩個人」、又拿到兩人都夠清楚的那一軌(B,麥克風較不靈敏,遠端的人相對沒那麼小),它做到 90%,19 句只多切了一刀——這是誠實的好成績。但在另一軌上,遠端講者小到分段模型直接漏掉近半的語音,剩下的還按時間前後分群而不是按人,只剩 37%。而現場你不會知道自己手上是哪一種軌。更實際的一點:真實會議裡沒人先輸入人數——不告訴它幾個人,它在兩軌上分別猜了 1 個人和 8 個人。

陣列不需要知道人數,也不在乎誰的聲音清不清楚,它只看距離。所以 OdiMeet 兩個都做,排序很清楚:兩支 iPhone 的精準模式是「量出來的」;單機的說話者分離是「沒有第二支手機時的估計」,預設關閉,介面上也這樣標。

那麼,三支 iPhone 陣列有辦法重建空間嗎?

寫到這裡,一個自然的問題冒出來:既然陣列知道誰在講、又量得到聲音到每支手機的時間差,能不能反過來畫出房間——誰坐哪、誰在誰旁邊、誰對面?

物理上是同一份資料。聲速 343 m/s,兩公尺差 5.8 毫秒;我們對齊用的相位互相關解析度在毫秒以下。而且一句話只從一個人的嘴出來:A 講話時,A 的手機先收到、B 的手機晚一點;B 講話時反過來。兩者的平均是時鐘偏移,差的一半是聲音多走的路。所以我把三段錄音逐句重算,兩端用 Hann 窗漸變(免得切口在兩軌相同的位置留下一樣的 click——上一個坑的變體):

同一講者、逐句量到的時間差,離散度± 0.1 ms ≈ 3 cm
第一段(1.5 m):換算「到遠端手機多走的路」1.0 m(預期 ≈ 1.1 m)
第三段(2 m):同樣的換算9.5 m(不可能)

前兩行是好消息:量測本身穩到 3 公分,第一段的距離也對上了。第三行是一個還沒解開的謎:B 講話時,每一句在 A 的手機上都晚了整整 27.7 毫秒——太一致了不像雜訊,太長了不像直達聲。最可能是 A 那支手機沒收到 B 的直達聲(被擋住或太弱),互相關鎖到了一個往返約 9.5 m 的強反射。這也提醒我:做幾何時只能採信物理上可能的時間差,一張 2 公尺的桌子不會出現 27 毫秒。

順帶抓到一個對現有功能有用的副產品:整檔一次算出的對齊值會偏向講話多的那個人——第三段的「53 毫秒」其實是 A 的值,真正的時鐘偏移約 66 毫秒。對 200 毫秒的歸屬窗無傷,但要量幾何就得改用兩位講者的平均。

那三支呢?兩支手機只能量出一個距離。三支手機,每位講者的話給出三組時間差,就是三支手機兩兩之間的距離——一個三角形,誰和誰相鄰、誰對面就出來了。差在左右:光靠距離,鏡像分不出來,三角形翻過來所有邊長都一樣。要打破鏡像需要一個方向參考,而 iPhone 剛好有兩個現成的:每支手機的羅盤方位(配對時順手讀一次,5–15° 的誤差足夠分左右),或是 iPhone XS 起支援的立體聲錄音。

這些都還沒寫進 app。它是一個探索,但探索的材料已經躺在每一次陣列錄音裡,不用多錄任何東西。如果它成立,會後除了逐字稿,還能多一張小小的俯視圖:誰坐哪、誰對誰講了多少話——只有一支麥克風的產品,做不出這張圖。

我要留下的教訓

我的對齊器通過了一個合成測試——一段訊號延遲 1.234 秒——完美無缺,然後在真實音檔上敗了兩次。合成案例沒有真實案例的任何結構:兩個獨立講者、微弱串音、輪流發言。估計器要用它真正會遇到的案例來驗證。

還有,好到不像真的答案,是儀器在講它自己。兩支手機對齊到 0.02 毫秒、峰值是背景的五百倍、偏移剛好落在搜尋邊界——每一個都是估計器,不是那間房間。最後把事情定下來的,是去問什麼東西是相干的,然後發現什麼都不是。

這會以 精準模式 隨 OdiMeet 1.1 推出:兩支 iPhone 配對、一起錄、按一下合併;只有一支手機時,可選的裝置端說話者分離會給你標示為估計的講者標記。1.0 版——錄音、匯入任何錄音筆的檔案、裝置端轉錄、用自己金鑰的 Gemini 摘要——正在 App Store 審核中。

OdiMeet 免費試用 90 天,之後月費 $2.99、年費 $29.99,或一次買斷 $39.99。App Store 連結會放在 odimeet.app。