9.3 KiB
Signal Flow
This document describes the complete data path from SDR hardware input to audio output.
Overview
CubicSDR processes radio signals through a pipeline of thread-connected stages. Each stage runs in its own thread and communicates with adjacent stages via ThreadBlockingQueue<T> instances.
Signal Pipeline
SDR Hardware (SoapySDR)
|
v
[Stage 1] SDRThread
| SDRThreadIQDataQueue (max 100 items)
v
[Stage 2] SDRPostThread
|---[pipeIQVisualData]-----> SpectrumVisualDataThread (main spectrum)
|---[pipeWaterfallIQVisualData]--> FFTVisualDataThread (waterfall)
|---[pipeDemodIQVisualData]--> SpectrumVisualDataThread (demod spectrum + demod waterfall)
|---[per-demod pipeIQInputData]--> DemodulatorPreThread
v
[Stage 3] DemodulatorPreThread
| DemodulatorThreadPostInputQueue (max 100 items)
v
[Stage 4] DemodulatorThread
|---[pipeAudioData]---------> AudioThread (speaker output)
|---[audioVisOutputQueue]---> ScopeVisualProcessor (scope display)
|---[audioSinkOutputQueue]--> AudioSinkFileThread (WAV recording)
v
[Stage 5] AudioThread -> RtAudio hardware callback -> speakers/headphones
Stage Details
Stage 1: SDR Hardware Input
Class: SDRThread (src/sdr/SoapySDRThread.h)
Reads IQ samples from SoapySDR hardware in a tight loop. Samples are liquid_float_complex (float I/Q pairs). The thread manages:
- Device open/close
- Sample rate and frequency configuration
- Gain settings (per-channel)
- DC offset correction
- Streaming start/stop
Output: SDRThreadIQData containing raw IQ samples, frequency, sample rate, and DC correction status.
Stage 2: Post-Processing / Channelization
Class: SDRPostThread (src/sdr/SDRPostThread.h)
Receives raw IQ from SDRThread. Behavior depends on channel count:
Multi-channel mode (numChannels > 1):
- Polyphase filterbank channelization (
firpfbch_crcforfirpfbch2_crcf) — splits wideband IQ into N sub-channels - DC blocking on channel 0 only via IIR filter (
iirfilt_crcf) — other channels pass through unfiltered - Distribution — non-blocking
try_push()each channel's data to its assigned demodulator's input queue
Single-channel mode (numChannels <= 1):
- DC blocking on the entire bandwidth via IIR filter (
iirfilt_crcf) - Distribution — non-blocking
try_push()the full bandwidth to every active demodulator (no channelization)
In both modes, also pushes full-rate IQ (DC-corrected in single-channel, raw in multi-channel) to visual processing queues for the main spectrum and waterfall displays. All output pushes use non-blocking try_push() — both visual data and per-demodulator data can be silently dropped when queues are full. SDRPostThread never blocks on any consumer.
Stage 3: Demodulator Pre-Processing
Class: DemodulatorPreThread (src/demod/DemodulatorPreThread.h)
Per-demodulator preprocessing:
- Frequency shifting — centers the desired signal using
nco_crcf - IQ resampling — decimates to the modem's bandwidth using
msresamp_crcf - Modem attachment — bundles
Modem*andModemKit*pointers with each IQ packet
When the modem type changes, a DemodulatorWorkerThread builds the new modem and filters on a background thread to avoid blocking the data flow. The worker thread communicates via its own command queue (max 2) and result queue (max 100).
Output uses blocking push() — if the downstream queue is full, the pre-thread stalls until space is available.
Stage 4: Demodulation
Class: DemodulatorThread (src/demod/DemodulatorThread.h)
Core demodulation execution:
- Calls
Modem::demodulate(kit, input, audioOut)— the virtual dispatch point - Computes signal level (mean magnitude, dB)
- Applies squelch logic
- Routes audio to output queues and visualization
All output pushes use non-blocking try_push() — if any output queue is full, data is dropped with a warning. This prevents a slow consumer (e.g. audio device) from stalling the demodulator.
Stage 5: Audio Output
Class: AudioThread (src/audio/AudioThread.h)
Manages RtAudio hardware output using a controller/bound pattern:
- One AudioThread per physical output device acts as controller (owns the RtAudio stream)
- Other AudioThreads (one per demodulator) are bound to the controller
- A file-scope static
audioCallbackfunction (real-time audio thread) receives the controller AudioThread asuserDataand mixes audio from all bound threads
Queue Wiring
Global Queues (set up in CubicSDR::OnInit())
| Queue Name | Type | Producer | Consumer | Max |
|---|---|---|---|---|
pipeSDRIQData |
SDRThreadIQDataQueue |
SDRThread | SDRPostThread | 100 |
pipeIQVisualData |
DemodulatorThreadInputQueue |
SDRPostThread | SpectrumVisualDataThread (main spectrum) | 1 |
pipeWaterfallIQVisualData |
DemodulatorThreadInputQueue |
SDRPostThread | FFTVisualDataThread (waterfall) | 128 |
pipeDemodIQVisualData |
DemodulatorThreadInputQueue |
SDRPostThread | SpectrumVisualDataThread (demod spectrum) | 1 |
pipeAudioVisualData |
DemodulatorThreadOutputQueue |
DemodulatorThread | ScopeVisualProcessor | 1 |
Per-Demodulator Queues (set up in DemodulatorInstance constructor)
| Queue Name | Type | Producer | Consumer | Max |
|---|---|---|---|---|
pipeIQInputData |
DemodulatorThreadInputQueue |
SDRPostThread | DemodulatorPreThread | 100 |
pipeIQDemodData |
DemodulatorThreadPostInputQueue |
DemodulatorPreThread | DemodulatorThread | 100 |
pipeAudioData |
AudioThreadInputQueue |
DemodulatorThread | AudioThread | 100 |
audioVisOutputQueue |
DemodulatorThreadOutputQueue |
DemodulatorThread | ScopeVisualProcessor | default |
audioSinkOutputQueue |
DemodulatorThreadOutputQueue |
DemodulatorThread | AudioSinkFileThread | default |
Note: audioVisOutputQueue is a per-DemodulatorThread member that is bound at runtime to the global pipeAudioVisualData queue via setOutputQueue("AudioVisualOutput", ...). audioSinkOutputQueue is bound dynamically only when WAV recording starts.
Note: DemodulatorThreadOutputQueue and AudioThreadInputQueue are both aliases for ThreadBlockingQueue<AudioThreadInputPtr> (defined in src/audio/AudioThread.h). They carry the same data type; the distinct names reflect the queue's role in the pipeline.
Buffer Management
ReBuffer Pool
To avoid heap allocation on every frame, CubicSDR uses ReBuffer<T> (src/IOThread.h) — a shared_ptr-based buffer pool:
getBuffer()scans for a buffer withuse_count == 1(not in transit), resets its age, and returns it; if none available, allocates a new one- When the last consumer releases a buffer, its
use_countdrops to 1 and it becomes available for reuse - Idle buffers age out: each time a buffer is selected, other unused buffers have their age decremented. When the oldest buffer's age drops below
-REBUFFER_GC_LIMIT(i.e. age < -100), it is garbage collected - A warning is emitted if the pool exceeds
REBUFFER_WARNING_THRESHOLD(2000) buffers
Used by: SDRThread, SDRPostThread, DemodulatorPreThread, DemodulatorThread, SpectrumVisualProcessor, ScopeVisualProcessor.
Data Types
| Type | File | Contents |
|---|---|---|
SDRThreadIQData |
src/sdr/SoapySDRThread.h |
Raw IQ samples, frequency, sample rate, DC correction flag, channel count |
DemodulatorThreadIQData |
src/demod/DemodDefs.h |
Channelized IQ samples, frequency, sample rate |
DemodulatorThreadPostIQData |
src/demod/DemodDefs.h |
Resampled IQ, sample rate, modem name/type, Modem* + ModemKit* pointers |
AudioThreadInput |
src/audio/AudioThread.h |
Float audio data, frequency, input rate, sample rate, channels, peak level, type, squelch flag |
SpectrumVisualData |
src/process/SpectrumVisualProcessor.h |
FFT spectrum points, hold points, floor/ceiling, center frequency, bandwidth |
ScopeRenderData |
src/process/ScopeVisualProcessor.h |
Waveform points, scope mode, input/sample rate, channels, spectrum flag, FFT size/floor/ceiling |
Visual Processing Pipeline
A parallel path handles display data:
SDRPostThread
|
+--[pipeIQVisualData]--> SpectrumVisualDataThread --> SpectrumCanvas (UI thread)
+--[pipeWaterfallIQVisualData]--> FFTVisualDataThread --> WaterfallCanvas
+--[pipeDemodIQVisualData]--> SpectrumVisualDataThread --> demodSpectrumCanvas + demodWaterfallCanvas
DemodulatorThread
|
+--[audioVisOutputQueue]--> ScopeVisualProcessor --> ScopeCanvas (UI thread)
FFTVisualDataThread contains an internal sub-pipeline: an FFTDataDistributor accumulates raw IQ samples into FFT-sized chunks, which are then processed by a SpectrumVisualProcessor to produce FFT output. The FFTDataDistributor rate-limits by linesPerSecond, so not every incoming IQ frame produces an output line.
The UI thread is pull-based on the consumer side — canvases poll their input queues via try_pop(), though the exact trigger varies: WaterfallCanvas does this in its OnIdle() handler, while SpectrumCanvas and ScopeCanvas do it in OnPaint(). Meanwhile, producer threads push data into these queues via non-blocking try_push(), which can silently drop data when queues are full. This is intentional for visual data where occasional dropped frames are acceptable.