Acoustic Link
16-MFSK, 4.0–6.8 kHz, ~187 bps raw · RS(15,11)/GF(16) FEC + erasure flagging · a third signaling channel for filedrop-manual: no camera, no QR, no list
Link settings
?k=…&salt=…) for sharing.
Send — speaker
Receive — microphone
Live decode
Self-test — no audio hardware needed
Notes
Demo on one machine: press Listen, then Send — the mic hears the speaker across the air gap between them. Two machines: one listens, the other sends.
Frame format: 250 ms pilot · 8-symbol sync · version + mode + length header block · payload + CRC-16/CCITT, all carried in RS(15,11) codewords (11 data + 4 parity symbols each), each block correcting 2 symbol errors or 4 erasures. Chord density is selectable: the payload lights k = 1, 2, 4 or 8 tones per symbol, and each constant-weight k-of-16 chord is one symbol in GF(24/6/10/13) — so a misdetected chord is still exactly one correctable RS symbol. That trades robustness for rate: 4, 6, 10 or 13 bits per symbol (up to 3.25×). Sync and the header always stay single-tone, so acquisition is mode-independent and the receiver auto-detects the mode. Per-tone amplitude is split 1/k to stay clear of clipping, so denser chords are quieter — the physical robustness/throughput trade.
Validated headless (Node invariant suite): GF(16) arithmetic, RS errata recovery across 400 random error/erasure patterns, clean loopback at 48 k and 44.1 k, random silence padding, AWGN to 6 dB SNR (decodes to 4 dB), 48 k → 44.1 k resample mismatch, 25% gain wobble, all combined, and recovery from a steady in-band interfering tone; pure noise never false-decodes (CRC + sync gate).
Browser-path (cannot be tested headless): mic requires HTTPS and a user grant; the getUserMedia constraints disable echo cancellation / noise suppression / AGC, but some stacks ignore them — OS-level noise suppression can eat the tones; AudioWorklet loads from a Blob URL and falls back to ScriptProcessorNode automatically; on iOS, sending forces the loudspeaker route instead of the earpiece; speaker volume around 60–80% works best; decode passes cost ~100–250 ms of CPU each while listening.
Known limits: RS(15,11) corrects up to 2 wrong tones or 4 erasures per 15-symbol block; a burst ruining 3+ symbols in one block still fails that frame (CRC catches it, resend is the fallback). Broadband noise spread thin across a long frame is the hard case, not localized transients. Range is conference-room scale, not hallway scale.
Deploy: single self-contained file, zero dependencies — drop modem.html at the site root; GitHub Pages serves it at /modem.