The Sonarish store page has a screenshot showing 16 breaths a minute, a signal quality of 92% and a displacement of 118 µm. No strap, no camera, no contact. The phone sat on a nightstand, played a tone most adults cannot hear and listened to what came back. Chest position fell out of the arithmetic. The store page gets roughly 40 words to explain that, and the math deserves more, so this post is the long version.
The short version fits in four sentences. Emit a continuous tone near 19 kHz from the speaker. The echo off a sleeping chest travels a path whose length changes as the chest rises and falls. Path length maps to phase, and phase on a clean carrier is measurable to a fraction of a degree. Everything after that is filtering.
Pick a tone the speaker can make and you cannot hear
The carrier frequency is boxed in from three sides. Sonarish captures at 48 kHz like every other uranashel audio path, so Nyquist caps the world at 24 kHz. Adult hearing collapses fast above 16 kHz; a typical 40-year-old tops out near 15 kHz and does not want a nightly sonar serenade regardless. And phone speakers, built for voices and ringtones, roll off steeply past 15 kHz while still producing usable output somewhat higher. The intersection of those constraints is the 18–20 kHz band.
Wavelength decides what the measurement can see. With λ = c/f and c = 343 m/s at 20 °C, an 18 kHz tone has λ = 19.1 mm and a 20 kHz tone has λ = 17.2 mm. Sonarish defaults to 19 kHz, so λ = 18.1 mm. Every displacement number below is scaled against those 18 mm.
Real sonars send pulses and time the round trip, which yields range. Phones cannot do useful range gating up here; the bandwidth is missing and so is the transducer. That turns out to be fine. The chest sits somewhere between 0.3 and 1 m away, its absolute distance is irrelevant and only the change matters. A continuous tone gives up range entirely and buys phase precision in return.
Why phase works where amplitude cannot
Sound leaves the speaker, hits the chest at distance d and returns to a microphone sitting next to that speaker, so the acoustic path is close to 2d. The received tone lags the emitted one by φ = 2π·(2d)/λ = 4πd/λ. Differentiate and the static geometry drops out: Δφ = 4π·Δd/λ. Now insert the screenshot numbers. Δd = 118 µm against λ = 18.1 mm gives Δφ = 0.082 rad, which is 4.7°. A small angle, but a narrowband carrier with decent SNR supports phase estimates well below 1°. Our bench floor: a stationary foam target at 0.5 m, 10-minute captures on 3 phones, residual phase noise equivalent to about 15 µm RMS of displacement. The 118 µm breath sits roughly 8 times above that floor.
Amplitude has no comparable story. Echo strength falls off near 1/d for a target like a torso, so moving 118 µm along a 500 mm path changes the received amplitude by about 0.02%. The microphone's real noise floor, the room's ambient sound, automatic gain stages and plain thermal drift each bury that number; in our logs the amplitude channel wanders more in one minute of temperature drift than a full breath would move it. Phase survives because it rides a narrow carrier at a frequency where a bedroom is otherwise silent, and because 4.7° stays 4.7° whether the echo is strong or weak.
The Doppler shift you will never see in a spectrum
There is a tempting frequency-domain framing. A reflector moving at velocity v shifts the carrier by f_d = 2·v·f0/c. Breathing at 16 breaths/min is 0.267 Hz; model the chest as a sinusoid of amplitude 118 µm and its peak velocity is v = 2π·f_b·A ≈ 198 µm/s. Then f_d = 2 × 198 µm/s × 19,000 Hz / 343 m/s ≈ 0.022 Hz.
Resolving a line 0.022 Hz away from a carrier needs a window on the order of 45 s, and chest velocity reverses sign every 1.9 s, so no such line ever forms — the energy smears into a faint haze hugging the carrier. The spectrum-first habits from FFT for mortals, which serve Sonarish well on humming machinery, are useless on this signal. The rescue is that phase is the time integral of Doppler shift. Instead of hunting an offset you cannot resolve, accumulate the angle sample by sample, and the invisible 0.022 Hz becomes a visible 4.7° swing every breath.
I/Q demodulation, then patience
Extracting the angle is textbook quadrature demodulation. Multiply the mic signal by cos(2πf0t) and by −sin(2πf0t), low-pass both products, and the surviving pair (I, Q) forms a vector whose angle is the received phase. After the low-pass nothing above a few tens of hertz remains, so the pair is decimated from 48 kHz down to 100 Hz and the rest of the chain runs on almost no data.
One large obstacle stands in the way. The strongest 19 kHz energy at the microphone never touched a chest: it leaked straight from the speaker through a few cm of chassis, joined by static echoes off walls and the headboard. That clutter vector dwarfs the breathing echo, and atan2 on the raw pair would report the clutter's angle with a microscopic wobble on top. The fix is old radar practice. Subtract a slow running mean, about 30 s, from I and Q; every static path cancels, and the angle of what remains is chest phase directly.
// i, q arrive from the 19 kHz mixer + low-pass,
// decimated to 100 Hz, on the processing queue
iC = i - runningMean(i, 30s) // static clutter out
qC = q - runningMean(q, 30s)
phi = atan2(qC, iC) // wrapped phase
phi = unwrap(phi) // stitch ±π jumps
d = phi * lambda / (4 * PI) // metres of motion
b = bandpass(d, 0.1, 0.5) // 6–30 breaths/min
rate = dominantPeriod(b, 60s) // breaths per minute
The band-pass at 0.1–0.5 Hz does quiet heavy lifting. It spans 6 to 30 breaths/min, which covers sleeping adults and children with margin, and it discards the two things that otherwise dominate: leftover DC below the band from imperfect clutter removal, and heartbeat plus hand tremor above it. The capture callback itself only copies samples into a ring buffer and returns, following the same no-malloc, no-locks discipline as everything else on our audio thread.
A number that says whether to trust the number
A breathing rate with no context is a dangerous display. The quality metric behind the 92% on the screenshot combines two measurements over the last 30 s: the fraction of displacement-signal energy between 0.05 and 2 Hz that falls inside the 0.1–0.5 Hz breathing band, and a periodicity score taken from the autocorrelation of the band-passed signal. Both run 0 to 1; their product, scaled to a percentage, is what you see. A supine sleeper 40 cm from the phone typically scores high 80s to mid 90s. Below 50% the app blanks the rate rather than print a number it would later have to apologize for.
Bench note for honesty: 14 nights, 2 phones on a nightstand roughly 40 cm from the chest, quality logged once per second. The metric stayed above 80% for 71% of total sleep time. The low stretches clustered immediately after position changes and recovered in under a minute.
Failure modes, catalogued
Each of these appears in our test logs, which is much of why the feature ships with a label on it.
- Gross motion. Rolling over moves the torso centimetres at centimetres per second. Phase winds through thousands of degrees each second and unwrapping produces confident garbage. Sonarish thresholds on phase rate, blanks the output and re-acquires once the phase settles.
- Bedding. The echo usually returns from the duvet surface rather than skin, which mostly works because the duvet rides the chest. Thick fill is different: in bench comparisons a winter duvet cost about 8 dB of echo versus a thin shirt, and 8 dB of SNR is the distance between 92% quality and the blanking threshold.
- Multipath and second movers. A ceiling fan, a second sleeper or a dog at the foot of the bed each contribute a moving echo, and the I/Q vector sums them all. Two comparable breathers produce a rate estimate that wanders between the two true rates. The periodicity term catches most of this and pulls the score down.
- Speaker roll-off. We swept 17–21 kHz on 6 phones with a calibrated measurement mic at 30 cm. At 19 kHz the output spanned a 22 dB range between the loudest and quietest model, and one model produced essentially nothing above 19.5 kHz. Sonarish runs a 2 s sweep when the feature is first enabled and picks the carrier bin with the best round-trip SNR on that specific device.
Hearing, pets and the experimental label
Inaudible is a statistical claim about adult humans and nobody else. Age drags the upper hearing limit straight through the band this feature uses: teenagers routinely hear 18 kHz, and few people past 40 hear above 15 kHz. So the tone is kept quiet in absolute terms, around 60 dB SPL at 0.5 m on our bench units, well under the 70 dB SPL figure that published guidance cites for public near-ultrasound exposure at 20 kHz. One caution from the decibel post applies directly here: A-weighting is already about 9 dB down at 19 kHz, so a dBA meter structurally understates a tone a 19-year-old hears perfectly well. We evaluate the emission level unweighted.
Pets hear it, full stop. Dogs perceive up to roughly 45 kHz and cats to about 64 kHz, so a 19 kHz carrier is a plain audible whistle to both. The setup flow says so and suggests leaving the feature off when animals sleep in the room. The first field report on this feature was our QA lead describing her cat staring at the phone with an expression she declined to interpret.
The experimental label is not modesty theatre. Sonarish is not a medical device: it does not detect apnea, raises no alarms and carries no clinical validation, and a breathing monitor that fails silently in exactly the situations you care about would be worse than none. The label comes off when the failure modes above are either fixed or reliably self-reported by the quality metric across a far wider device list than 6 phones. Until then the app shows its number, shows its confidence in that number and blanks both the moment the data stops earning them.
Trang app Sonarish có một screenshot: 16 nhịp thở mỗi phút, chất lượng tín hiệu 92%, biên độ dịch chuyển 118 µm. Không dây đeo, không camera, không chạm vào người. Điện thoại nằm trên tủ đầu giường, phát một tông đa số người lớn không nghe được rồi lắng nghe phần dội lại. Vị trí lồng ngực rơi ra từ phép toán. Trang store chỉ có chỗ cho vài chục chữ. Bài này là bản đầy đủ.
Tóm tắt trước đã. Loa phát tông liên tục quanh 19 kHz. Tiếng dội từ lồng ngực người ngủ đi một quãng đường thay đổi theo nhịp hít thở. Quãng đường đổi ra phase. Phase trên carrier sạch đo được tới dưới một độ. Phần còn lại toàn là lọc.
Chọn tông loa phát được mà tai không nghe được
Tần số carrier bị ép từ ba phía. Sonarish thu ở 48 kHz như mọi đường audio khác của uranashel, nên Nyquist chặn thế giới ở 24 kHz. Thính giác người lớn sập rất nhanh trên 16 kHz; tầm 40 tuổi thường chỉ còn nghe tới quanh 15 kHz, mà cũng chẳng ai muốn nghe sonar hát ru mỗi đêm. Loa điện thoại sinh ra cho giọng nói và nhạc chuông, roll off mạnh sau 15 kHz nhưng vẫn còn chút công suất cao hơn nữa. Giao của ba ràng buộc là dải 18–20 kHz.
Bước sóng quyết định phép đo thấy được gì. λ = c/f với c = 343 m/s ở 20 °C: tông 18 kHz có λ = 19,1 mm, tông 20 kHz có λ = 17,2 mm. Mặc định của Sonarish là 19 kHz, tức λ = 18,1 mm. Mọi con số dịch chuyển phía dưới đều so với 18 mm này.
Sonar thật phát xung rồi bấm giờ tiếng dội, từ đó ra khoảng cách. Điện thoại không làm nổi range gating ở dải này: thiếu băng thông, thiếu cả transducer. Hóa ra lại ổn. Lồng ngực nằm đâu đó 0,3–1 m, khoảng cách tuyệt đối không quan trọng, chỉ phần thay đổi mới đáng đo. Tông liên tục bỏ hẳn khoảng cách để đổi lấy độ chính xác phase.
Vì sao phase làm được còn biên độ thì chịu
Âm rời loa, chạm ngực ở khoảng cách d rồi quay về mic nằm ngay cạnh loa, nên đường đi xấp xỉ 2d. Tông thu về trễ phase so với tông phát: φ = 2π·(2d)/λ = 4πd/λ. Lấy vi phân, phần hình học cố định biến mất: Δφ = 4π·Δd/λ. Thay số từ screenshot: Δd = 118 µm trên λ = 18,1 mm cho Δφ = 0,082 rad, tức 4,7°. Góc nhỏ. Nhưng carrier băng hẹp với SNR tử tế cho phép ước lượng phase dưới 1°. Số bench của team: bia mút đứng yên ở 0,5 m, thu 10 phút trên 3 máy, nhiễu phase còn lại tương đương khoảng 15 µm RMS dịch chuyển. Nhịp thở 118 µm cao hơn cái sàn đó chừng 8 lần.
Biên độ không có câu chuyện tương tự. Tiếng dội yếu dần cỡ 1/d với vật phản xạ như thân người, nên dịch 118 µm trên đường 500 mm chỉ đổi biên độ khoảng 0,02%. Sàn nhiễu thật của mic, tiếng ồn phòng, tầng gain tự động rồi trôi nhiệt đè lên con số đó vài bậc độ lớn. Trong log của team, kênh biên độ trôi vì nhiệt trong một phút còn nhiều hơn một nhịp thở trọn vẹn. Phase sống sót vì ngồi trên carrier băng hẹp ở tần số mà phòng ngủ hoàn toàn im lặng. Thêm nữa 4,7° vẫn là 4,7° dù tiếng dội mạnh hay yếu.
Dịch Doppler không bao giờ hiện trên phổ
Có một cách nhìn tần số nghe rất xuôi tai. Vật phản xạ chuyển động vận tốc v dịch carrier đi f_d = 2·v·f0/c. Thở 16 nhịp/phút là 0,267 Hz; coi lồng ngực dao động sin biên độ 118 µm thì vận tốc đỉnh v = 2π·f_b·A ≈ 198 µm/s. Ra f_d = 2 × 198 µm/s × 19.000 Hz / 343 m/s ≈ 0,022 Hz.
Muốn phân giải một vạch cách carrier 0,022 Hz cần cửa sổ cỡ 45 giây. Mà vận tốc ngực đảo chiều mỗi 1,9 giây, nên vạch đó không bao giờ hình thành — năng lượng nhòe thành quầng mờ ôm sát carrier. Thói quen nhìn phổ từ FFT cho người bình thường giúp Sonarish rất tốt với máy móc kêu ù, gặp tín hiệu này thì vô dụng. Lối thoát nằm ở chỗ phase là tích phân theo thời gian của dịch Doppler. Thay vì săn một offset không phân giải nổi, cứ cộng dồn góc từng sample. Con số 0,022 Hz vô hình thành cú lắc 4,7° mỗi nhịp thở, nhìn được bằng mắt thường.
Giải điều chế I/Q rồi kiên nhẫn
Tách góc là bài quadrature demodulation trong giáo trình. Nhân tín hiệu mic với cos(2πf0t) và −sin(2πf0t), low-pass cả hai tích, cặp (I, Q) còn lại là một vector mà góc chính là phase thu về. Sau low-pass chẳng còn gì trên vài chục hertz, nên decimate từ 48 kHz xuống 100 Hz và phần sau của chuỗi xử lý chạy gần như miễn phí.
Có một chướng ngại to đứng chắn. Năng lượng 19 kHz mạnh nhất tại mic chưa từng chạm vào ngực ai: nó rò thẳng từ loa qua vài cm khung máy, cộng thêm tiếng dội tĩnh từ tường và đầu giường. Vector clutter đó đè bẹp tiếng dội nhịp thở; atan2 trên cặp thô sẽ báo góc của clutter kèm gợn li ti bên trên. Cách chữa là bài cũ của dân radar. Trừ trung bình chạy chậm, cỡ 30 giây, khỏi I và Q; mọi đường tĩnh tự triệt tiêu, góc của phần còn lại chính là phase lồng ngực.
// i, q ra từ mixer 19 kHz + low-pass,
// decimate còn 100 Hz, chạy trên queue xử lý
iC = i - runningMean(i, 30s) // bỏ clutter tĩnh
qC = q - runningMean(q, 30s)
phi = atan2(qC, iC) // phase còn gói
phi = unwrap(phi) // vá bước nhảy ±π
d = phi * lambda / (4 * PI) // mét chuyển động
b = bandpass(d, 0.1, 0.5) // 6–30 nhịp/phút
rate = dominantPeriod(b, 60s) // nhịp thở mỗi phút
Band-pass 0,1–0,5 Hz gánh phần việc thầm lặng. Dải này là 6–30 nhịp/phút, phủ người lớn lẫn trẻ nhỏ đang ngủ có dư. Nó cũng vứt đi hai thứ hay chiếm sóng nhất: DC sót dưới dải do khử clutter chưa hoàn hảo, cùng nhịp tim với run tay phía trên dải. Bản thân callback thu âm chỉ chép sample vào ring buffer rồi trả về — đúng kỷ luật không malloc, không lock như mọi thứ trên audio thread của team.
Một con số cho biết có nên tin con số kia
Nhịp thở đứng một mình là màn hiển thị nguy hiểm. Con số 92% trên screenshot ghép từ hai phép đo trong 30 giây gần nhất: tỉ lệ năng lượng tín hiệu dịch chuyển trong khoảng 0,05–2 Hz rơi đúng vào dải thở 0,1–0,5 Hz cùng điểm tuần hoàn lấy từ autocorrelation của tín hiệu đã band-pass. Cả hai chạy từ 0 tới 1, nhân với nhau rồi quy ra phần trăm. Người ngủ ngửa yên cách máy 40 cm thường được cuối 8x tới giữa 9x. Dưới 50%, app để trống thay vì in một con số mà sau này phải xin lỗi.
Số bench cho minh bạch: 14 đêm, 2 máy đặt trên tủ đầu giường cách ngực chừng 40 cm, log chất lượng mỗi giây. Chỉ số giữ trên 80% suốt 71% thời gian ngủ. Các quãng thấp dồn ngay sau lúc trở mình rồi hồi lại trong chưa đầy một phút.
Danh mục các kiểu hỏng
Kiểu nào dưới đây cũng có mặt trong log thử nghiệm. Đó là phần lớn lý do tính năng phải mang nhãn.
- Cử động lớn. Trở mình dịch thân người vài cm với tốc độ vài cm/s. Phase quay hàng nghìn độ mỗi giây, unwrap nhả ra rác rất tự tin. Sonarish đặt ngưỡng trên tốc độ phase, tắt hiển thị rồi bắt lại tín hiệu khi phase lắng xuống.
- Chăn đệm. Tiếng dội thường quay về từ mặt chăn thay vì da, đa phần vẫn ổn vì mặt chăn nhấp nhô theo ngực. Chăn dày là chuyện khác: bench cho thấy chăn mùa đông ăn mất khoảng 8 dB tiếng dội so với áo mỏng, mà 8 dB SNR là khoảng cách giữa mức 92% với ngưỡng bị để trống.
- Đa đường và vật chuyển động thứ hai. Quạt trần, người ngủ cạnh hay chó nằm cuối giường đều góp tiếng dội riêng, vector I/Q cộng tất cả lại. Hai người thở ngang nhau cho ước lượng nhịp lang thang giữa hai nhịp thật. Thành phần tuần hoàn của chỉ số chất lượng bắt được phần lớn và kéo điểm xuống.
- Loa roll off. Team sweep 17–21 kHz trên 6 máy với mic đo đã hiệu chuẩn đặt cách 30 cm. Ở 19 kHz, công suất chênh nhau 22 dB giữa máy to nhất và máy nhỏ nhất; một máy gần như câm hẳn trên 19,5 kHz. Sonarish chạy sweep 2 giây lần đầu bật tính năng rồi chọn bin carrier có SNR khứ hồi tốt nhất cho đúng máy đó.
Tai người, tai thú và cái nhãn thử nghiệm
"Không nghe được" là phát biểu thống kê về người lớn, không hơn. Tuổi tác kéo trần thính giác xuyên đúng dải tính năng này dùng: thiếu niên nghe 18 kHz bình thường, ngoài 40 hiếm ai còn nghe trên 15 kHz. Nên tông được giữ nhỏ theo mức tuyệt đối, quanh 60 dB SPL ở 0,5 m trên máy bench, thấp hơn hẳn mốc 70 dB SPL mà các hướng dẫn công bố nêu cho phơi nhiễm cận siêu âm nơi công cộng ở 20 kHz. Một lưu ý từ bài decibel áp thẳng vào đây: A-weighting đã hạ khoảng 9 dB ở 19 kHz, máy đo dBA vì cấu trúc sẽ báo thấp một tông mà người 19 tuổi nghe rõ mồn một. Team đánh giá mức phát không qua weighting.
Thú cưng thì nghe hết, chấm hết. Chó nghe tới khoảng 45 kHz, mèo tới khoảng 64 kHz; carrier 19 kHz với chúng là tiếng huýt rõ ràng. Màn setup ghi rõ điều này và khuyên tắt tính năng khi thú ngủ cùng phòng. Báo cáo hiện trường đầu tiên về tính năng là mèo của bạn QA nhìn chằm chằm cái điện thoại với biểu cảm mà bạn ấy từ chối diễn giải.
Nhãn thử nghiệm không phải màn khiêm tốn. Sonarish không phải thiết bị y tế: không phát hiện ngưng thở, không báo động, không có kiểm chứng lâm sàng. Máy theo dõi nhịp thở mà hỏng im lặng đúng lúc quan trọng nhất còn tệ hơn không có máy. Nhãn chỉ được gỡ khi các kiểu hỏng bên trên hoặc được sửa hoặc được chỉ số chất lượng tự khai báo đáng tin trên danh sách máy rộng hơn nhiều con số 6. Trước lúc đó, app hiện con số, hiện độ tin của con số và để trống cả hai ngay khi dữ liệu không còn xứng đáng.