File transfer over audio using a data fountain Live demo on https://services.coopix.eu/airgap-sound/
  • Rust 82.2%
  • JavaScript 10.5%
  • Shell 2.5%
  • HTML 2.3%
  • Dockerfile 1.4%
  • Other 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Solène RAPENNE ec4a7c9c9a Record the first two-device transfer in the status section
The maintainer reported a working transfer between two devices with
the fast profile: a laptop sending (with both the CLI and the web
page) and an iPhone receiving through the web page. The received file
was identical to the original.

This is also the first time the web page ran in a real browser, so
the status no longer claims it never did. The robust profile across
two devices remains unconfirmed.

Documentation only. The checks were not run: this environment has no
Rust toolchain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:23:40 +00:00
docs Add a guide for choosing the speaker and the microphone 2026-11-02 11:16:12 +00:00
src Add a fast profile, detected by the receiver from the chirp direction 2026-11-02 12:12:50 +00:00
tests Add a fast profile, detected by the receiver from the chirp direction 2026-11-02 12:12:50 +00:00
web Stamp the web site with its build date and source commit 2026-11-02 12:14:19 +00:00
.dockerignore Stamp the web site with its build date and source commit 2026-11-02 12:14:19 +00:00
.gitignore Add a Containerfile building the static site and the CLI 2026-11-02 11:33:10 +00:00
AGENTS.md Record the first two-device transfer in the status section 2026-11-02 12:23:40 +00:00
Cargo.lock Add a WebAssembly build of the sender and receiver 2026-11-02 11:29:09 +00:00
Cargo.toml Link the source repository from the web page 2026-11-02 11:57:45 +00:00
CLAUDE.md Add AGENTS.md, SECURITY.md and CLAUDE.md 2026-11-02 11:05:14 +00:00
Containerfile Stamp the web site with its build date and source commit 2026-11-02 12:14:19 +00:00
README.md Document the speed profiles 2026-11-02 12:12:50 +00:00
SECURITY.md Add the browser page and document the web version 2026-11-02 11:29:09 +00:00

airgap

File transfer over sound, in near-ultrasound (18–19.85 kHz), using a fountain code: the sender broadcasts in an endless loop, the receiver can start at any time and rebuilds the file piece by piece, whatever frames it missed.

Written in Rust (FFT, filters, error correction and the fountain code are implemented in-tree). Linux command-line tool, plus a web version (WebAssembly) that can send or receive from a browser, phones included: see web/README.md.

sender:   airgap send document.pdf
receiver: airgap recv
          [##########----------] 41/80 blocks (51%), ~40s left | band level -52 dBFS | frames ok 43 / bad 2 | quality 18 dB
          Received "document.pdf" (10.0 KiB) -> ./document.pdf: 82 valid packets for 80 needed, after 1m19s of listening.

Is it really inaudible?

Yes, for most adults, with some caveats:

  • The whole signal sits between 18.0 and 19.85 kHz. A Kaiser FIR band-pass (511 taps) removes everything below ~17.4 kHz at −80 dB. Measured: energy emitted in the audible range (<17 kHz) is 130 dB below the useful signal (airgap sim prints it on every run).
  • Bursts start and end with smooth ramps, so there are no clicks.
  • Caveats: children and teenagers sometimes hear up to 19–20 kHz. Dogs and cats hear it. A speaker driven too hard can produce audible distortion. Keep the volume reasonable, and if you hear anything (hiss, ticking), lower it.

Install

cargo build --release          # Rust >= 1.87
cp target/release/airgap ~/.local/bin/

Without Rust on the machine, build everything in a container (podman or docker):

podman build -o site .                # static web site in ./site, ready to host
podman build --target cli -o bin .    # static Linux binary in ./bin/airgap

See the Containerfile for a local test server (--target serve).

Playback and capture go through pacat (package pulseaudio-utils, works with PulseAudio and PipeWire) or aplay/arecord (alsa-utils). Otherwise use --player/--recorder or the --raw pipes.

Usage

# Broadcast in a loop (Ctrl-C stops cleanly at the end of the current frame)
airgap send file.zip [--profile fast] [--volume 0.5] [--device NAME]

# Receive (stops as soon as the file is complete, writes it under its original name)
airgap recv [--dir dir/] [-o output.zip] [--keep]

# Test the whole chain without hardware: sender -> simulated room -> receiver
airgap sim file.zip --snr 8 --ppm 150 --drop 0.3 --skip 40

Other ways to connect audio:

airgap send f.txt --wav tx.wav --count 100          # make a WAV to play with anything
airgap recv --wav recording.wav                      # decode a recording (any sample rate)
airgap send f.txt --raw | aplay -t raw -f S16_LE -r 48000 -c 1
arecord -t raw -f S16_LE -r 48000 -c 1 | airgap recv --raw
airgap send f.txt --raw --count 50 | airgap recv --raw   # perfect digital loopback

airgap --help lists every option.

Throughput

Two speed profiles, chosen on the sender (--profile); the receiver recognizes each frame's profile by itself, so recv needs no option.

profile speed use it when
robust (default) ~83 bytes/s always works first: echoing rooms, distance, speakers facing a wall
fast ~254 bytes/s (3x) short range, receiving microphone facing the speaker, quality above ~20 dB
file robust fast
1 KiB ~17 s ~6 s
10 KiB ~2 min ~45 s
100 KiB ~21 min ~7 min
~1 MiB (max) ~3.5 h ~70 min

Start with robust, look at the receiver's quality and packet counts, then try fast. If the receiver shows many bad packets with fast, go back to robust.

Compress before sending (zstd -19, xz).

Tips for real audio

  1. Start with the system volume around 50% and watch the receiver's status line:
    • band level: energy captured in 17.6–20.3 kHz. If it does not move while the sender runs, the microphone or the speaker does not reproduce these frequencies (see point 3).
    • quality: frames get through above ~10 dB. Below ~8 dB, move the devices closer or raise the volume.
  2. Disable microphone processing: noise suppression, echo cancellation (PipeWire/PulseAudio echo-cancel module, "noise suppression" filters) erase ultrasound. Disable "audio enhancements" on the speaker side too.
  3. Built-in laptop microphones and small speakers (including monitor speakers) often lose sensitivity above 18 kHz. An external speaker and a decent USB microphone give several meters of range.
  4. Losses are fine: the fountain code fills the gaps with any other frames. Someone walking by, a door slamming or a conversation in the room do not stop the transfer.

Troubleshooting the audio path

When nothing is received, check the devices, find out where the sound really goes, then measure what survives the trip. docs/choosing-devices.md explains how to pick a speaker and a microphone and how to read every output below.

airgap devices                # which speakers/microphones can work, with a verdict for each
airgap send f.txt --device 27g2g4   # --device: ID, part of the name, or full name
                              # send/recv print the device the stream is REALLY routed to
airgap recv -v                # same for the input, plus one log line per detected frame

send, recv, probe and monitor look up their own stream in PulseAudio/PipeWire (with pactl) and print the real route. This matters because the sound server remembers a device per application and can ignore the default. They also warn about known traps: muted device, low volume, HDMI/DisplayPort monitor speakers (often cut above 16–18 kHz), Bluetooth (codecs low-pass around 16 kHz), a monitor source (records what the computer plays, not the microphone), echo cancellation or noise suppression sources.

Then measure the frequency response of the whole chain, speaker -> air -> microphone:

# sender: test tones from 15 to 21 kHz; --audible adds a 1 kHz double beep per cycle
# so you can hear which speaker is playing
airgap probe --audible

# receiver: live spectrum, best level of each tone above the noise floor, and a verdict
airgap monitor
tone         best level above noise floor (* = data band)          now
 16.0 kHz    ##################################...... 52 dB    50
 17.0 kHz    ###########################............. 41 dB    40
 18.0 kHz *  #####................................... 8 dB     6
 19.0 kHz *  ........................................ 0 dB     0

FAIL: the path carries tones up to about 17.0 kHz but the data band is lost (worst 0 dB). ...

A full-range sweep (--from 1000 --to 21000 --step 1000, audible) checks that sound flows at all. Note: a loopback test on a single computer may succeed through a monitor source without the microphone being involved. recv prints the input route, so check it.

Security

Audio is a broadcast medium. Anyone within earshot with a microphone receives the file. There is no encryption and no authentication: the CRC32 detects transmission errors, not tampering. Encrypt and sign beforehand if needed (e.g. age, minisign). See SECURITY.md.

How it works

file -> object (name + data) -> K blocks of 128 B -> fountain code -> 144 B packets (CRC32)
     -> scrambler -> K=7 r=1/2 convolutional code -> interleaver -> OFDM DQPSK -> 18-20 kHz filter -> speaker

Fountain code (src/fountain.rs): random linear code over GF(2). Packet number esi is the XOR of a subset of blocks. For esi < K it is the block itself (systematic part: with no loss, exactly K frames suffice). Beyond that, each block is included with probability 1/2, drawn by a PRNG seeded with (file id, esi), so the receiver rebuilds the mix from the header. Any K independent packets are enough. With K+n packets received, failure probability is ~2⁻ⁿ, so about K+1.6 are needed on average. Decoding is incremental Gaussian elimination: every useful packet moves the progress bar one step. The file id is the CRC32 of the content, checked at the end.

Modem (src/modem.rs):

| silence | chirp | guard | 2 ref symbols | data symbols | silence |
profile chirp symbol (FFT + cyclic prefix) carriers modulation code packets per frame
robust up, 18 -> 19.85 kHz 2048 + 2048 (85 ms) 80 x 23.4 Hz DQPSK 1/2 1
fast down, 19.85 -> 18 kHz 1024 + 512 (32 ms) 40 x 46.9 Hz D8PSK 2/3 4
  • Detection: matched filter on the chirp, normalized by local energy (correlation between 0 and 1). The received signal is band-pass filtered first, so voice, music and low-frequency noise do not interfere.
  • Profile detection: the receiver runs one matched filter per chirp direction and decodes each frame with the profile of the strongest one.
  • Robust: the 43 ms cyclic prefix absorbs room reverberation, even when the microphone hears more echo than direct sound (speakers facing a wall, screen speakers firing backwards).
  • Fast: shorter guard, 8 phases instead of 4, less redundancy (punctured code) and 4 packets per frame, for setups where the direct sound dominates.
  • Differential PSK in time: no channel estimation needed, and timing offsets cancel out.
  • Clock drift between sound cards: the 2 identical reference symbols measure the phase rotation per symbol, which is then tracked symbol by symbol (M-th power estimator). Tested up to ±250 ppm.
  • Error correction: K=7 (171/133) convolutional code with soft-decision Viterbi, and a pseudo-random interleaver over the whole frame. A CRC32 per packet rejects corrupted ones, and the fountain code makes up for them.

Simulation results (airgap sim). Robust: no frame lost down to ~7 dB in-band SNR, in a room with RT60 300 ms and 5 dB more echo than direct sound (--reverb 300 --drr -5), and with ±200 ppm clock mismatch. Fast: no loss at 15 dB SNR, in a room with RT60 150 ms and direct sound 10 dB above the echoes, and at ±250 ppm; it degrades quickly with more echo. Beyond that, frames get lost, but the transfer still completes as long as some get through (the fountain code compensates).

Layout

file role
src/main.rs CLI: send, recv, sim, devices, probe, monitor
src/modem.rs OFDM modulation / demodulation, synchronization
src/fountain.rs fountain code (encoder, Gaussian elimination decoder)
src/fec.rs CRC32, scrambler, interleaver, convolutional code + Viterbi
src/packet.rs packet and transmitted object formats
src/receiver.rs per-transfer packet collection
src/dsp.rs complex numbers, FFT, FIR filters, streaming convolution, spectrum
src/audio.rs WAV, raw streams, driving pacat / aplay / arecord
src/sim.rs simulated acoustic channel (echoes, clock drift, noise)
src/diag.rs pactl parsing: devices, real stream routes, warnings
web/ WebAssembly build and browser page (sender and receiver)
src/probe.rs probe tones and spectrum analyzer for probe / monitor

cargo test runs the unit tests and two end-to-end tests through the simulated channel.

Contributing

See AGENTS.md for the architecture, the invariants that must not break, the checks to run and the commit conventions. Security issues: SECURITY.md.