| @@ -758,39 +758,38 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] | |||||
| if decim1 < 1 { | if decim1 < 1 { | ||||
| decim1 = 1 | decim1 = 1 | ||||
| } | } | ||||
| // WFM override: force decim1=2 (256kHz) instead of round(512k/192k)=3 (170kHz). | |||||
| // At decim1=3, Nyquist is 85kHz which clips FM broadcast ±75kHz deviation. | |||||
| // At decim1=2, Nyquist is 128kHz → full FM deviation + stereo pilot + guard band. | |||||
| // Bonus: 256000→48000 resampler ratio is L=3/M=16 (96 taps, 1kB) instead of | |||||
| // the pathological L=24000/M=85333 (768k taps, 6MB) from 170666→48000. | |||||
| if isWFM && decim1 > 2 && snipRate/2 >= 200000 { | |||||
| decim1 = 2 | |||||
| } | |||||
| actualDemodRate := snipRate / decim1 | actualDemodRate := snipRate / decim1 | ||||
| logging.Debug("demod", "rates", "snipRate", snipRate, "decim1", decim1, "actual", actualDemodRate) | logging.Debug("demod", "rates", "snipRate", snipRate, "decim1", decim1, "actual", actualDemodRate) | ||||
| var dec []complex64 | var dec []complex64 | ||||
| if decim1 > 1 { | if decim1 > 1 { | ||||
| // For WFM: skip the pre-demod anti-alias FIR entirely. | |||||
| // FM broadcast has ±75kHz deviation. The old cutoff at | |||||
| // actualDemodRate/2*0.8 = 68kHz was BELOW the FM deviation, | |||||
| // clipping the modulation and causing amplitude dips that | |||||
| // the FM discriminator turned into audible clicks (4-5/sec). | |||||
| // The extraction stage already bandlimited to ±125kHz (BW/2 * bwMult), | |||||
| // which is sufficient anti-alias protection for the 512k→170k decimation. | |||||
| // | |||||
| // For NFM/other: use a cutoff that preserves the full signal bandwidth. | |||||
| // FIR cutoff: for WFM, use 90kHz (above ±75kHz FM deviation + guard). | |||||
| // For NFM/other: use standard Nyquist*0.8 cutoff. | |||||
| cutoff := float64(actualDemodRate) / 2.0 * 0.8 | |||||
| if isWFM { | if isWFM { | ||||
| // WFM: decimate without FIR — extraction filter is sufficient | |||||
| dec = dsp.DecimateStateful(fullSnip, decim1, &sess.preDemodDecimPhase) | |||||
| } else { | |||||
| cutoff := float64(actualDemodRate) / 2.0 * 0.8 | |||||
| // Lazy-init or reinit stateful FIR if parameters changed | |||||
| if sess.preDemodFIR == nil || sess.preDemodRate != snipRate || sess.preDemodCutoff != cutoff { | |||||
| taps := dsp.LowpassFIR(cutoff, snipRate, 101) | |||||
| sess.preDemodFIR = dsp.NewStatefulFIRComplex(taps) | |||||
| sess.preDemodRate = snipRate | |||||
| sess.preDemodCutoff = cutoff | |||||
| sess.preDemodDecim = decim1 | |||||
| sess.preDemodDecimPhase = 0 | |||||
| } | |||||
| cutoff = 90000 | |||||
| } | |||||
| filtered := sess.preDemodFIR.ProcessInto(fullSnip, sess.growIQ(len(fullSnip))) | |||||
| dec = dsp.DecimateStateful(filtered, decim1, &sess.preDemodDecimPhase) | |||||
| // Lazy-init or reinit stateful FIR if parameters changed | |||||
| if sess.preDemodFIR == nil || sess.preDemodRate != snipRate || sess.preDemodCutoff != cutoff { | |||||
| taps := dsp.LowpassFIR(cutoff, snipRate, 101) | |||||
| sess.preDemodFIR = dsp.NewStatefulFIRComplex(taps) | |||||
| sess.preDemodRate = snipRate | |||||
| sess.preDemodCutoff = cutoff | |||||
| sess.preDemodDecim = decim1 | |||||
| sess.preDemodDecimPhase = 0 | |||||
| } | } | ||||
| filtered := sess.preDemodFIR.ProcessInto(fullSnip, sess.growIQ(len(fullSnip))) | |||||
| dec = dsp.DecimateStateful(filtered, decim1, &sess.preDemodDecimPhase) | |||||
| } else { | } else { | ||||
| dec = fullSnip | dec = fullSnip | ||||
| } | } | ||||