Inspiration

After the April 2024 Hualien earthquake, 172 mobile base stations went offline, most of them in Hualien and Yilan (National Communications Commission, via CNA, 2024-04-03); most were back the same day. Where the towers are down, two people standing a metre apart cannot send each other a text. The oldest trick in networking, two machines whistling at each other over a phone line, needs no network at all. Handshake brings that dead technology back for that gap.

What it does

  • Turns up to 140 bytes of text (about 46 Chinese characters) into sound and back. Two tones, 1800 and 2400 Hz, 100 baud.
  • Works in two modes: real sound (one device plays, the other listens through its microphone) and two browser tabs on one laptop, so a judge can see it without two phones.
  • The interface is a séance board. The ring of the planchette lens is the measured tone clarity: sharp and bright when the tones are clean, smeared when noise wins. A failed decode slides to GOOD BYE and shows the real reason (no sync, bad length, checksum failed). It never shows a guess.
  • Works offline after the first load. No account, no server.

How we built it

  • Kiro steering set three rules every spec obeys: layers that test without each other, a property test for every encode/decode pair, and "never claim it works when the receiver could not decode".
  • Three Kiro specs, each with EARS requirements, a design and tasks: the modem and protocol, then real audio and the two-tab demo, then error correction after the first real air test failed.
  • Spec review caught real bugs before code existed. In slice 3 it caught a design that compared robust and normal modes at four times the air time, and a receiver that could not know a frame's length before decoding it. Kiro's first design could not find a frame that started mid-symbol, so every real recording would have failed while the noise test passed vacuously. Its second design framed messages twice. Both fixes are in the spec files.
  • Kiro hooks re-run the property tests when the modem changes and keep the README's measured table tied to the test output.
  • Who wrote what. Kiro generated all three specs (from steering written by hand and with a coding assistant) and wrote the slice-1 code (modem, framing, receiver, simulated channel and their tests). Slice 2's code (Web Audio, two-tab demo, séance screen) was written with another coding assistant, task by task from the tasks.md Kiro generated, after the free tier's 50 credits ran out. Slice 3's spec was generated by Kiro on the last free credits; its code was written the same way as slice 2.
  • TypeScript, Vite, Web Audio AudioWorklet, BroadcastChannel, Vitest and fast-check. No runtime dependencies.

Challenges

  • Finding where a message starts in a stream of audio that never lines up with a symbol: the receiver tries 8 timing offsets and searches the sync pattern bit by bit.
  • Making "spirit clarity" honest: it is the share of received energy that sits on the two modem tones, measured per symbol, not an animation.
  • Offline: the service worker first listed a file Vite had deleted (so it never installed), then missed cached scripts because of a Vary: Origin header. Both were caught by stopping the server and reloading.

Accomplishments

  • 107 automated tests, most of them fast-check properties. The core one: under any noise, the decoder returns the exact message or a failure, never different data.
  • Measured on the simulated channel: 50 of 50 messages decode down to −10 dB SNR, 8 of 50 at −15 dB, 0 of 50 at −20 dB.
  • Verified in a real browser through the full Web Audio path (worklet out → worklet in) at both 48 kHz and 44.1 kHz.
  • First real over-the-air test on one desktop failed (0 of 10 at 100 baud): bits arrived with 2–9% errors because the speaker was too quiet at the mic. The checksum rejected every damaged frame. That failure drove slice 3.
  • Over the air, working: after raising the speaker volume, the same desktop setup decoded 5 of 5 messages in the new "Robust, slow" mode (error correction at 50 baud) through the real Send and Listen screens, each on screen within a second of its sound ending, with no wrong message accepted. Fast mode got 0 of 5 in the same room. Phones are not measured yet.
  • Slice 3, error correction: a rate-1/2, K=7 convolutional code with soft-decision Viterbi and an interleaver, plus a sync search that tolerates 2 wrong bits. On the simulated channel, at the same seconds of sound, it beats simply slowing down by about 3 dB (−18 dB: 62/100 vs 11/100). Listeners detect the mode on their own.

What's next

  • Phone tests across devices and rooms, logged in the field page (one desktop and one room so far).
  • GPS and contact-card payloads.

Honest limits

  • 100 baud: a full message takes about 12 seconds of sound.
  • CRC-16 lets a random corrupted frame through about once in 65,536.
  • Browsers may keep echo cancellation or noise suppression on; the listen screen reports what was honored.
  • Prior work exists (ggwave and others).

Built With

  • fast-check
  • kiro
  • typescript
  • vercel
  • vite
  • vitest
  • web-audio-api
Share this project:

Updates

Submission history