┌────────────────────────────────────────────────────────────┐
│ │
│ S O N I C L I N K · Big Chat 3.3.0 │
│ ───────────────────────────────── │
│ Data Over Audio · No Wi-Fi · No Bluetooth · No Net │
│ │
│ ▮▮▮ ▮▮▮▮▮ ▮▮▮ ▮▮▮▮▮▮ ▮▮▮ ▮▮▮▮ ▮▮▮▮▮ ▮▮▮ │
│ │
│ Universal pako · Reed-Solomon · Adaptive ACK │
│ Multiband 4-FSK · 200 B/s Turbo · 148 tests │
│ │
└────────────────────────────────────────────────────────────┘
Ship text and files through the air — literally. No Wi-Fi, no Bluetooth, no network. Just a speaker and a microphone.
A browser app that transfers arbitrary data between nearby devices using nothing but audible sound. One device encodes your message into a burst of carefully engineered tones; the other hears it, decodes it, and reconstructs the bytes exactly.
Built as a complete, self-contained protocol stack — FSK modem, multiband receiver, Reed-Solomon FEC, adaptive ACK back-channel, universal deflate compression, and a chat UI — all client-side, serverless, and binary-safe.
Current build: Big Chat
3.3.0(2026080413, protocol7) · Live demo
Most "sound transfer" projects beep a QR-code-like stream at a phone. This is a different animal: a real layered protocol over audio, with the kind of engineering you'd expect from a radio link —
- 10 ms symbols tuned so room reflections and multipath nulls average out instead of destroying a symbol. The original 5 ms modem failed on real phones; this one survived them.
- Multiband 4-FSK modulation — a Safe channel (4 bands, 1 B/symbol) and a Turbo channel (8 bands, 2 bytes/symbol = 200 B/s raw) sharing one 100 Hz frequency grid. The receiver auto-detects which mode the sender used, no handshake.
- Reed-Solomon FEC on every frame (16 parity bytes per 239-byte block, corrects up to 8 corrupted bytes) — a few bad bytes now get repaired on the fly instead of dropping the whole frame.
- Universal deflate compression with the bundled pure-JS
pakolibrary — noCompressionStream/DecompressionStreambrowser API dependency, so it works on every device, including Safari 16.3 and older WebViews. - Adaptive ACK back-channel — the sender's quiet pass gap doubles as a window for the receiver to reply
done(sender stops, half the airtime) ormissing(sender replays only the lost frames). A lost reply costs one pass; it can never stall a transfer. - A chat app over sound — sender profiles, bubbles, live ETA estimates, honest "this would take 10 minutes" warnings, duplicate-free idempotent reassembly.
Honest limits: this is not a Wi-Fi replacement. The acoustic channel is intentionally slow (a few hundred bytes/second at best). It's a research and learning project — and the right tool for the very specific job of moving a note from one device to another with nothing but sound.
The Turbo channel — used by every chat burst — packs 8 bands of 4-FSK into a 1000–4100 Hz slice of audible spectrum, on a 100 Hz grid. Each band carries 2 bits per symbol, 100 symbols per second. All 8 bands transmit in parallel inside one window.
1000 Hz ─┬─ T0 ░░▒▒▓▓████▓▓▒▒░░ ─┬─ 3400 Hz
1100 Hz ─┼─ T1 ░░▒▒▓▓████▓▓▒▒░░ ─┤
1200 Hz ─┤ │
1300 Hz ─┤ ░░▒▒▓▓████▓▓▒▒░░ │ 4-FSK tones
1400 Hz ─┤ T2 .. T3 .. T4 │ per band
1500 Hz ─┤ │ (one bit each)
1600 Hz ─┤ │
1700 Hz ─┼─ T7 ░░▒▒▓▓████▓▓▒▒░░ ─┤
│ │
░ low energy │
▒ medium │
▓ strong │
█ peak (decoded) │
│ │
3400 Hz ─┤ 16 bits/symbol │
3500 Hz ─┤ 8 bands × 4-FSK │
3600 Hz ─┤ 100 symbols/second │
3700 Hz ─┤ = 200 bytes/second │
3800 Hz ─┤ │
3900 Hz ─┤ on a 100 Hz grid │
4000 Hz ─┤ in audible mid-band │
4100 Hz ─┴─ ─┘
The receiver's AudioWorklet computes Goertzel energy on every 100 Hz bin in a single sweep, so it auto-detects which bands the sender lit up without any preamble handshake.
| Symbol rate | 100 symbols/s @ 10 ms |
| Modulation | 4-FSK, 2 bits/symbol/band, 100 Hz grid |
| Safe channel | 4 bands × 4-FSK = 1 byte/symbol = 100 B/s raw |
| Turbo channel | 8 bands × 4-FSK = 2 bytes/symbol = 200 B/s raw |
| FEC | Reed-Solomon, 16 parity / 239, corrects 8 bytes |
| Frame payload | 430 bytes chat / 256 bytes file |
| Compact headers | ~48 bytes (was ~95) |
| Pass gap / ACK window | 3500 ms / 350 ms |
Max missing indices |
6 (more → silent → full replay) |
| Message limit | 100,000 characters |
| Tests | 148 automated tests, 8 suites, 0 hardware required |
| Protocol version | 7 · wire-format documented in source/docs/PLATFORM_SPEC.md |
Effective end-to-end chat send time for a typical ~1 KB message — shorter is better:
v1.x │████████████████████████████████████████████████████│ ~60 s 5 ms, stop-and-wait
v2.0 │██████████████████████████████│ ~30 s 10 ms, no-ACK
v2.2 │█████████████████│ ~17 s compact headers, 256 B payloads
v2.3 │█████████████│ ~12 s + Reed-Solomon FEC
v2.4 │██████████│ ~9 s + adaptive done reply
v3.1 │██████│ ~5 s + Turbo multiband
v3.3 │█│ ~0.8 s Big Chat burst
The single-frame chat path on v3.3 plays a 1 KB message in roughly one second because the text deflated to a few hundred bytes fits in one 430-byte frame on the Turbo band — no probe, no announce, no handshake, no replay.
┌─────────────────────────────────────────────────────────────┐
│ Application chat bubbles, file download, profiles │
├─────────────────────────────────────────────────────────────┤
│ Session SenderSM / Receiver state machines │
├─────────────────────────────────────────────────────────────┤
│ Transport framing, CRC32, Reed-Solomon, deflate │
├─────────────────────────────────────────────────────────────┤
│ Reliability adaptive ACK (done / missing) │
├─────────────────────────────────────────────────────────────┤
│ Link multiband channels (Safe / Turbo) │
├─────────────────────────────────────────────────────────────┤
│ Physical 4-FSK symbols @ 10 ms, Goertzel decode │
└─────────────────────────────────────────────────────────────┘
Each layer is small, testable in isolation, and platform-neutral — the protocol contract in source/docs/PLATFORM_SPEC.md can be reimplemented in any language.
Every frame on the wire is the same shape — a length-prefixed JSON header followed by a payload, the whole thing Reed-Solomon coded so a few bad bytes get repaired before the CRC32 gate:
┌───────────┬────────────┬──────────────────────────────────────┐
│ len : u16 │ hdr : JSON │ payload : bytes ... │
│ big-endian│ 1 byte len │ (raw or zlib-deflated) │
└───────────┴────────────┴──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Reed-Solomon coded block: │
│ [ data (≤239 B) | parity (16 B) ] × ceil(len/239) │
│ corrects up to 8 corrupted bytes per 239-byte block │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 2-bit symbols × N bands × 100 symbols/s → 100 Hz grid │
│ [ preamble | sync | coded bytes | guard silence ] │
└──────────────────────────────────────────────────────────────┘
Frame types: file-start, file-data, chat, probe, mcs-probe, plus compact ack / done / missing / probe-ack on the back-channel.
A chat frame header is a small piece of the contract:
{"t":"c","f":"a1b2c3"} // single-frame, uncompressed
{"t":"c","f":"a1b2c3","z":1} // single-frame, deflate-compressed
{"t":"c","f":"a1b2c3","n":5,"i":2} // multi-frame, uncompressed
{"t":"c","f":"a1b2c3","n":5,"i":2,"z":1}z:1 is the v3.3 wire flag that tells the receiver to inflate the payload before decoding it as text. v3.1 endpoints ignore it; v3.3 endpoints honour it.
┌──────────┐
│ idle │
└────┬─────┘
│ send(text)
▼
┌──────────┐
┌──── │ compress│ ─────┐
│ │ + chunk │ │ ≤ 430 B
│ └────┬─────┘ │ → 1 frame
│ │ >430 B │
│ ▼ ▼
│ ┌──────────────┐ ┌────────────┐
│ │ arm ACK │ │ play burst │ → complete
│ │ listener │ │ (1 pass) │
│ └────┬─────────┘ └────────────┘
│ │ play pass 1
│ ▼
│ ┌──────────────┐
│ │ listen in │ pass gap 3500 ms
│ │ pass gap │ (350 ms ACK window)
│ └────┬─────────┘
│ │ drain queue
│ ├─ done ─→ complete (skip rest)
│ ├─ missing → replay only listed
│ └─ silence → full pass 2 broadcast
│ │
│ ▼
│ complete
▼ error
┌──────────┐
│ error │
└──────────┘
The receiver side is the mirror: it reassembles chunks idempotently, renders one bubble when all chunks arrive (or one in-order for single-frame), and decides the adaptive reply (done / missing / silence) during the sender's quiet pass gap.
- 4-FSK modem @ 10 ms symbols (12–48 cycles per tone — survives room reflections)
- Multiband channel layout (Safe 4-band, Turbo 8-band) on a 100 Hz grid
- Auto-detect mode: the receiver's worklet scans all bins in one pass
- Adaptive link probing: the sender scores the channel from the receiver's reply quality
- Silence erasure tolerance: 24 consecutive silent symbols become RS erasures, not a frame abort
- RX auto-gain calibrated from ~500 ms of ambient room noise
- Local self-test gate: a device must prove its own speaker→mic loopback (both bands) before send/receive is enabled
- Compact ~48-byte JSON headers (6-char session tokens, single-letter keys)
- Reed-Solomon FEC on every frame: 16 parity bytes per 239-byte block, corrects 8
- CRC32 payload gate after FEC repair
- Idempotent reassembly by
(fileId, frameIndex)— duplicate chunks ignored - Broadcast-repeat (default 2 passes) — receiver needs only one clean pass
- Adaptive ACK back-channel:
done,missing, or silent (full replay) - Up to 6 frame indices per
missingreply (fits the pass gap)
- Universal deflate compression via pure-JS pako (no browser API dependency)
- Wire flag
z:1marks compressed payloads - Same zlib format everywhere: estimate, airtime, and reconstruction agree
- Fast chat: single-frame = ~1 second burst, no ACK listener
- Big Chat: 100,000-character ceiling, all compressed on the wire
- Chat UI: profiles (name / avatar / colour), bubbles, live ETA estimate, sent/failed markers
- End-to-end SHA-256 integrity per file
- Live counters: heard / FEC-corrected / dropped / aborted
- Chunk grid: one tile per announced frame (blue = received, empty = missing, gray = about-to-replay)
- Spectrogram: live waterfall of the audio the receiver is hearing
- Mic-level readout calibrated from ambient noise
- Link-probe counter (sender side) and quality score (receiver side)
- Serverless: no telemetry, no upload, no network
- Zero infrastructure: works in airplane mode, behind a firewall, anywhere
- Self-contained: the protocol is documented independent of the browser
- Headless test harness: every layer runs in Node with no audio hardware
- Synthetic room-channel simulator: 5–20 ms echoes, −8/−12/−18 dB, noise, ±200 ppm clock drift
This project is the result of learning from real hardware, not theory. The version history reads like a log of what the channel actually taught us:
| Version | What happened |
|---|---|
| 1.x | 4-FSK @ 5 ms symbols + stop-and-wait ACK. Worked in simulations. Died on real phones: a 5 ms symbol holds only 6–12 tone cycles, so a single room reflection cancels whole symbols; and the receiver's ACK tones rarely reached the sender's mic. Transfers stalled on "waiting for ACK" forever. |
| 2.0 | The reboot. 10 ms symbols (12–48 cycles per symbol — reflections average out) and a no-ACK broadcast-repeat protocol. The receiver needs only one clean pass. This is what actually transferred a file between two phones. |
| 2.1 | Tried going back to 5 ms symbols for a 2× bit-rate boost. Real hardware said no: the receiver couldn't decode a single frame. Speed is useless if a transfer never completes. |
| 2.2 / 2.3 | Speed came from the framing layer instead: compact ~48-byte headers (6-char session tokens, single-letter JSON keys), 256-byte payloads, and Reed-Solomon FEC so the channel no longer had to be perfect. Plus a silence-tolerant decoder: up to 24 consecutive silent symbols become erasures the FEC fixes rather than a dropped frame. |
| 2.4 | The ACK back-channel returns — but as an optimization, not a dependency. In the quiet pass gap the receiver replies done (sender stops) or missing (sender replays only the lost frames). A lost reply costs one pass, never a stall. |
| 2.5 / 2.6 | A chat app over sound: messages ride the file frame layer flagged as chat, rendered as bubbles; sender profiles ride in the announce frame. |
| 3.0 / 3.1 | Multiband modulation and adaptive link scoring. Channels share a frequency grid; the receiver auto-detects mode. The Turbo channel nearly doubles the effective rate. |
| 3.3 | Big Chat. Universal pako compression removed the last browser-API dependency, adaptive ACK cut multi-frame chat airtime in half on clean links, and the message ceiling went from 4,000 to 100,000 characters — with 148 tests proving it works. |
The design philosophy that survived: measure the real channel, fix the real failure, and never let an optimization become a single point of failure.
send: text → pako deflate → chunk into ≤430 B frames
→ one Turbo-band burst (single frame = 1 pass, no ACK)
→ multi-frame = 2 passes + ACK listener in the pass gap
recv: deframe → idempotent reassembly by index → pako inflate
→ one bubble → optional `done` / `missing` reply
- Single-frame messages play once, with no ACK listener — ~1 second stays ~1 second.
- Multi-frame messages open the ACK back-channel:
done→ stop;missing→ replay only those indices; silence → full broadcast pass. - Lost ACK replies cost one pass, never a stall.
A 100 KB message is roughly 4,800 frames uncompressed. Deflated, the text becomes a few hundred bytes — a handful of frames. Compression happens before framing, on the wire, and at the receiver, using the same zlib format everywhere, so the estimate, the airtime, and the reconstruction all agree.
This isn't a 2014-era audio-modem hack. The choices that make it "ahead":
- Universal compression on the wire — the same pattern modern protocols (gRPC, QUIC, MQTT-SN) are moving toward, but done over audio with a single 50 KB library.
- Self-adapting reliability — adaptive ACK with done/missing is the acoustic equivalent of TCP fast retransmit / SACK, but for an unreliable physical layer that can drop ~10% of frames without notice.
- Receiver auto-gain from ambient noise — the modem measures the room before it transmits, the way modern radios perform channel estimation.
- Channel simulation in tests — the synthetic room-channel harness (reflections, noise, clock drift) is the same pattern as 5G NR link-level simulators.
- Zero-infrastructure P2P — no signaling server, no STUN/TURN, no relay, no accounts. The acoustic channel IS the address.
- Privacy by construction — the bytes never leave the room. No server sees them. No logs. No analytics. No telemetry.
- Open the live demo on two devices (or two tabs).
- On the sending device, tap Test This Device until it says ready — it must prove its own speaker→mic loopback first.
- On the receiving device, tap Start Listening.
- Type a message (try a big one — up to 100,000 characters) and hit send.
- Watch it appear as a bubble on the receiver. For files, a download link appears.
Keep devices close, moderate volume, quiet room for best results.
Requirements: Node.js 20.19+ or 22.12+, a browser with Web Audio + AudioWorklet, and localhost or HTTPS (microphone access).
npm install
npm run devOpen the Vite URL in two tabs or on two devices.
npm run typecheck # strict TypeScript
npm test -- --run # 148 headless tests (framing, FEC, modem loopback, channel sim, e2e)
npm run build # tsc --noEmit && vite build
npm audit --audit-level=moderateEvery test runs with no audio hardware: the modem is validated headlessly against a synthetic room channel (5–20 ms echoes, −8/−12/−18 dB, noise, ±200 ppm clock drift), and end-to-end transfers are simulated.
src/dsp/ 4-FSK modem, multiband channels, AudioWorklet receiver, channel simulator
src/transport/ framing, CRC32, Reed-Solomon FEC, adaptive link, reassembly
src/ui/ sender, receiver, self-test, spectrogram, link-test
src/utils/ pako compression + integrity helpers
docs/ protocol spec, technical plan, v3.3 big-chat spec, task status
source/docs/PLATFORM_SPEC.md— the full protocol contract: frame formats, modem parameters, reliability rules, security model, cross-platform adapters.source/docs/V3_3_BIG_CHAT.md— deep dive on universal compression and the adaptive ACK chat path.source/docs/TECHNICAL_PLAN.md— architecture and implementation plan.
| v1.x | v2.0 | v2.3 | v2.4 | v2.5 | v2.6 | v3.0 | v3.1 | v3.3 | |
|---|---|---|---|---|---|---|---|---|---|
| File transfer (no ACK) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Reed-Solomon FEC | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||
| Adaptive done / missing | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |||
Chat bubbles (m:1) |
✓ | ✓ | ✓ | ✓ | ✓ | ||||
| Sender profiles (pn/pa/pc) | ✓ | ✓ | ✓ | ✓ | |||||
| Adaptive link probe | ✓ | ✓ | ✓ | ||||||
| Multiband Turbo (200 B/s) | ✓ | ✓ | |||||||
| Fast chat burst | ✓ | ✓ | |||||||
| Universal pako compression | ✓ | ||||||||
| 100 000-character chat | ✓ |
- Encrypted transfers (AES-GCM above the transport layer).
- Optical screen→camera transport reusing the same frame layer.
- Recorded WAV fixtures for deterministic decoder regression tests.
- Measured throughput + retry counters surfaced in the UI.
ISC. See source/LICENSE.md.