Commit Graph

295 Commits

Author SHA1 Message Date
sjg 617255cd32 [feat](trx-rs): expose live sdr gain control
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 23:30:08 +01:00
sjg 93e507606e [feat](trx-server,trx-backend-soapysdr): add sdr gain ceiling
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 23:23:41 +01:00
sjg dbeec9fb39 [feat](trx-backend-soapysdr): use fir resampler for wfm audio
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 23:17:15 +01:00
sjg 5a6ff1427a [fix](trx-client,trx-backend-soapysdr): improve rig switching and fm agc
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 23:06:53 +01:00
sjg 8440fe164b [fix](trx-backend-soapysdr): reduce wfm audio cutoff
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 22:54:03 +01:00
sjg 735c9a7f07 [fix](trx-backend-soapysdr): bypass agc for wfm audio
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 22:38:54 +01:00
sjg 3ccda5fb5a [fix](trx-backend-soapysdr,trx-frontend-http): reset stereo state and soften wfm top end
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 22:34:54 +01:00
sjg b6053729a4 [fix](trx-backend-soapysdr): emit proper stereo pcm frames
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 22:07:34 +01:00
sjg df1bbf8f5b [fix](trx-rs): remove wfm denoise and default stereo audio
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 22:01:17 +01:00
sjg d6c89bcc6b [fix](trx-backend-soapysdr): tighten wfm pilot detect and stereo agc
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 21:51:36 +01:00
sjg b722787ada [feat](trx-rs): show live wfm stereo detect state
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 21:44:32 +01:00
sjg 862f0200bb [fix](trx-backend-soapysdr): tune wfm stereo separation trim
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 21:39:35 +01:00
sjg 73fe9d52ff [fix](trx-backend-soapysdr): improve wfm stereo separation
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 21:34:31 +01:00
sjg a456864957 [fix](trx-backend-soapysdr): restore wfm stereo separation
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 21:26:32 +01:00
sjg eb740dbb43 [fix](trx-backend-soapysdr): add missing wfm_denoise arg in dsp tests
Fix pre-existing compilation failures in four test call sites that were
missing the wfm_denoise: bool argument added to ChannelDsp::new() and
SdrPipeline::start().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 20:43:10 +01:00
sjg bbc7e857e5 [fix](trx-backend-soapysdr): improve WFM stereo demodulation quality
- Raise audio LPF cutoff from 15 kHz to 17 kHz to pass full FM stereo
  audio bandwidth without excessive HF rolloff
- Replace 2-point linear interpolation resampler with 4-point Hermite
  cubic spline for a much flatter passband up to 17 kHz
- Add FM discriminator gain normalization (fm_gain = fs / 150000) so
  ±75 kHz deviation maps to ±1.0 regardless of composite sample rate,
  stabilizing stereo carrier amplitude reconstruction
- Double pilot PLL proportional (0.0015→0.003) and integral
  (0.00002→0.00005) gains for faster lock and better tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 20:43:01 +01:00
sjg 3d8fd32488 [fix](trx-backend-soapysdr): prevent double agc on mono wfm
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 20:30:12 +01:00
sjg 244f91d97b [fix](trx-backend-soapysdr,trx-frontend-http): make wfm mono mode real
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 20:25:04 +01:00
sjg b7c1da138b [fix](trx-backend-soapysdr): fix WFM stereo separation
The 19 kHz pilot notch was applied only to the L+R sum path, introducing
~22° of phase shift at 15 kHz relative to the L-R diff path.  This phase
mismatch caused interchannel crosstalk (≈ −14 dB separation at 15 kHz).

Fix: remove the notch from the sum processing chain so both sum and diff
pass through identical 4th-order Butterworth LPFs, giving phase-coherent
demodulation across the full audio band.  The notch is relocated to the
mono output branch where phase alignment with the diff channel is not
required.  Pilot rejection on the stereo L/R outputs is still adequate
(~28 dB) from the combined LPF + deemphasis response at 19 kHz.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 20:11:15 +01:00
sjg 95716a0fc3 [feat](trx-rs): expose WFM stereo denoise toggle
Add a server-side toggle for the multiband stereo denoiser so it can be
enabled or disabled at runtime without restarting the server.

Backend (trx-backend-soapysdr):
- Add `denoise_enabled: bool` to `WfmStereoDecoder`; gate multiband
  blend behind it (falls back to uniform single-band blend when off)
- Add `set_denoise_enabled()` method on `WfmStereoDecoder`
- Propagate `wfm_denoise: bool` through `ChannelDsp`, `SdrPipeline`,
  and `SoapySdrRig`; add `set_wfm_denoise()` at each layer
- Include `wfm_denoise` in `filter_state()` so it flows into snapshots

Protocol / core (trx-core, trx-protocol, trx-server):
- Add `SetWfmDenoise(bool)` to `RigCommand` and `ClientCommand`
- Add default `set_wfm_denoise()` trait method to `RigCat`
- Handle `SetWfmDenoise` in `rig_task.rs` and update `RigFilterState`
- Add `wfm_denoise: bool` (default `true`) to `RigFilterState`

Frontend (trx-frontend-http):
- Add `POST /toggle_wfm_denoise` endpoint
- Add "Denoise On/Off" button next to the stereo/mono audio picker
- Sync button state from SSE filter snapshot (`update.filter.wfm_denoise`)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 19:47:36 +01:00
sjg ffdc193671 [feat](trx-backend-soapysdr): add multiband stereo denoising for WFM
Split the L-R diff channel into three frequency bands at audio rate and
apply SNR-weighted blending per band driven by pilot magnitude:

  - 0–2 kHz:   blend¹  (most stereo — low frequencies have best SNR)
  - 2–8 kHz:   blend²  (moderate noise reduction)
  - 8–15 kHz:  blend⁴  (aggressive noise reduction — hiss-prone range)

Move blend from composite rate to audio rate so the crossover filters
(2nd-order Butterworth at 2 kHz and 8 kHz) operate at 48 kHz and the
pilot blend is linearly interpolated per audio sample for smooth
transitions. Unblended diff is now stored in prev_diff; prev_blend
tracks the blend value for the same interpolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 17:16:46 +01:00
sjg 332ad4448b [fix](trx-backend-soapysdr): fix AM demodulation quality
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>
2026-02-28 17:00:32 +01:00
sjg 1f9c09668d [fix](trx-backend-soapysdr): demodulate CW as USB (real part, not envelope)
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>
2026-02-28 16:25:53 +01:00
sjg ba9881ff62 [feat](trx-backend-soapysdr): add AGC and DC blocking to all demod modes
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>
2026-02-28 16:04:26 +01:00
sjg 6dc26e48a3 [feat](trx-backend-soapysdr): improve WFM audio quality
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>
2026-02-28 15:46:56 +01:00
sjg b172ace0ee [feat](trx-rds,trx-backend-soapysdr): condition weak-signal rds updates
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:52:31 +01:00
sjg 5a27cb634d [feat](trx-backend-soapysdr): prefilter rds subcarrier
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:41:19 +01:00
sjg 017b7be8a8 [fix](trx-rds,trx-backend-soapysdr,trx-frontend-http): relax rds filtering
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:26:11 +01:00
sjg 2ed68e4210 [feat](trx-rds,trx-backend-soapysdr): improve weak-signal rds recovery
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:19:36 +01:00
sjg 8f7afed132 [fix](trx-backend-soapysdr,trx-frontend-http): keep rds state on bw changes
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:13:06 +01:00
sjg adadcb4a77 [fix](trx-backend-soapysdr,trx-frontend-http): preserve raw rds ps text
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 12:01:27 +01:00
sjg 4b5dd36778 [feat](trx-server,trx-client): raise default audio opus bitrate
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 11:33:52 +01:00
sjg b0bf9e3ee0 [feat](trx-rds,trx-frontend-http): reset rds on tune changes
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 11:31:42 +01:00
sjg e886f97eb9 [fix](trx-backend-soapysdr): back off after stream overruns
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 11:24:09 +01:00
sjg 6f09022563 [fix](trx-rs): sync sdr rds tuning and deemphasis default
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>
2026-02-28 11:04:40 +01:00
sjg 855d21fd8a [debug](trx-backend-soapysdr,trx-frontend-http): add RDS diagnostics
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>
2026-02-28 09:31:24 +01:00
sjg d5b1b6f020 [fix](trx-backend-soapysdr): widen WFM IQ filter to preserve RDS subcarrier
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>
2026-02-28 09:10:14 +01:00
sjg 3f29ba3db8 [fix](trx-server): inherit global [audio].listen for per-rig audio listeners
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>
2026-02-28 08:59:15 +01:00
sjg 2f2a74b810 [chore](trx-app): drop per-binary config file support
Only trx-rs.toml with [trx-server] / [trx-client] section headers is
now supported. Simplify ConfigFile trait to a single required method
section_key(); remove config_filename(), combined_key(), and
default_search_paths(). load_from_file() now errors when the expected
section is absent rather than falling back to flat parsing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 08:45:32 +01:00
sjg b5a1b4af33 [feat](trx-frontend-rigctl): per-rig rigctl listeners for multi-rig setups
Add rig_ports map to RigctlFrontendConfig. When non-empty, one rigctl
TCP listener is spawned per entry instead of the single shared listener,
each routing commands to its assigned rig via rig_id_override on RigRequest.

Add rig_id_override: Option<String> to RigRequest so the remote client
can route individual requests to a specific rig without changing the
globally selected rig. build_envelope prefers the override when set.

Example config:
  [frontends.rigctl]
  enabled = true
  listen = "127.0.0.1"
  port = 4532
  rig_ports.ft817 = 4532
  rig_ports.airspyhf = 4533

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 08:41:39 +01:00
sjg 7a4c2d52b1 [chore](trx-rs): drop legacy ~/.trx-{server,client}.toml search paths
Remove the home-directory dotfile paths that predate the XDG layout.
Search order is now: CWD → ~/.config/trx-rs/ → /etc/trx-rs/, checked
for both the combined trx-rs.toml and the per-binary flat file at each
tier.

Update doc comments and tests accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 08:31:21 +01:00
sjg d8f030667f [chore](trx-rs): remove unused example_toml() methods
Superseded by example_combined_toml() which now covers all use cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 08:13:00 +01:00
sjg 06312abe42 [feat](trx-app): support combined trx-rs.toml config file
Both trx-server and trx-client now look for a combined trx-rs.toml
with [trx-server] and [trx-client] section headers respectively,
falling back to per-binary config files as before.

Search order per tier: combined trx-rs.toml → flat per-binary file,
checked in CWD, ~/.config/trx-rs/, and /etc/trx-rs/.

--print-config now outputs the config under the appropriate section
header so the combined file can be generated directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 08:08:45 +01:00
sjg 6a47fb00ad [fix](trx-frontend-http,trx-backend-soapysdr): fix audio rate and stereo copy
JS: fix stereo AudioData channel copy — frame.copyTo with planeIndex:0
only fills the left plane; reading it as interleaved data caused every
other sample to be zero, making WFM stereo play at half speed.  Now
calls copyTo per channel with the correct planeIndex.

Rust WFM: replace integer output_decim/output_counter in WfmStereoDecoder
with a fractional phase accumulator (output_phase_inc = audio_rate /
composite_rate).  Integer division caused the effective output rate to
drift from audio_sample_rate when the SDR rate is not an exact multiple
(e.g. 2 MHz SDR → 250 kHz composite → ~50 kHz output instead of 48 kHz,
making audio play 4% slow).

Rust non-WFM: add resample_phase/resample_phase_inc to ChannelDsp and
use a fractional-phase resampler in process_block for non-WFM paths,
ensuring exactly audio_sample_rate samples/sec regardless of SDR rate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 07:57:42 +01:00
sjg 4f8658b773 [fix](trx-frontend-http): fix waterfall freeze, RDS bandwidth, add jog multiplier
Fix waterfall overview freezing at steady state by tracking a monotonic
push counter instead of row array length — the array size stays constant
once the waterfall is full, so the previous row-count comparison never
triggered the incremental draw path.

Fix WFM RDS not decoding when switching to WFM from a narrowband mode:
set_mode now resets audio_bandwidth_hz to the mode-appropriate default
(180 kHz for WFM) before rebuilding the FIR, preventing the 57 kHz RDS
subcarrier from being filtered out.

Add 1×/10×/100× multiplier button group next to the jog unit selector.
jogUnit × jogMult gives the effective jog step; both are persisted to
localStorage independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 02:25:39 +01:00
sjg 4c4d14b705 [feat](trx-rs): refine overview strip and logging
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-28 00:57:15 +01:00
sjg fffc4c6b90 [feat](trx-rs): add WFM RDS and playback controls
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-27 23:57:46 +01:00
sjg e392e7ffa5 [feat](trx-backend-soapysdr): decode WFM stereo in SDR pipeline
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-27 23:40:25 +01:00
sjg c5f1c5308b [fix](trx-server): duplicate mono SDR frames for stereo opus
Signed-off-by: Stan Grams <sjg@haxx.space>
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-02-27 23:29:23 +01:00
sjg 7f222eaf10 [chore](trx-rs): refine pending SDR frontend and backend changes
Signed-off-by: Stan Grams <sjg@haxx.space>
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-02-27 23:09:21 +01:00