sdroxide-brown

C-QUAM (AM stereo) decoding — design note

A proposal for decoding Motorola C-QUAM AM stereo on the medium-wave broadcast band, as a receive-only SWL feature of this fork. Written before any code; the aim is to fix the signal model, the integration points and the test plan, and to name the risks while they are still cheap to change.

Status: implemented. Mode::Cquam (receive-only) exists, with the decoder in crates/sdroxide-dsp/src/demod.rs and synthetic round-trip, pilot and carrier-inversion tests that pass. It is unverified against a real signal until the 918 kHz capture below is decoded, so the blend thresholds and the sign conventions are still first-cut.

Scope

The signal, briefly

C-QUAM keeps an ordinary AM envelope (so a diode detector hears mono) and hides the stereo difference in the carrier phase:

s(t) = [1 + (L + R)] · cos(ω_c t + φ(t))
φ(t) ∝ (L − R) plus a 25 Hz pilot term

So a C-QUAM receiver is: carrier recovery, in-phase = L + R, quadrature = L − R + pilot, a 25 Hz pilot PLL for the sense, and a matrix.

Why this fits the codebase

Two of the three hard parts already exist, which is what makes this a small feature rather than a project:

Integration points

Decoder design

Blocks, in order:

  1. Complex bandpass, as SamDemod does (PASSBAND_TAPS), ±~5 kHz about the carrier — narrow enough to reject the adjacent channel without clipping the L − R sidebands.
  2. Carrier PLL — the existing SamDemod loop. Rotate by e^{−jθ}; the real part I is L + R, the imaginary Q is the difference plus pilot.
  3. DC block on I — the carrier term, as SamDemod already does.
  4. 25 Hz pilot recovery from Q — a narrow band-pass plus a PLL, mirroring PilotPll: track frequency and phase, report locked() and a blend() figure from lock quality (the way WFM derives its blend from pilot SNR).
  5. L − R — multiply Q by the recovered pilot reference (giving the correct sense) and low-pass to the audio band. A degree of phase alignment matters: the pilot reference, not Q alone, is what guarantees L is L.
  6. Matrix and blend — L = M + S, R = M − S, with M = (L + R)/2 (post-DC) and S = (L − R)/2. Blend S toward zero as pilot lock falls, so a weak or fading signal falls back to a clean mono rather than a wrong stereo.
  7. Stereo output — via the trait’s stereo_diff; stereo_locked from the pilot PLL, exactly as WFM does.

Parameters to pick and document: audio low-pass corner (~5 kHz), pilot loop natural frequency (a few Hz) and damping, lock/blend thresholds and time constants, and the L − R low-pass.

Testing

The synthesised-signal route covers nearly everything without a recording:

Real-signal validation

A real signal is available: 918 kHz, receivable between 10:00 and 20:00 local. That is daytime, so it is a groundwave path and should be stable — the easier case, not the night-time fading one — which makes it a good fixture for tuning the decoder and the blend.

Capture it as raw IQ, not demodulated audio: the stereo lives in the carrier’s phase, and an AM or WFM audio recording throws exactly that away. sdroxide already records the format it can replay (--record-iq writes raw interleaved CF32; REC on the top strip does the same):

# Once the mode exists. Tune to 918 kHz, record 30-60 s, and replay it.
sdroxide --record-iq 918k.cf32            # or press REC
sdroxide --file 918k.cf32 --rate <captured> --freq 918000 --mode Cquam

Record at the front end’s own rate — a couple of hundred kHz about 918 kHz is plenty — and long enough to cover a stereo passage and a mono one, so the mono/stereo switch can be heard as well as tested. The file stays a local fixture: a broadcast recording is the station’s copyright, so it is not committed. The repository keeps the synthetic tests; the capture is what turns “unverified against real signals” into “verified” on the developer’s machine.

Open questions

Effort

A focused feature: a new demodulator (~200–300 lines) plus a pilot PLL (~100), a small amount of mode plumbing, a UI lamp, and the test module and its synthetic encoder (~150). One short branch. The main cost is the real-signal validation, not the code.