[fix](trx-rds): tune RDS parameters for maximum sensitivity

- RRC_ALPHA 0.75→0.50: narrower noise BW, ~0.6 dB SNR gain
- COSTAS_KI 3.5e-7: maintain ζ≈0.68 (1e-6 caused loop instability)
- Soft confidence: use biphase_i.abs() instead of full vector magnitude
  so OSD confidence is aligned with bit-decision sign; suppresses
  false groups under noise with residual Costas phase error
- OSD(2) in locked mode: corrects ≤2-bit errors after block sync
- Search mode: hard decode only for Block A; OSD(1) in search yielded
  ~13% false Block A rate per bit, letting wrong clock candidates
  accumulate false groups as fast as the correct candidate
- Incumbent candidate tracking (best_candidate_idx): the winning
  candidate updates best_state at equal score; challengers need strictly
  higher score; best_score tracks incumbent even on no-state-change
  groups so challengers can't leapfrog on a single false group
- blocks_to_chips: add NRZI (NRZ-Mark) pre-encoding so the differential
  biphase decoder recovers actual data bits rather than XOR-of-pairs
- Add blocks_to_chips_round_trips_all_groups test: verifies all 16 blocks
  across 4 PS segments round-trip correctly without BPSK modulation

[fix](trx-backend-soapysdr): lower pilot lock threshold for weak-signal RDS

- PILOT_LOCK_THRESHOLD 0.25→0.20, add PILOT_LOCK_ONSET=0.30 constant
- Pilot reference engages at coherence ≥0.36 (was ≥0.45)

WIP: end_to_end_clean_signal_decodes_ps still failing (13/15 pass).
Decoder skips segment 2 due to ISI from rectangular test chips through
RRC receive filter. chips_to_rds_signal needs RRC pulse shaping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-27 01:46:01 +01:00
parent 8b310c184b
commit 57e88b3590
4 changed files with 700 additions and 147 deletions
@@ -10,7 +10,13 @@ use super::{math::demod_fm_with_prev, DcBlocker};
const RDS_SUBCARRIER_HZ: f32 = 57_000.0;
/// Tech 2: pilot lock level above which the ×3 pilot reference is used.
const PILOT_LOCK_THRESHOLD: f32 = 0.25;
/// Effective pilot coherence threshold ≈ ONSET + THRESHOLD × 0.2 = 0.36.
const PILOT_LOCK_THRESHOLD: f32 = 0.20;
/// Coherence below which pilot_lock contribution is zero (linear ramp 0→1
/// over the range [ONSET, ONSET+0.2]). Lower value → pilot ref used on
/// weaker stations; risk: noisier reference. 0.30 vs original 0.40 means
/// we engage at coherence ≥ 0.36 instead of ≥ 0.45.
const PILOT_LOCK_ONSET: f32 = 0.30;
/// Tech 9: number of complex CMA equalizer taps.
const CMA_N_TAPS: usize = 8;
/// Tech 9: CMA LMS step size.
@@ -722,7 +728,7 @@ impl WfmStereoDecoder {
let avg_mag = self.detect_pilot_mag_acc * inv_n;
let avg_abs = self.detect_pilot_abs_acc * inv_n;
let pilot_coherence = (avg_mag / (avg_abs + 1e-4)).clamp(0.0, 1.0);
let pilot_lock = ((pilot_coherence - 0.4) / 0.2).clamp(0.0, 1.0);
let pilot_lock = ((pilot_coherence - PILOT_LOCK_ONSET) / 0.2).clamp(0.0, 1.0);
self.pilot_lock_level += 0.12 * (pilot_lock - self.pilot_lock_level);
let stereo_drive = (avg_mag * pilot_lock * 120.0).clamp(0.0, 1.0);
let detect_coeff = if stereo_drive > self.stereo_detect_level {