Skip to content
pschatzmann edited this page Oct 29, 2026 · 1 revision

The protocol of ggwave by Georgi Gerganov: short messages of up to 140 bytes, sent as several simultaneous tones. Each byte is sent as 2 tones, one of 16 tones for each nibble, and a symbol carries 1 or 3 bytes at once. A message starts and ends with a marker and is protected by Reed-Solomon codes. ggwave is used by the Waver app, the ggwave web demos and its Python, JavaScript and Arduino bindings, so TinyModem can exchange messages with phones and browsers.

Advantages

  • Compatible with ggwave in both directions: the generated audio is the same as ggwave's (within 1 of 32767 per sample), tested with ggwave-to-file and ggwave-from-file for all 9 protocols.
  • Each message has a length and Reed-Solomon error correction: the receiver only delivers messages which are correct.
  • Robust against noise: no errors with noise of about 1.0, like MFSK.
  • Ultrasound protocols (15 – 19.5 kHz), which most people can't hear.
  • The receiver detects the speed (normal, fast or fastest) of the protocol.

Disadvantages

  • Slow: 5 to 47 bytes/s before error correction and markers. A message of 10 bytes takes 1.45 s with ggwave-fast.
  • Messages of up to 140 bytes: there is a delay until 140 bytes were written or flush() is called.
  • The sample rate must be a multiple of 187.5 Hz: 48000 or 24000 Hz work, 44100 Hz doesn't.
  • The audible protocols use tones up to 6.4 kHz, above the telephone band.
  • Different clocks of sender and receiver: about ±0.3% (ultrasound ±0.1%).
  • The decoder needs about 10 times more CPU than MFSK.
  • The mono-tone (MT) protocols and the fixed length format of ggwave are not supported.

Modes

Mode ggwave protocol Tones Frames per symbol Bytes per symbol Data rate
ggwave-normal 0 GGWAVE_AUDIBLE_NORMAL 1875 – 6375 Hz 9 3 16 bytes/s
ggwave-fast (ggwave) 1 GGWAVE_AUDIBLE_FAST 1875 – 6375 Hz 6 3 23 bytes/s
ggwave-fastest 2 GGWAVE_AUDIBLE_FASTEST 1875 – 6375 Hz 3 3 47 bytes/s
ggwave-u-normal 3 GGWAVE_ULTRASOUND_NORMAL 15000 – 19500 Hz 9 3 16 bytes/s
ggwave-u-fast 4 GGWAVE_ULTRASOUND_FAST 15000 – 19500 Hz 6 3 23 bytes/s
ggwave-u-fastest 5 GGWAVE_ULTRASOUND_FASTEST 15000 – 19500 Hz 3 3 47 bytes/s
ggwave-dt-normal 6 GGWAVE_DT_NORMAL 1125 – 2625 Hz 9 1 5 bytes/s
ggwave-dt-fast 7 GGWAVE_DT_FAST 1125 – 2625 Hz 6 1 8 bytes/s
ggwave-dt-fastest 8 GGWAVE_DT_FASTEST 1125 – 2625 Hz 3 1 16 bytes/s

A frame has 1024 samples at 48000 Hz (21.3 ms) and the tones are multiples of 46.875 Hz. The data rates are the raw rates: each message adds 2 markers of 16 frames, 3 bytes for the length and 2 to 56 bytes of error correction. ggwave-fast (protocol 1) is the default, like in the ggwave tools. The ultrasound modes need 48000 Hz; all others also work at 24000 Hz, and the DT modes at 12000 Hz.

Usage

I2SStream i2s;
GGWaveStream modem(i2s, RXTX_MODE);

void setup() {
  GGWaveConfig cfg = modem.defaultConfig();  // ggwave-fast
  cfg.protocol = GGWAVE_AUDIBLE_FASTEST;     // or cfg.setMode("ggwave-fastest")
  cfg.sample_rate = 24000;
  ...
  modem.begin(cfg);
}

void loop() {
  modem.print("Hello");
  modem.flush();  // sends the message
  while (modem.available()) Serial.write(modem.read());
}
echo -n "Hello" | tinymodem --tx -f hello.wav ggwave-fast
tinymodem --rx -f hello.wav ggwave-fast

GGWaveConfig, GGWaveEncoder, GGWaveDecoder, GGWaveStream and GGWaveStreamRTOS are the ggwave classes. The encoder collects the written bytes and sends a message when 140 bytes were written or when flush() (or endTransmission()) is called. The decoder writes the payload of each correct message to its output. On the command line, -R, -v, -q, --rx-one, --packets and --Xrxnoise are supported. Note that echo adds a newline, which is part of the message: use echo -n to send exactly the text.

The ggwave-loopback and ggwave-send-i2s examples show the use with the AudioTools.

GGWaveConfig field Default
protocol GGWAVE_AUDIBLE_FAST ggwave protocol (GGWaveProtocolId, see the modes)
volume 1.0 Transmit amplitude. ggwave's tools use 0.5 (-v50)
dss false XOR the payload with ggwave's DSS sequence (ggwave option -d)
rx_any_speed true Receiver: accept the normal, fast and fastest variant of the protocol
marker_threshold 3.0 Receiver: power ratio of a marker tone to its neighbor
rx_one false Receiver: stop after the first message
channel 0 Receiver: input channel
sample_rate, channels, bits_per_sample 48000, 1, 16 Audio format

The receiver only listens to the tones of the configured protocol: an audible receiver doesn't receive the ultrasound or DT protocols.

How it works

A message consists of:

  1. The start marker: 16 frames with 16 tones. Tone i uses the bin 2i if i is even and 2i + 1 if i is odd.
  2. The length of the payload (1 – 140), Reed-Solomon coded with 2 parity bytes (3 bytes).
  3. The payload, Reed-Solomon coded with max(4, 2 * (length / 5)) parity bytes (2 for less than 4 bytes).
  4. The end marker: like the start marker, but with the other bin of each pair.

Each data symbol lasts 3, 6 or 9 frames and carries 1 or 3 bytes: nibble j of the symbol selects one of the 16 bins 16j to 16j + 15 above the first tone. All tones of a symbol or marker are sent at the same time with the phase offsets of ggwave, and the amplitude rises during the first 15% and falls during the last 15% of each symbol and marker. The Reed-Solomon code is the same as ggwave's (GF(256) with the polynomial 0x11d, generator roots α⁰ to α^(ecc-1)), but TinyModem implements it with lengths which are defined at runtime (GGWaveRS).

The receiver works differently from ggwave, which records the audio of a whole message (up to several MB) and analyses it afterwards. TinyModem decodes while the audio arrives, with a fixed amount of memory:

  • All tones are multiples of the frame rate, so they are periodic in a frame. The decoder calculates a DFT of the used bins for each quarter frame (block) and adds 4 blocks to the spectrum of a frame.
  • For each block, a marker score is calculated: the sum of (on - off) / (on + off) over the 16 bin pairs of the marker. It is +16 for the start marker, −16 for the end marker and about 0 for noise and data. Pairs which are 40 dB below the strongest bin are ignored.
  • When the score indicated a start marker, the position with the highest sum of the scores over the length of the marker provides the start of the data (a matched filter). This is more accurate than the first or last detection in noise.
  • Then 9 candidates decode the data in parallel: normal, fast and fastest, each with the timing offsets −1, 0 and +1 block. Each symbol is decoded by adding the power of the frame windows inside the symbol, without its first and last block. ggwave adds the frames coherently, which fails if the clocks of sender and receiver differ by 0.1%.
  • A candidate is dropped if the Reed-Solomon code of the length or of the payload can't be corrected, or if the end marker isn't found at the expected position. The first candidate which passes all checks provides the message.

Measured performance

Loopback with the unit tests at 48000 Hz. The noise level is the amplitude of the uniform noise which is added in the receiver (--Xrxnoise): the signal has an rms level of about 0.3, so a noise level of 1.0 (rms 0.58) corresponds to a signal-to-noise ratio of about −6 dB over the whole band.

normal fast fastest
Noise without errors (audible, ultrasound, DT) 1.0 1.0 1.0
Clock difference without errors, audible ±0.3% ±0.3% ±0.3%
Clock difference without errors, ultrasound ±0.1% ±0.1% ±0.1%
Clock difference without errors, DT ±0.7% ±0.7% ±0.7%

On a PC, the decoder of ggwave-fast ran 35 times faster than real time at 48000 Hz and 79 times at 24000 Hz (DT: 91 and 160 times). This is much slower than the other modems, because it calculates a DFT of 96 bins at the full sample rate: on a microcontroller use 24000 Hz and check the speed on your board. It hasn't been tested on real hardware yet.

Clone this wiki locally