Skip to main content

How to send data between phones using sound: 600 ms frames

How to send data between phones using sound with 16-ary FSK at 50 ms per symbol. A ball is a 600 ms frame, and CRC-8 drops a bad one.

• • 10 min read
Dashboard cover of a two-phone acoustic link: 50 ms symbols, 18 tones per band, and no network

TL;DR You can learn how to send data between phones using sound with a modem that fits in one static page: 16-ary FSK, 4 bits every 50 ms, two frequency bands, no Wi-Fi. A browser acoustic modem is that page. It turns a short binary frame into sine tones and reads the tones back with the microphone. A Pong ball is one 600 ms frame, and a bad CRC-8 means the point never arrives.

Simon Willison shipped this on 15 October 2026 as Chirp Pong, a two-phone game whose only network is the air between the speakers. I traced ChirpCore in the published chirp-pong.html. The numbers below are that file’s constants, not a lab I ran across a room.

What is a browser acoustic modem?

A browser acoustic modem uses the Web Audio API as a radio. One OscillatorNode retunes itself on each symbol boundary. An AudioWorklet feeds the microphone into a detector. Nothing leaves the two devices.

The interesting constraint is throughput. Each data symbol carries 4 bits in 50 ms. That is 80 bits per second while data is actually on the air. A chat message would be painful. A game event is fine, if you send the event once.

This is the same bet as doing the heavy work inside the tab instead of standing up a server for a job two phones can finish on a table. The page is static HTML. There is no account and no signaling host to fall over.

How to send data between phones using sound

The procedure is the one the page already runs. You are not inventing a codec. You are following the one in ChirpCore.

  1. Open the page on both phones. Tap play on two phones. The modem does not start a socket.

  2. Turn the volume all the way up and switch off silent mode. Lay the phones top to top, a few centimetres apart, the way the on-page instructions say.

  3. Tap Call on one phone. The caller repeats a HELLO frame on the low band: an 8-bit nonce and a 4-bit target score (7, 11, or 15). That frame is 400 ms of audio. It waits the airtime plus 1.0 to 1.9 seconds, then calls again.

  4. The other phone answers WELCOME on the high band, 150 ms after it hears a HELLO whose nonce wins. If both phones call, the lower nonce gives way. The caller becomes side 0. The answerer becomes side 1.

  5. Play. When the ball leaves the top of your court, send one BALL frame: x, angle, speed, spin, trick, rally count, and a smash bit. The receiver peeks the x field before the frame finishes so the ball can start appearing in the right place.

  6. Treat BALL, MISS, BOMB, REMATCH, and TAUNT as reliable. The receiver sends a short ACK. If the ACK does not arrive, the sender chirps the same sequence bit again after the airtime plus 0.95 to 1.45 seconds. A duplicate sequence bit is dropped.

The ball’s airtime falls out of the frame layout. Type and sequence take 4 bits, the fields take 26, and CRC-8 takes 8.

JavaScript
const T_SYM = 0.05;
const PRE_SYMS = 2; // 100 ms sync tone
const fieldBits = 6 + 5 + 3 + 3 + 3 + 5 + 1; // 26
const bits = 4 + fieldBits + 8; // 38
const symbols = Math.ceil(bits / 4); // 10
const airtime = (PRE_SYMS + symbols) * T_SYM; // 0.6 seconds

Angle is not a float on the air. It is 5 bits spanning ±70 degrees. x is 6 bits across the court width. Spin is stored as spin + 3 so a value from −3 to 3 fits in 3 bits. Rally is capped at 31 because the field is 5 bits. Speed is snapped to the nearest entry in a small table, not sent as a raw velocity.

Symbols are coded differentially. The next tone index is (previous + 1 + value) mod 17, so two symbols in a row never share a tone. The receiver refuses to score the previous tone as the next symbol. That is how it finds edges without a shared clock, and how a reverb tail does not become a false bit.

Differential FSK never repeats the previous tone, so the receiver places 50 ms symbols without a shared clock, then CRC-8 tries alignments within plus or minus 15 ms of the sync edge

The detector is the Goertzel recurrence: one coefficient, 2 * cos(2πf / sampleRate), per tone. The analysis window is 1 / 46.875 seconds, about 21 ms, hopped every quarter window. A tone counts only if it clears an amplitude of 0.0004, beats the median bin by 5×, and beats the runner-up by 1.25×.

Sync has to win 7 of the last 10 hops, about 50 ms of agreement. After the sync tone falls, CRC-8 tries alignments from −15 ms to +15 ms in 2.5 ms steps and keeps the first of the clearest five that passes. The polynomial is 0x07 with the register starting at 0x5A.

That is a lot of mechanism for 30 content bits. It is also the whole product. A few hundred bytes of precise behavior beat a framework you would spend the weekend configuring.

Why one ball frame takes 600 milliseconds

Thirty content bits and eight CRC bits do not divide cleanly by four. Thirty-eight bits become ten symbols. Ten symbols at 50 ms is 500 ms, and the 100 ms sync tone brings the frame to 600 ms. An ACK is shorter: 15 bits, four symbols, 300 ms including sync. HELLO and WELCOME are 400 ms.

A ball frame is 600 milliseconds of audio, split into a 100 millisecond sync tone and 500 milliseconds of ten data symbols at 4 bits each, with CRC-8 in the frame

The game is built around that budget. Physics runs locally at full frame rate. The air only hears the ball when it crosses the top edge, which the code treats as a portal. Heat is local too, then sent as an integer: rallyValue is 1 point, 2 points once the rally hits 6, and 3 points once it hits 12. A miss sends both scores as absolute numbers, not a delta, so a lost frame cannot desync the scoreboard forever. The next good MISS repairs it.

PathNetworkWhat one message isTime you should budget
Chirp FSK, this pageNone. Speaker and mic, a few centimetres.One frame: hello, ball, miss, bomb, or ack.300 ms for an ack, 600 ms for a ball, then a retry about 1.6–2.1 s after the send starts.
WebRTC data channelA signaling server, then a peer connection.A stream, if you want one.Updates every animation frame once the path is up.
On-screen QRNone. Camera and line of sight.One code, one shot.A single camera frame, then you are done.
Web BluetoothNo internet. Pairing and a radio.GATT writes.Whatever the OS pairing dialog costs before the first byte.

I would not pick the chirp column for anything that must feel instantaneous. I would pick it when the product is “these two phones are on the same table” and a half-second whoosh is the point. The portal works because you hear the ball leave. A socket would be faster and would feel like less.

Shipping that as a running page, rather than a diagram in a slide, is the same move as treating a research spike as code you can open. The 15 October note is four sentences. The HTML file is the spec.

What breaks if both phones share one frequency band?

Both directions die. Each player owns 18 tones: 17 data tones plus a sync tone at the top of the band. Band 0 sits on bins 30, 32, … 64 of a 46.875 Hz grid, which the file comments as 1,406–3,000 Hz. Band 1 sits on bins 72 through 106, commented as 3,375–4,969 Hz. The grid step is 48000 / 1024. Tones are every other bin so neighbors do not smear into each other.

The low band from 1406 to 3000 hertz and the high band from 3375 to 4969 hertz sit apart, and a low-band chirp mutes both bands until it ends because its harmonics land in the high band

While you transmit, you cannot hear the other phone. The demodulator marks your own band deaf until the chirp, including speaker and mic latency and room echo, has died away. A low-band transmission also mutes the high band until 30 ms after the frame ends, because the low band’s harmonics land in the high band. The comment in the source is blunt: the other phone never answers until your whole frame has reached it. A design that expects full-duplex overlap on these two bands will drop the answer.

Sound effects are kept out of both bands on purpose. They pass through a low-pass at 1,100 Hz. A hit, a wall, a smash: all of that stays below the modem. Add a UI click up at 2 kHz and you have built a jammer.

The other failure is two callers with no rule. The lobby already has one. A phone that is itself calling ignores a HELLO whose nonce is less than or equal to its own. The higher nonce becomes the caller. Reload mid-game and a fresh HELLO is treated as a new call, not as noise. Forget that rule and two phones both “answer” and neither plays.

If a surprising browser behavior bites you here, read the platform the way you would read why innerHTML refuses a declarative shadow root. The phone is not broken. The echo canceller, the band plan, and the deaf window are doing what they were written to do. The Web Audio surface is worth reading before you add a third tone “for flair.”

When does the speaker test lie?

The page can chirp and then listen for its own tone. Success means “this phone heard itself.” Failure often means the opposite of a broken mic. The UI says it outright: some phones filter out their own sound. Acoustic echo cancellation removes the local speaker from the capture so a call does not howl. Your loopback test is exactly the signal AEC is trained to delete.

The two-phone path does not need that loopback. The other phone’s microphone is not cancelling your speaker. Volume still matters. The detector’s floor is an amplitude of 0.0004, which is tiny on a normalized signal and still useless if the phone is on silent or the speakers are facing the ceiling from across a desk. The instructions are the range spec this file actually gives you: a few centimetres, top to top, volume up. I am not going to invent a metres-long figure the code does not claim.

Retries hide a lot until they don’t. A ball that needs a second chirp has already spent 600 ms plus roughly another second of waiting. The rally on the far phone is frozen on the last good snapshot that whole time. That is acceptable for a point. It is a bug if you were planning to stream paddle positions.

Keep the modem off the initial page load. Microphone permission is a tap, the same way you refuse to spend a download the visitor did not ask for. The pairing button is the right moment to call getUserMedia. A banner that asks for the mic so a footer animation can chirp is how this technique gets a bad reputation.

FAQ

How do two phones send data using sound without Wi-Fi? Each phone plays a short frame of sine tones and the other reads them with the microphone. The codec is 16-ary FSK, 4 bits per 50 ms symbol, with a 100 ms sync tone and a CRC-8 trailer. Nothing is paired over Bluetooth and nothing is sent to a server. The phones do have to be close, loud, and out of silent mode.

How long does one ball take to cross by sound? The ball frame itself is 600 ms of audio: 100 ms of sync and ten 50 ms symbols. If the ACK does not come back, the same sequence bit is sent again after the airtime plus 0.95 to 1.45 seconds. Count speaker and mic delay on top. This is a snapshot when the ball hits the portal, not a stream of positions.

Why do the phones use two frequency bands? The caller uses 1,406–3,000 Hz and the answerer uses 3,375–4,969 Hz, 18 tones each. One shared band would make every transmission step on the other phone’s symbols. A low-band chirp also deafens the high band while it plays, because its harmonics land there, so the protocol waits out the whole frame before the reply.

Why does the speaker test fail when the two-phone game still works? The test listens for the phone’s own speaker. Acoustic echo cancellation is built to remove that. The other phone is a different microphone, and it is not cancelling you. A failed self-hear check is a reason to try the call, not a reason to conclude the modem is dead.

Can you stream live game state this way? No. You get 80 bits per second during data symbols, and every frame pays 100 ms of sync first. Chirp Pong sends one ball, one miss, or one bomb, then waits for an ACK. A paddle that must move on the other screen every frame needs a network, not a speaker.

Sources

  • Simon Willison, “Chirp Pong” (15 October 2026). The note that the two phones talk by chirps, with no Wi-Fi.
  • Chirp Pong, the running page, including the on-screen setup instructions quoted above.
  • simonw/tools chirp-pong.html. ChirpCore, the lobby handshake, the band plan, and the retry timers traced in this post.
  • MDN, Web Audio API. The oscillator and worklet surface the modem is built on.

Frequently asked questions

Share this article:
X LinkedIn

Google Search · Preferred sources

Prefer this site on Google

If you already read this writing, add umesh-malik.com as a Preferred Source. Google can then highlight it with a preferred badge in Top Stories, AI Overviews, and AI Mode — for you, not as a site-wide ranking boost.

Keep reading

Get new posts on AI, Claude Code & LLMs

New deep-dives on AI engineering, Claude Code, and developer tooling — follow along however you prefer.