Previous: Synthesis · Course home · Next: How we measure “better”
The vocoder makes 2,400 bits per second. But radio signals get noisy, and in a moving car they fade in and out many times a second. So before sending, sotto adds extra bits that let the receiver find and fix errors, and it shuffles the bits so one fade can’t wipe out one whole frame. Then the bits are turned into radio frequency hops called C4FM.
sotto uses the same kind of signal as Yaesu’s System Fusion radios. It’s called C4FM (4-level FM). The transmitter’s frequency hops between four values, 4,800 times a second. Each hop carries 2 bits:
| Frequency shift | Bits |
|---|---|
| +1,800 Hz | 01 |
| +600 Hz | 00 |
| −600 Hz | 10 |
| −1,800 Hz | 11 |
4,800 hops × 2 bits = 9,600 bits per second on the air. The hops are smoothed (the curvy blue line) so the signal doesn’t splatter onto neighboring channels. The receiver measures the frequency at the center of each hop (orange dots) and decides which of the four levels it was closest to.
Because only the frequency changes, the signal’s strength stays constant. That’s what lets it work through an ordinary FM radio’s transmitter.
The bits are sent in frames of 100 ms = 960 bits:
100 ms of voice is 5 sotto frames × 48 bits = 240 bits. But the payload has room for 720. sotto uses the extra 480 bits for protection. (Yaesu’s own “DN” mode uses only part of the payload for voice. sotto uses the whole “VW” voice slot, because the team’s simulations showed that spending the space on stronger protection gives about 6 dB more margin: it works with a signal about one quarter as strong.)
A CRC (cyclic redundancy check) is a short fingerprint calculated from the data. sotto adds an 8-bit CRC to each 48-bit frame. The receiver recalculates it. If the fingerprints don’t match, the frame is broken. A CRC can’t fix anything, but it tells the receiver which frames not to trust.
Next the 5 × 56 = 280 bits go through a convolutional code. Every output bit is a mix (using XOR) of the last 7 input bits. Out come 720 bits: about 2.6 output bits for every input bit.
Because each input bit affects many output bits, and each output bit depends on several input bits, a few wrong bits don’t destroy the message. The receiver uses the Viterbi algorithm: it considers the possible messages and finds the one that best explains what it received, even with errors sprinkled in. It’s like reading a sentence with a few smudged letters: the letters around them tell you what they must have been.
The receiver also uses soft bits. Instead of deciding “this is a 1” right away, the demodulator says how sure it is (“probably a 1”, “definitely a 0”, “no idea”). The Viterbi decoder trusts the confident bits more. The code calls these confidence values LLRs (log-likelihood ratios).
Radio signals in a moving car fade. Reflections off buildings and hills add together, sometimes helping and sometimes canceling. The signal strength dips sharply many times a second:
At 446 MHz and 60 km/h there are about 25 dips per second, each very short. At 146 MHz and 30 km/h there are only about 4, but each one lasts much longer. Long fades are the hardest case.
During a deep fade, a whole run of bits in a row comes out as garbage. If all of one frame’s bits sat together, that frame would be lost. So sotto spreads the bits: it shuffles the 720 positions so that each frame’s bits are sprinkled across the whole 100 ms. This is also called interleaving.
This is a real run of sotto’s encoder and decoder with a 10 ms burst of garbage in the middle of the payload. Without spreading, the burst lands mostly on frame 3, and frame 3 is lost. With spreading, every frame loses only a few bits, the code fixes them all, and all 5 frames come through.
Last, the bits are mixed (XOR) with a fixed pseudo-random pattern called PN9. This avoids long runs of the same symbol, which a computer sound card (which can’t pass a steady level) would distort. The receiver mixes with the same pattern again to undo it.
The receiver runs the steps backwards:
When a frame fails its CRC, the decoder has to play something for those 20 ms. Hiding the damage is called concealment. Playing garbage bits would be a loud burst of noise, so instead the decoder reuses the last good frame’s pitch, voicing and envelope. The question is what to do if the loss goes on.
Why 80 ms? At 30 to 60 km/h, 40% of lost frames come in runs of exactly five: a whole 100 ms radio frame, since the code spreads each radio frame’s bits across all five vocoder frames. When a fade is deep enough to beat the code, the whole radio frame goes.
Could a second CRC on just the most important bits (voicing, pitch and energy) save part of a lost frame? The team checked: a failed frame typically has 12 to 21 wrong bits out of 48, and only 16 to 34% of failed frames had their first 14 bits intact. Losses come from deep fades, not scattered errors, so the extra CRC wouldn’t be worth its bits.
Because sotto is memoryless (chapter 4), a lost frame doesn’t damage the frames after it.
sotto includes a softmodem: a program that does all of this in software. It lets people try sotto without Yaesu hardware, using an FM radio’s 9600-baud data jack and a computer sound card, or a software-defined radio (SDR).
From docs/encoder-v1.md and docs/results/encoder_race.json, with every codec sent through the identical radio chain (scores are PESQ, chapter 7; higher is better; averaged over 3 fading patterns):
| Condition | Codec | Woman | Man (LibriSpeech) | Man (VE9QRP) |
|---|---|---|---|---|
| No radio | Codec 2 2400 | 2.21 | 2.94 | 2.78 |
| sotto v1 | 2.65 | 3.20 | 2.87 | |
| sotto v3.5 | 2.90 | 3.36 | 3.09 | |
| 446 MHz, 60 km/h, weak signal | Codec 2 2400 | 1.69 | 2.30 | 2.48 |
| sotto v1 | 2.27 | 2.78 | 2.61 | |
| sotto v3.5 | 2.47 | 2.97 | 2.94 | |
| 146 MHz, 30 km/h | Codec 2 2400 | 1.82 | 2.06 | 1.98 |
| sotto v1 | 2.26 | 2.65 | 2.20 | |
| sotto v3.5 | 2.58 | 2.92 | 2.60 |
sotto v3.5 beats Codec 2 for every voice in every condition, by 0.3 to 0.9. The slow VHF fades are the hardest, as the fading picture suggests, and that’s where v3.5’s better concealment and quantizer help most: 0.27 to 0.4 better than v1.
You can hear sotto v3.5 against Codec 2 over these channels on the listening page.
modem/c4fm.py: the C4FM modulator and demodulator (sync search, flywheel, soft bits).modem/ysf.py: the frame layout, FICH, Golay code and CRC-16.modem/payload.py: SottoVW, which adds the CRC-8, runs the code, spreads and scrambles.modem/fec.py: the convolutional code, puncturing and Viterbi decoder.modem/link.py: Transmitter and Receiver (which flags failed frames).sim/vocoder_lab.py: SottoV34.decode(), the decoder’s concealment (hold, slope, max_lost); SottoV35 sets the values above.tools/conceal_sweep.py: the concealment experiments.sim/c4fm_sim.py and sim/waveforms.py: the simulated radio channel with noise and fading.docs/softmodem.md, docs/waveform-race.md: details and design choices.