Three bugs made the AM path sound wrong:
1. AGC attack too fast (5 ms). The slowest audio a broadcast AM station
can transmit is ~50 Hz (20 ms period). A 5 ms attack lets the AGC track
individual audio cycles, which causes severe pumping and amplitude
distortion. Change to 500 ms attack / 5 s release so the AGC only
responds to slow carrier-amplitude fading, not the audio modulation itself.
2. Bandwidth too narrow. The IQ filter cutoff is audio_bandwidth_hz / 2,
so the previous 6 000 Hz setting gave only 3 kHz audio bandwidth.
AM broadcast sidebands extend to ±4.5–5 kHz; raise the default to
12 000 (cutoff 6 kHz) to cover the full audio band.
3. DC blocker rate inconsistent. For AM the demodulated magnitude is
always ≥ 0 and the DC component equals the carrier amplitude; only true
DC needs removing. Unify all non-WFM modes to r = 0.9999 (corner
≈ 0.76 Hz @ 48 kHz), which strips carrier DC without touching any
audible bass content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
CW signals in SDR are centred at an audio offset (e.g. 700 Hz) by the
upstream FIR filter, so demodulating as USB (taking the real part) produces
the correct side-tone. The previous magnitude/envelope approach produced a
DC pulse per key press with no audible tone.
Re-enable the DC blocker for CW/CWR (r = 0.9999): the output is now audio
that can carry a DC offset from BFO frequency error, identical to USB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SoftAgc — a fast-attack/slow-release envelope AGC with a max-gain cap
— to all demodulated audio paths so that switching between modes (WFM, AM,
SSB, CW, FM) no longer produces large volume jumps. AGC is applied after
every demodulator, including WFM, with a shared target level of 0.5.
Add per-mode DC blocking (DcBlocker) for USB/LSB/AM/FM/DIG paths to remove
carrier frequency-offset DC from the FM discriminator and LO bleedthrough in
SSB. CW is excluded because high-passing a non-negative envelope creates
negative-going artifacts on each key release; WFM already has internal DC
blockers on each output channel.
AGC time constants are tuned per mode:
CW/CWR – 1 ms attack / 50 ms release (follows individual dots/dashes)
AM – 5 ms attack / 200 ms release (tracks fading carriers)
all else– 5 ms attack / 500 ms release (suits voice and data)
Simplify demod_am and demod_cw: remove per-block peak normalisation and DC
removal that caused block-boundary level discontinuities ("pumping"). Both
now return raw magnitudes and rely on the downstream DC blocker and AGC for
normalisation.
DIG is already wired as Passthrough (identical to USB); no change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace one-pole sum/diff filters with a proper 4th-order Butterworth
cascade (Q = 0.5412 / 1.3066) at 15 kHz. This reduces pilot tone
leakage from −4 dB to −12 dB at 19 kHz and suppresses the 38 kHz DSB
carrier from −9 dB to −32 dB, significantly improving stereo crosstalk.
Add a biquad notch at 19 kHz on the L+R channel to eliminate the residual
pilot tone that would otherwise be audible after downsampling to 48 kHz.
Replace nearest-neighbor (sample-hold) resampling with linear interpolation
inside WfmStereoDecoder. The output sample is now placed at the exact
fractional position between the two adjacent composite samples using the
phase accumulator state, removing timing jitter and harmonic distortion on
sustained tones.
Add DC blockers (pole at 0.9999, corner ≈ 0.75 Hz at 48 kHz) to all audio
outputs to remove carrier frequency-offset DC from the FM discriminator
without any audible bass roll-off.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Drop the now-unused rigctl_port local after removing\nthe shared rigctl listener path.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove the shared rigctl listener path so rigctl only\nruns as per-rig listeners configured through\nfrontends.rigctl.rig_ports.\n\nTighten client config validation to require at least one\nper-rig rigctl port when the rigctl frontend is enabled.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make the primary SoapySDR DSP channel follow the tuned\nfrequency so RDS decoding stays aligned with the active\nfrequency rather than the hardware center.\n\nMove the default WFM deemphasis setting to server SDR\nconfig and default it to 50 us, then pass that value into\nthe SoapySDR backend.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add server-side debug log when RDS data is decoded (PI, PS, PTY).
Extend the RDS panel with active mode, frame counter, and a raw JSON
dump of the last spectrum frame (bins excluded) to help diagnose why
RDS remains absent from the SSE stream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Draw small peak markers on strong visible spectrum maxima\nso snap-tune targets are easier to spot.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Apply the same explicit height and padding rules to the\nAuto BW button as the Set button in the spectrum\ncontrols, including the mobile layout.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an RDS sub-tab to the Plugins panel showing PI code, PS name, PTY
number and name, decoder status, and a raw JSON dump of the latest RDS
data received via SSE. Also list the RDS decoder in the Overview tab.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Include the snapped peak signal level in the spectrum\nhover tooltip alongside the peak frequency.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The FIR LPF cutoff was audio_bandwidth_hz/2; with wfm_bandwidth_hz=75000
this gave 37.5 kHz, stripping the 57 kHz RDS subcarrier before FM
demodulation. Clamp the IQ filter bandwidth to at least 130 kHz (cutoff
≥ 65 kHz) for WFM so the RDS subcarrier always reaches the decoder,
regardless of the configured audio bandwidth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an Auto BW control that estimates a suitable\nreceive bandwidth from the live spectrum around the\ncurrently tuned peak and applies it to the server.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Update the local tuned-frequency state immediately after\nsuccessful set_freq requests so the marker and display stay\nin sync with click-to-tune, manual entry, and jog tuning.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show snapped peak frequencies in the spectrum hover tooltip\nand move the bandwidth label to the bottom of the tuned\nfrequency marker.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Improve spectrum click-to-tune by snapping the selected\nfrequency to a nearby dominant local peak, making signals\neasier to select.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
In multi-rig mode, each RigInstanceConfig.audio.listen defaulted to
127.0.0.1 independently of the global [audio] listen setting, causing
per-rig audio ports to bind to localhost only and refuse connections
from remote clients.
Fix by passing cli.listen.or(Some(cfg.audio.listen)) as the listen
override, so the global address is always the fallback when --listen
is not supplied on the CLI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>