One of the most intersting things I've made. A completely lossless stream of audio over SimTX. Using FEC and OFDM.
- C 90%
- Shell 7.4%
- Makefile 2.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| test | ||
| .gitignore | ||
| AGENTS.md | ||
| decode.sh | ||
| DESIGN.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| receive-stream.sh | ||
| start-stream.sh | ||
| test.stxpreset | ||
400IQ (LARO): Lossless Audio Radio OFDM
Bit-exact, live-streamable audio broadcast over OFDM radio.
Overview
400IQ (Lossless Audio Radio OFDM, or LARO) streams real-time, bit-exact audio over Software Defined Radio (SDR) channels.
The system is designed for lossy, unidirectional radio links:
- Lossless reconstruction: Reconstructs the original 16-bit PCM samples on reception when SNR is at or above 19 dB.
- Independent frames: Every frame decodes on its own without inter-frame prediction. A receiver tuning in mid-broadcast decodes on the next received frame.
- Continuous sample-clock pacing: Direct TCP streaming paces output to the SDR sample rate, transmitting idle zero symbols during pauses to prevent transmitter buffer underruns.
- RTL-TCP frontend: A C client connects directly to remote or local RTL-TCP servers, handles frequency and gain commands, and converts incoming 8-bit I/Q samples to 32-bit floating-point complex samples through a lookup table.
- Jitter buffer: The receiver uses packet loss concealment and frontier re-anchoring to absorb packet jitter and recover from dropouts.
Architecture
flowchart TD
subgraph TX ["Transmitter Pipeline (start-stream.sh)"]
AudioIn["Audio Input<br/>(PulseAudio / PipeWire parec)"] --> LaroTX["build/laro_tx<br/>LPC + Rice Encoder"]
LaroTX -->|"LARO Frames<br/>(CRC-8 + CRC-32)"| OFDM_TX["build/ofdm_tx<br/>OFDM Modulator & Pacer"]
OFDM_TX -->|"complex64 TCP (127.0.0.1:5000)<br/>Clock Pacing"| SimTX["SimTX / SDR Transmitter<br/>(RF 40.0 MHz)"]
end
SimTX ==>|"RF Channel<br/>AWGN, Multipath, Fading"| RTL_TCP["RTL-TCP Server<br/>(argon.simtx.net:1240)"]
subgraph RX ["Receiver Pipeline (decode.sh / receive-stream.sh)"]
RTL_TCP -->|"uint8 interleaved IQ"| RTL_RX["build/rtl_tcp_rx<br/>RTL-TCP Client & LUT Converter"]
RTL_RX -->|"complex64 float32 IQ"| OFDM_RX["build/ofdm_rx<br/>liquid-dsp ofdmflexframesync"]
OFDM_RX -->|"Demodulated LARO Frames"| LaroRX["build/laro_rx --live<br/>Jitter Buffer + PLC + LPC Decoder"]
LaroRX -->|"Signed 16-bit PCM"| AudioOut["PulseAudio / PipeWire Playout<br/>(paplay) or PCM File"]
end
classDef tx fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef rf fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#f8fafc;
classDef rx fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#f8fafc;
class AudioIn,LaroTX,OFDM_TX tx;
class SimTX,RTL_TCP rf;
class RTL_RX,OFDM_RX,LaroRX,AudioOut rx;
Operating Presets
| Preset | Audio Format | Block Size | Subcarriers (M) | Cyclic Prefix (CP) | IQ Sample Rate (Fs) | Bandwidth | Profile |
|---|---|---|---|---|---|---|---|
--stereo (default) |
44.1 kHz Stereo | 960 | 1024 | 128 | 1,200,000 S/s | ~1.17 MHz | Full fidelity CD stereo |
--simtx-200k |
22.05 kHz Mono | 960 | 128 | 16 | 200,000 S/s | ~185 kHz | FM-fidelity audio |
--simtx-124k |
16.0 kHz Mono | 960 | 128 | 16 | 124,000 S/s | ~118 kHz | Low uplink bandwidth (3.97 Mbps wire rate) |
Building and Testing
Prerequisites
- GCC supporting C11, GNU Make, glibc, and libm.
- PulseAudio or PipeWire utilities (
parec,paplay,pactl) for live audio. - No system radio libraries needed:
makefetches and buildsvendor/liquid-dspautomatically.
Compilation
# Build all binaries
make
# Run offline unit tests
make test
Test Suite
make test runs three offline test programs:
loopback_test: Verifies that the LPC compressor, Rice bit-packer, and framer round-trip bit-exact PCM audio across sine waves, noise bursts, DC offsets, and corrupted headers.channel_test: Sweeps simulated bit error rates (10^{-6}to10^{-3}) to confirm that frame CRC checks reject corrupted packets without silent data corruption.resample_test: Validates clock drift absorption under\pm 500ppm frequency skew.
Quick Start
1. Transmit Audio
Stream local audio into SimTX:
# Transmit stereo audio (44.1 kHz, Fs=1.2 MS/s) to SimTX on TCP port 5000:
./start-stream.sh --simtx
# Transmit 16 kHz mono (Fs=124 kS/s):
./start-stream.sh --simtx --simtx-124k
# Transmit over UDP to a local port:
./start-stream.sh --rate 44100 --channels 2
2. Receive and Decode
Receive and decode the radio signal:
# Decode off-air from argon.simtx.net:1240 at 40 MHz and play through default sink:
./decode.sh
# Record decoded PCM audio directly to a file:
./decode.sh --out capture.pcm
# Run the receiver with custom frequency and gain:
./receive-stream.sh --simtx --freq 40000000 --gain 15.0
System Modules
1. Audio Codec (src/codec.c, src/codec.h)
- LPC Order-4 Predictor: Uses a Levinson-Durbin auto-regressive model to predict each sample from preceding samples within the current block.
- Golomb-Rice Coding: Adapts the Rice parameter
kto the residual variance for compact bit-packing. - PCM Fallback: If high-entropy input (such as white noise) causes the compressed residual to exceed the original size, the encoder outputs uncompressed signed 16-bit samples (
flag = 0x01).
2. Frame Format (src/framer.c, src/framer.h)
- Header Structure:
[8-byte Header] - 0x00..0x01: Magic 0x4C 0x51 ('LQ') - 0x02: Stream Generation ID - 0x03: Flags (0x80 = Stream Header, 0x01 = Raw Fallback) - 0x04..0x05: Sequence Number (uint16 big-endian) - 0x06: Payload Length (uint8) - 0x07: Header CRC-8 [Payload: N bytes] [4-byte Payload CRC-32 (IEEE 802.3)] - Stream Headers: Transmitted roughly once per second. They broadcast audio parameters (sample rate, channels, block size), allowing new listeners to synchronize quickly.
3. OFDM Physical Layer (src/ofdm_tx.c, src/ofdm_rx.c)
- Modulation: Uses
liquid-dsp'sofdmflexframegenerator and synchronizer with configurable FFT sizes (M=128to1024) and cyclic prefix lengths (CP). - Constellations: Supports 64-QAM (default), 16-QAM, QPSK, and BPSK.
- Clock Pacing:
ofdm_txpaces output using microsecond timers (--fs RATE). Between audio bursts, it sends zero symbols to keep transmitter buffers from draining.
4. RTL-TCP SDR Receiver (src/rtl_tcp_rx.c)
- Network Ingest: Connects to RTL-TCP servers (such as
argon.simtx.net:1240) over TCP. - Tuner Control: Issues 5-byte command packets to set frequency (
0x01), sample rate (0x02), tuner gain (0x04), and AGC state (0x08). - Float Conversion: Converts incoming unsigned 8-bit I/Q sample pairs to float32 values in
[-1.0, +1.0]using a 256-entry lookup table.
5. Jitter Buffer (src/laro_rx.c, src/jitbuf.c)
- Ring Buffer: Reorders frames that arrive out of order and smooths network jitter.
- Loss Concealment: If an audio frame is lost or fails its CRC check, the buffer repeats the previous block to keep playback continuous.
- Frontier Re-anchoring: Recovers from multi-second signal dropouts by advancing directly to the newest sequence number.
Repository Layout
400iq/
├── Makefile # C11 build rules (gcc and make)
├── decode.sh # Default 40 MHz off-air decoder
├── receive-stream.sh # Receive pipeline script (RTL-TCP, UDP, file)
├── start-stream.sh # Transmit pipeline script (parec to SimTX)
├── test.stxpreset # SimTX RF and antenna configuration preset
├── DESIGN.md # Detailed format specification and link budget
├── HANDOFF.md # Dated project status and change log
├── AGENTS.md # Development instructions and project rules
├── src/
│ ├── codec.c / .h # LPC-4 and Golomb-Rice lossless audio codec
│ ├── framer.c / .h # Frame structure, CRC-8, and CRC-32
│ ├── jitbuf.c / .h # Receiver ring buffer and loss concealment
│ ├── laro_tx.c # PCM ingest and frame pack frontend
│ ├── laro_rx.c # Frame deframer and audio playout frontend
│ ├── ofdm_tx.c # OFDM modulation, symbol pacing, and TCP streaming
│ ├── ofdm_rx.c # OFDM frame synchronizer and payload extraction
│ └── rtl_tcp_rx.c # Standalone RTL-TCP client and converter
├── test/
│ ├── loopback_test.c # In-memory bit-exact codec verification
│ ├── channel_test.c # Simulated BER channel sweep and CRC test
│ ├── channel_sim.c # Software RF channel simulator (AWGN and fading)
│ └── resample_test.c # Clock skew adaptation tests
└── vendor/
└── liquid-dsp/ # Physical-layer DSP library (auto-fetched)
Link Performance
AWGN channel simulation results using build/channel_sim (64-QAM, block 960):
SNR Sweep (64-QAM, Block 960, Fs = 1.2 MS/s):
-----------------------------------------------------------------------------
SNR >= 19.0 dB : 101/101 frames OK (100% bit-exact, 0 concealed)
SNR = 18.5 dB : 100/101 frames OK (1 concealed)
SNR = 18.0 dB : 98/101 frames OK (3 concealed)
SNR = 17.0 dB : 73/101 frames OK (Waterfall knee)
SNR <= 15.0 dB : Muted; headers only
-----------------------------------------------------------------------------