# An inaudible sonar that watches you breathe > Sonarish's experimental breathing monitor drives the speaker at 19 kHz, listens on the mic, and reads 118 µm of chest motion out of the phase of the echo. 2026-08-18 · 20 min read · sonarish, dsp, physics · by ntan (ntan) for uranashel Canonical HTML: https://uranashel.com/blog/sonar-breathing-doppler.html --- The [Sonarish store page](https://uranashel.com/apps/sonarish.html) 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](https://uranashel.com/blog/fft-made-readable.html), 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](https://uranashel.com/blog/dsp-audio-thread.html). ## 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](https://uranashel.com/blog/a-weighting-decibels.html) 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. --- uranashel · [Home](https://uranashel.com/) · [Apps](https://uranashel.com/apps.html) · [Lab](https://uranashel.com/lab.html) · [About](https://uranashel.com/about.html) · [Blog](https://uranashel.com/blog/) · [Developers](https://uranashel.com/developers/) · [API docs](https://uranashel.com/docs/) · [Privacy](https://uranashel.com/privacy.html) Machine-readable: [llms.txt](https://uranashel.com/llms.txt) · [sitemap.xml](https://uranashel.com/sitemap.xml) · [openapi.json](https://uranashel.com/openapi.json) · [API](https://uranashel.com/api/v1/)