Breathing Sonar
Your chest moves a few millimetres when you breathe. That is enough to be seen from across the desk with nothing but a laptop speaker and its microphone: emit an inaudible tone above 18 kHz, listen to what comes back, and watch the echo breathe. Two methods, side by side — a single-tone phase tracker, and the FMCW range-bin pipeline from Nandakumar, Gollakota & Watson, ApneaApp (MobiSys 2015).
The idea in one minute
Sound leaves the speaker, bounces off your torso, and returns to the microphone. The round trip takes 2d/c seconds. Inhale and d shrinks by a few millimetres; exhale and it grows back. Every method here is a different way of noticing that few-millimetre change against a room full of much stronger, perfectly stationary echoes.
Method 1 — CW phase. Hold one tone. At 18 kHz a wavelength is 19 mm, so a 1 mm chest displacement changes the round-trip path by 2 mm and rotates the returning echo by 4π·1 mm/λ = 38°. You cannot measure absolute distance this way — but you can measure change with absurd sensitivity, because phase is a ruler with 19 mm markings.
Method 2 — FMCW range cells (ApneaApp). Sweep 18→20 kHz over and over. Mixing the echo with the sweep turns delay into a beat frequency, so one FFT sorts the room into range cells 8.6 cm deep. Breathing shows up as the energy in a cell rising and falling. Less sensitive per millimetre than holding a single tone — but it tells you where the sleeper is, and can follow two of them at different distances at once.
Both end the same way: take the slow, wobbling signal you extracted, run a 30-second FFT, and look for a peak between 0.12 and 0.7 Hz — 7 to 42 breaths a minute. That peak, if it is sharp enough to stand clear of the rest of the band, is a breathing rate.
Method
A single inaudible tone. The complex baseband echo is one vector; the room pins it in place and your chest rotates it. Most sensitive, but blind to distance.
Parameters · CW phase
Carrier frequency, 1–22 kHz. Higher → shorter wavelength → more phase per millimetre of chest movement (38°/mm at 18 kHz, 2°/mm at 1 kHz), but laptop tweeters and microphones both roll off hard above 20 kHz. If the echo looks dead in the signal panel, come down — and keep coming down. Below about 17 kHz you will hear it, which is unpleasant but diagnostic: an audible tone proves the speaker is actually emitting, and the 8–14 kHz region is where cheap hardware is happiest. Below ~2 kHz the wavelength is longer than a room feature and clutter cancellation has little left to separate.
Time constant of the running average that estimates the static part of the echo — walls, desk, the direct speaker-to-mic leak. Subtracting it re-centres the constellation on the origin so the breathing arc is what is left. Too short and it starts eating slow breaths.
Low-pass on the complex echo before the phase is taken. Breathing lives below 1 Hz, so heavy smoothing costs nothing and keeps the phase from random-walking when the echo is weak.
Run
Echo cancellation, noise suppression, and AGC are all switched off so the microphone reports the sonar honestly. Headphones will not work — the sound has to reach your chest through the air. Hold reasonably still: the same physics that sees a 1 mm breath sees a 1 cm fidget far more loudly. The Signal check panel below shows the raw microphone waveform and spectrum — start there if the results look like nonsense.
Method 1 — how phase becomes millimetres
Transmit. One sine at f0, looped from an audio buffer holding a whole number of cycles so the loop point is silent. The buffer length rounds the frequency slightly; the receiver uses the rounded value, so transmitter and receiver share one clock and one frequency exactly.
Receive and demodulate. Multiply each microphone sample by e−j2πf0t and average over 512 samples. That is a quadrature demodulator followed by a very blunt low-pass: the 18 kHz carrier collapses to DC, everything else averages away, and what drops out at ~94 Hz is one complex number per block:
z(t) = Σk Ak e−j·4πdk(t)/λ
One term per echo path. The wall, the desk, and the speaker-to-mic leak have fixed dk, so they sum to a large constant vector. Your chest is a small term whose angle sweeps as you breathe.
Cancel the room. Subtract a running average of z with time constant τ. The constant vector vanishes; what remains is a small arc centred on the origin. That is the whole clutter-cancellation step, and it is why the constellation plot above starts as a blob and opens into an arc after a few seconds.
Read the angle. Unwrap ∠z across time and convert:
Δd = λ·Δφ / 4π (λ = c/f0 ≈ 19 mm at 18 kHz)
The 4π rather than 2π is the round trip — the path changes by twice the chest displacement. Note what this measurement is not: absolute distance is unrecoverable, since every added half-wavelength returns the same phase. And it is not selective — every mover in the room lands in the same single number.
Method 2 — ApneaApp’s pipeline, step by step
1. Sweep. Transmit a linear chirp from f0 to f1 over Tsweep, on repeat. An echo delayed by Δt is a copy of the sweep shifted in time, which — because frequency rises linearly — means shifted in frequency:
Δf = (f1 − f0) / Tsweep · Δt, Δt = 2d/c
2. Dechirp. Multiply the received signal by the conjugate of the sweep the transmitter is playing at that instant. Each echo collapses from a 2 kHz-wide chirp to a steady tone at its own Δf. Delay has become frequency, so one FFT is a range profile.
3. Transform over M sweeps, not one. This is the paper’s move. A 512-sample FFT at 48 kHz has 93.75 Hz bins; transform over ten consecutive sweeps instead and the same 107 ms of audio yields 9.375 Hz bins, short enough a frame that nothing meaningful moves during it.
It is worth being exact about what those extra bins hold, because the answer is visible in this page’s output. The sweep repeats every N samples, so a stationary echo dechirps to a signal that is periodic with the sweep — and an M-sweep FFT of a period-N signal has energy only on bins that are multiples of M. The spectrum comes out as a comb: one line per physical range cell, spaced c/2B = 8.6 cm, exactly the resolution the bandwidth allows. The M−1 bins between two comb lines are not extra range — they are the Doppler bins of that cell, and only motion pushes energy into them. Breathing is far too slow to reach them (a chest moving 6 mm/s Doppler-shifts a 19 kHz echo by under 1 Hz, a fifteenth of one fine bin). So this page reads the comb lines and treats them as what they are: range cells 8.6 cm apart, refreshed once per frame, with M sweeps of coherent gain behind each one.
4. Find the person. Take a cell’s magnitude as a function of frame number — a signal sampled at the frame rate, ~12 Hz here — run a 30-second FFT on it, and score any peak in the breathing band. ApneaApp scans outward from the phone and stops at the first cell that shows one; keep scanning and a second peak, well clear of the first, is a second sleeper. The scan repeats whenever the subject shifts, which is why the system never has to be told how far away the bed is.
4b. Read the rate from phase, not amplitude. One departure from the paper. Amplitude works for finding the cell, but it folds: the chest sweeps through 4π·6 mm/λ ≈ 240° of phase per breath, so the magnitude of that cell rises and falls twice per breath and a naive spectral peak reports double the true rate. The complex bin value does not fold. Cancel its mean, unwrap its angle, and you get the same millimetre trace method 1 produces — but attached to a range. That is the trace plotted above; the amplitude test still drives the event monitor, as in the paper.
5. Score events. Track the amplitude of the chest-motion signal. ApneaApp calls a hypopnea when it drops below 30% of baseline while staying periodic, and a central apnea when it goes to nothing. In a 296-hour clinical study across 37 patients at the UW Medicine Sleep Center, event counts correlated with polysomnography at 0.9957 (central), 0.9860 (obstructive) and 0.9533 (hypopnea), with an average error of 1.9 events per hour.
Things to try
- Start with CW, sit still, and watch the constellation. For the first few seconds it is a tight knot — the clutter average has not converged. Then the knot cancels and an arc opens up, one sweep out and back per breath.
- Hold your breath. The arc collapses to a point, the spectrum flattens, and the envelope strip crosses its threshold within about ten seconds.
- Breathe deliberately fast, then slow. The spectral peak walks across the axis. Deep breaths widen the arc — and if you exceed half a wavelength of chest travel the phase wraps, which the unwrapper handles but the constellation shows as a full loop.
- Switch to FMCW and move your chair 20 cm back. The bright ripple in the waterfall and the peak in the energy-vs-range plot both follow you — two cells over. That is the coordinate CW cannot see. Ranges are measured from the direct speaker-to-microphone arrival, so treat cell 0 as the laptop, not as your nose.
- Get a second person to sit a metre behind you. Two peaks in the energy profile, two independent breathing rates — click either to lock it.
- Drop the FMCW bandwidth to 500 Hz. Range resolution falls to 34 cm, the peaks smear together, and two people become one. Bandwidth is resolution, always.
- Set chirps-per-frame to 4. Range cells stay 8.6 cm apart — bandwidth fixed that — but each one now integrates a quarter as much sound, so the trace gets noisier while updating four times as often.
- Cover the laptop microphone with a finger. Both methods should fall to noise — a useful reminder of how much of this is real echo and how much is wishful thinking.
What this isn’t
This page is the sensing front end only, running on whatever single speaker and single microphone the browser hands it. ApneaApp ran on a phone placed beside a bed for eight hours a night, with a sleep-staging stage, a peak detector tuned against scored polysomnography, and an event classifier that separates obstructive from central apnea by looking for the effortful chest motion that continues during an obstruction. None of that is here.