sotto

Open 2400 bit/s digital voice for FM radios: a vocoder, error correction and a C4FM modem path, built for small fixed-point DSPs. Slower links get the same 48-bit frame stretched to 1200, 600 and 400 bit/s.

New to vocoders? How sotto works is a short illustrated course (written for high-school students) on pitch, envelopes, quantization, synthesis and the radio link.

Why sotto

Open digital voice on ham radio mostly means Codec 2, whose low-rate modes are no longer being developed, while every Fusion, D-STAR and DMR radio carries a closed AMBE vocoder. The newest open voice codecs are neural networks, which need far more processing than the radios people already own. sotto is built for those radios: the TI C5517 DSP inside a Yaesu FTM-300D, and the 168 MHz Cortex-M4 in OpenRTX handhelds such as the TYT MD-UV380, which the M17 project uses. In simulation its v3.8 encoder and decoder fit both chips (on average, C5517: 137 and 55 MHz of 200; Cortex-M4: 118 and 36 MHz of 168), and it is MIT-licensed.

Is a small codec giving up much? We tested sotto against FARGAN, the neural vocoder in Opus, decoding the same 2400 bit/s bits. Stock FARGAN came out level with sotto's own decoder, at several times the compute and eight times the memory, too much for either radio. Remove sotto's hiss index (the 4-8 kHz band carried in bits sotto already sends) and FARGAN gets worse, so that idea helps a neural decoder too. Listen in Part 4; details in fargan-study.md.

Decoder of the same sotto v3.8 bitsComputeWeights and tablesWB PESQ
sotto v3.8 decoder55 MHz on the C5517, 36 MHz on the Cortex-M4under 100 KB1.78
FARGAN (Opus), with sotto's hiss indexabout 305 million multiply-adds/s: fits neither radio811 KB1.77
FARGAN, 4-8 kHz guessed without the hiss indexthe same811 KB1.63

Wideband PESQ (0-8 kHz), mean of 10 clean voices, no radio channel. FARGAN fed the original speech's own features scores 2.72, but those take far more than 2400 bit/s to send; the gap is mostly sotto's 34-bit envelope, and closing it would mean retraining a network for each target, on hardware this project doesn't assume.

Try it with your own voice

Record up to 30 seconds, or pick an audio file, and the page encodes the same recording at all four rates (2400, 1200, 600 and 400 bit/s) and decodes each one, so you can switch between them. The encoders and decoders run here, in your browser, compiled to WebAssembly from the same C code that targets the radios; nothing is uploaded. A .sot file is the bitstream itself: 6 bytes per frame, so 300, 150, 75 or 50 bytes a second (the C tools decode it too: c/sotto_dec -16 [-1200|-600|-400] file.sot out.raw). Every rate here carries the 4-8 kHz hiss band; the menu also plays plain sotto 600 and 400, the 8 kHz streams the UV-K6 pager sends (sotto_enc -600|-400).

Loading the codec…

The level is set automatically: speech is scaled to −26 dBFS, the level the codec expects. Use headphones or a quiet room. Microphones that filter out everything above 4 kHz (some phones, Bluetooth headsets) leave sotto no hiss to send. A .sot file has no header, so the page takes its rate from the name it downloads with (…-600.sot) or from the menu.

Four rates: 2400, 1200, 600 and 400 bit/s

Every sotto mode sends the same 48-bit frame; the slower ones make it cover more time. Halving the rate halves the air time of a message (the UV-K6 pager sends sotto 1200 at 1800 baud), and 600 and 400 bit/s leave room for stronger error correction or a slower, more robust modem. Each voice below was encoded once at every rate from the same 16 kHz recording, with no radio channel, and is decoded here in your browser from the frames.

Mode48 bits everyWhat it sendsAudioPESQ
sotto v3.8 (2400)20 mspitch, voicing, energy and a 34-bit envelope every frame16 kHz, hiss band
sotto 1200 hiss40 msone frame with a coarser envelope, then one coded as small steps from its neighbours16 kHz, hiss band
sotto 600 hiss80 ms4 frames: one voicing pattern, a pitch line, an energy level and its change, and envelopes every 40 ms with straight lines between; the hiss band rides in the pitch bits of unpitched stretches16 kHz, hiss band
sotto 400 hiss120 ms6 frames, the same way16 kHz, hiss band

PESQ is narrowband (0-4 kHz, mean of the six voices), so it can't hear the 4-8 kHz hiss band. Listening tests on other voices: sotto 1200 hiss beat plain sotto 1200 in all 6 pairs and came close to v3.8 (2 ties, 1 loss); sotto 600 beat Codec 2 700C (3-0) and Codec 2 1200 (2-0); sotto 400 split 1-1 with Codec 2 700C. Adding the hiss band to 600 and 400 won all 7 of its listening pairs against the plain modes, and sotto 600 hiss tied sotto 1200 hiss (2 of 2); with it, narrowband PESQ moves from 2.09 to 2.12 at 600 and from 1.76 to 1.79 at 400. Details: sotto-1200-hiss.md, sotto-vlr.md.

Part 1: sotto's encoder against Codec 2's

Both encoders run at 2400 bit/s (48 bits every 20 ms). Each pair went through exactly the same chain: the same System Fusion (C4FM) frames and error protection, the same simulated radio channel with the same fading, and the same software receiver. The only difference is the encoder and decoder.

no radio channel UHF 446 MHz, 60 km/h, 52 dB-Hz (a weak mobile signal) VHF 146 MHz, 30 km/h, 56 dB-Hz (slow fading)

The score is PESQ, an automatic speech-quality estimate from 1 (bad) to 4.5 (excellent), averaged over three fading runs. It is the narrowband version (0-4 kHz), so it can't hear sotto's 4-8 kHz hiss band. sotto v3.8 is an early prototype: a mel-spaced spectral envelope in place of Codec 2's 10th-order LPC, quantized with a codebook trained on 632 speakers and small enough (29 KB) for the radio's DSP, a tracker that decides pitch and voicing together, its own handling of lost frames, and, new in v3.8, 16 kHz output with the hiss of s, sh, f and whispers rebuilt from bits it already sends.

The sotto rows are not audio files. The page fetches what the receiver produced (6 bytes per 20 ms frame, under 5 KB per clip, plus a list of the frames that failed their check) and decodes it in your browser with sotto's C decoder compiled to WebAssembly. Loading the decoder…

Part 2: more voices, no radio channel

Eight LibriSpeech readers whose voices were kept out of the codebook's training (the same 12 s clips sotto's tests score), at 16 kHz. No radio channel, so this is the codec alone. The sotto rows are decoded here from the encoder's 3600 bytes per clip.

Part 3: same encoder (Codec 2), different channel protection

These clips all use the stock Codec 2 2400 encoder. The rows differ only in how the voice bits are protected and where they sit in the Fusion frame.

UHF 446 MHz, 60 km/h, 52 dB-Hz same transmit power for every row

Part 4: sotto's decoder against a neural vocoder

The same v3.8 bits (no radio channel), decoded four ways. FARGAN is Opus's neural vocoder with its stock weights; it gets the features of sotto's decoded speech, so its top bands carry sotto's 4-8 kHz hiss index. The last row is FARGAN fed the original speech's own features, which no 2400 bit/s link could carry: it shows how good the vocoder itself is. Scores here are wideband PESQ (0-8 kHz).

What you're hearing

sotto v3.8 (Part 1) describes the spectrum with 20 mel-spaced amplitudes, quantized with 34 bits by a four-stage codebook trained on 632 speakers (Google's Speech Commands volunteers and LibriSpeech readers), plus a formant postfilter and a frame-energy field, where Codec 2 uses a 10th-order LPC filter. The codebook is stored as 8-bit values in 29 KB, so it fits in the radio's DSP alongside the modem, and the energy field measures only the band the envelope covers, so a low hum doesn't turn into buzz. Its tracker starts from Codec 2's pitch estimate and fixes octave errors (pitch reported at double or half its true value), which Codec 2 makes most often on higher voices. The same tracker decides which frames are voiced, follows pitch up to 500 Hz, and avoids the warbling that wandering pitch causes in quiet transitions. Breathy sounds like the "v" in "five" get harmonics only below 1 kHz with noise above, at no extra bits. Pauses decode as true silence. When a frame is lost, it holds the last sound for 40 ms and fades it out within 80 ms, which a listener preferred to repeating and cutting. It keeps Codec 2's synthesis. It needs no memory between frames, so a lost frame doesn't damage the next one.

New in v3.8: the decoder plays 16 kHz audio. Codec 2 and earlier sotto versions stop at 4 kHz, which is why s, sh and f sound dull on them. In frames with no pitch (the hiss sounds), the 7 pitch bits are free, so v3.8 uses 6 of them to send how loud the 4-8 kHz hiss is, and the decoder rebuilds it as shaped noise. Voiced sounds get an upper band predicted from their 0-4 kHz shape, and the seventh bit carries a slow side channel (three numbers every 2 s) that corrects that prediction for the speaker's voice. Frames stay 48 bits, and a v3.7 decoder still plays a v3.8 stream (without the upper band). Whispers and voiced hiss (the z in "prism") are sent as hiss too. The ve9qrp recording only exists at 8 kHz, so its original has no upper band; its sotto rows still get some, because the decoder predicts the upper band of voiced sounds from the lower one.

VW slot (used for everything in Part 1) puts the voice in the full-rate part of the Fusion frame, adds a CRC to each 20 ms frame and protects everything with a strong tail-biting convolutional code. DN slot + our code uses the half-rate slot Fusion radios use for DN mode, with a weaker version of the same code. Fusion-style protection mimics how DN protects its voice bits (the first 27 bits sent three times, the rest unprotected), applied to Codec 2. That's a statement about Codec 2 with that protection, not about AMBE+2, whose decoder is built around it.

At 146 MHz and 30 km/h, fades outlast the 100 ms interleaver, so whole frames are lost. sotto fades lost frames out quickly. Spreading each codeword over two Fusion frames (200 ms) helps at 60 km/h and is still being tested.

Simulation only, not yet on the air. Sources: the ve9qrp recording from the Codec 2 project, and LibriSpeech readers (CC BY 4.0; see samples/README.md). Details: encoder-v1.md (v1 to v3.6), unvoiced-hiss.md (v3.8), audio-race.md. Code: github.com/baitisj/sotto (MIT; the Codec 2 fork is LGPL-2.1).