@@ -80,18 +80,25 @@ assume_aligned_ptr(const T* p, size_t /*align_unused*/) {
8080
8181/* HB_TAPS and hb_q15_taps provided by dsp/halfband.h */
8282
83- /* Fixed channel low-pass for high-rate (24 kHz) mode.
83+ /* Fixed channel low-pass for high-rate mode.
8484 *
85- * Profile 0 (wide/analog): ~8 kHz cutoff @ 24 kHz Fs .
86- * Designed as Blackman-windowed sinc, 63 taps; passband ripple ~0 dB through 6 kHz,
87- * ~-6 dB at 8 kHz, stopband < -65 dB by 9 kHz .
85+ * Profile 0 (wide/analog): target ~8 kHz cutoff.
86+ * Profile 1 (digital-narrow): target ~5 kHz cutoff.
87+ * Profiles 2/3 (OP25) are generated dynamically per sample rate .
8888 *
89- * Profile 1 (digital-narrow): ~5 kHz cutoff @ 24 kHz Fs.
90- * Designed as Blackman-windowed sinc, 63 taps; passband ~0 dB through ~3.6 kHz,
91- * ~-3 dB at 4.8 kHz, stopband < -60 dB by 6 kHz; tailored for 4.8 ksps 4FSK/CQPSK. */
92- static const int kChannelLpfTaps = 63 ;
89+ * Legacy 63-tap Blackman prototypes are kept as fallback; preferred taps are
90+ * generated per sample rate to preserve the intended spectral shape at any Fs.
91+ *
92+ * At 48 kHz with 1200 Hz transition width:
93+ * - Hamming: ntaps = (53 * 48000) / (22 * 1200) = 97
94+ * - Blackman: ntaps = (74 * 48000) / (22 * 1200) = 135
95+ * Size 144 provides headroom for higher sample rates. */
96+ static const int kChannelLpfTaps = 144 ;
9397static const int kChannelLpfHistLen = kChannelLpfTaps - 1 ;
94- static const float channel_lpf_wide[kChannelLpfTaps ] = {
98+ /* Legacy fallback filters are 63 taps (designed for 24 kHz). Only used when
99+ * dynamic filter generation fails; prefer dynamically generated taps. */
100+ static const int kChannelLpfFallbackTaps = 63 ;
101+ static const float channel_lpf_wide[kChannelLpfFallbackTaps ] = {
95102 0 .0f ,
96103 0 .0f ,
97104 -1 .0f / 32768 .0f ,
@@ -159,7 +166,7 @@ static const float channel_lpf_wide[kChannelLpfTaps] = {
159166
160167/* Digital-narrow profile taps (fc≈5 kHz @ 24 kHz, 63 taps). Designed as
161168 Blackman-windowed sinc and normalized to unity DC gain. */
162- static const float channel_lpf_digital[kChannelLpfTaps ] = {
169+ static const float channel_lpf_digital[kChannelLpfFallbackTaps ] = {
163170 0 .0f ,
164171 0 .0f ,
165172 0 .0f ,
@@ -353,11 +360,18 @@ audio_polydecim_process(struct demod_state* d, const float* in, int in_len, floa
353360
354361/* ---------------- Fixed channel LPF (complex, no decimation) ----------------- */
355362
363+ /* Dynamically generated wide/digital taps (per-rate); fall back to legacy static prototypes on error. */
364+ static float s_channel_wide_taps[kChannelLpfTaps ];
365+ static float s_channel_digital_taps[kChannelLpfTaps ];
366+ static int s_channel_wide_ntaps = 0 ;
367+ static int s_channel_digital_ntaps = 0 ;
368+ static double s_channel_taps_sample_rate = 0.0 ;
369+
356370/* OP25-compatible dynamically generated filter taps (cached).
357371 * Generated once per sample rate using GNU Radio's firdes.low_pass() algorithm.
358- * Max tap count for Hamming window at 24kHz with 1200Hz transition: 49 taps . */
359- static float s_op25_tdma_taps[128 ];
360- static float s_op25_fdma_taps[128 ];
372+ * At 48 kHz with 1200 Hz transition, Hamming: ntaps = (53 * 48000) / (22 * 1200) = 97 . */
373+ static float s_op25_tdma_taps[kChannelLpfTaps ];
374+ static float s_op25_fdma_taps[kChannelLpfTaps ];
361375static int s_op25_tdma_ntaps = 0 ;
362376static int s_op25_fdma_ntaps = 0 ;
363377static double s_op25_taps_sample_rate = 0.0 ;
@@ -379,14 +393,16 @@ channel_lpf_ensure_op25_taps(double sample_rate) {
379393 }
380394
381395 /* Generate TDMA filter: firdes.low_pass(1.0, sample_rate, 9600, 1200, WIN_HAMMING) */
382- s_op25_tdma_ntaps = dsd_firdes_low_pass (1.0 , sample_rate, 9600.0 , 1200.0 , DSD_WIN_HAMMING, s_op25_tdma_taps, 128 );
396+ s_op25_tdma_ntaps =
397+ dsd_firdes_low_pass (1.0 , sample_rate, 9600.0 , 1200.0 , DSD_WIN_HAMMING, s_op25_tdma_taps, kChannelLpfTaps );
383398 if (s_op25_tdma_ntaps < 0 ) {
384399 s_op25_tdma_ntaps = 0 ;
385400 fprintf (stderr, " [channel_lpf] Failed to generate OP25 TDMA filter for Fs=%.0f\n " , sample_rate);
386401 }
387402
388403 /* Generate FDMA filter: firdes.low_pass(1.0, sample_rate, 7000, 1200, WIN_HAMMING) */
389- s_op25_fdma_ntaps = dsd_firdes_low_pass (1.0 , sample_rate, 7000.0 , 1200.0 , DSD_WIN_HAMMING, s_op25_fdma_taps, 128 );
404+ s_op25_fdma_ntaps =
405+ dsd_firdes_low_pass (1.0 , sample_rate, 7000.0 , 1200.0 , DSD_WIN_HAMMING, s_op25_fdma_taps, kChannelLpfTaps );
390406 if (s_op25_fdma_ntaps < 0 ) {
391407 s_op25_fdma_ntaps = 0 ;
392408 fprintf (stderr, " [channel_lpf] Failed to generate OP25 FDMA filter for Fs=%.0f\n " , sample_rate);
@@ -403,6 +419,39 @@ channel_lpf_ensure_op25_taps(double sample_rate) {
403419 }
404420}
405421
422+ /* *
423+ * @brief Ensure base wide/digital channel LPF taps are generated for the given sample rate.
424+ *
425+ * Matches the legacy 24 kHz designs by keeping the same absolute cutoffs:
426+ * - Wide: cutoff ~8 kHz, transition ~1.2 kHz (Blackman)
427+ * - Digital: cutoff ~5 kHz, transition ~1.2 kHz (Blackman)
428+ *
429+ * Falls back to the legacy static prototypes if generation fails.
430+ */
431+ static void
432+ channel_lpf_ensure_base_taps (double sample_rate) {
433+ if (sample_rate <= 0.0 ) {
434+ return ;
435+ }
436+ if (sample_rate == s_channel_taps_sample_rate && s_channel_wide_ntaps > 0 && s_channel_digital_ntaps > 0 ) {
437+ return ; /* Already generated for this sample rate */
438+ }
439+
440+ s_channel_wide_ntaps =
441+ dsd_firdes_low_pass (1.0 , sample_rate, 8000.0 , 1200.0 , DSD_WIN_BLACKMAN, s_channel_wide_taps, kChannelLpfTaps );
442+ if (s_channel_wide_ntaps < 0 ) {
443+ s_channel_wide_ntaps = 0 ;
444+ }
445+
446+ s_channel_digital_ntaps = dsd_firdes_low_pass (1.0 , sample_rate, 5000.0 , 1200.0 , DSD_WIN_BLACKMAN,
447+ s_channel_digital_taps, kChannelLpfTaps );
448+ if (s_channel_digital_ntaps < 0 ) {
449+ s_channel_digital_ntaps = 0 ;
450+ }
451+
452+ s_channel_taps_sample_rate = sample_rate;
453+ }
454+
406455static void
407456channel_lpf_apply (struct demod_state * d) {
408457 if (!d || !d->channel_lpf_enable || d->lp_len < 2 ) {
@@ -415,8 +464,14 @@ channel_lpf_apply(struct demod_state* d) {
415464 /* Select filter taps based on profile */
416465 switch (d->channel_lpf_profile ) {
417466 case DSD_CH_LPF_PROFILE_DIGITAL:
418- taps = channel_lpf_digital;
419- taps_len = kChannelLpfTaps ;
467+ channel_lpf_ensure_base_taps ((double )d->rate_out );
468+ if (s_channel_digital_ntaps > 0 ) {
469+ taps = s_channel_digital_taps;
470+ taps_len = s_channel_digital_ntaps;
471+ } else {
472+ taps = channel_lpf_digital; /* fallback (63 taps, 24 kHz design) */
473+ taps_len = kChannelLpfFallbackTaps ;
474+ }
420475 break ;
421476 case DSD_CH_LPF_PROFILE_OP25_TDMA:
422477 /* OP25-compatible TDMA filter: 9600 Hz cutoff, 1200 Hz transition, Hamming */
@@ -425,9 +480,9 @@ channel_lpf_apply(struct demod_state* d) {
425480 taps = s_op25_tdma_taps;
426481 taps_len = s_op25_tdma_ntaps;
427482 } else {
428- /* Fallback to wide if generation failed */
483+ /* Fallback to wide if generation failed (63 taps, 24 kHz design) */
429484 taps = channel_lpf_wide;
430- taps_len = kChannelLpfTaps ;
485+ taps_len = kChannelLpfFallbackTaps ;
431486 }
432487 break ;
433488 case DSD_CH_LPF_PROFILE_OP25_FDMA:
@@ -437,15 +492,21 @@ channel_lpf_apply(struct demod_state* d) {
437492 taps = s_op25_fdma_taps;
438493 taps_len = s_op25_fdma_ntaps;
439494 } else {
440- /* Fallback to wide if generation failed */
495+ /* Fallback to wide if generation failed (63 taps, 24 kHz design) */
441496 taps = channel_lpf_wide;
442- taps_len = kChannelLpfTaps ;
497+ taps_len = kChannelLpfFallbackTaps ;
443498 }
444499 break ;
445500 case DSD_CH_LPF_PROFILE_WIDE:
446501 default :
447- taps = channel_lpf_wide;
448- taps_len = kChannelLpfTaps ;
502+ channel_lpf_ensure_base_taps ((double )d->rate_out );
503+ if (s_channel_wide_ntaps > 0 ) {
504+ taps = s_channel_wide_taps;
505+ taps_len = s_channel_wide_ntaps;
506+ } else {
507+ taps = channel_lpf_wide; /* fallback (63 taps, 24 kHz design) */
508+ taps_len = kChannelLpfFallbackTaps ;
509+ }
449510 break ;
450511 }
451512
0 commit comments