|
|
|
@@ -758,39 +758,38 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] |
|
|
|
if 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 |
|
|
|
logging.Debug("demod", "rates", "snipRate", snipRate, "decim1", decim1, "actual", actualDemodRate) |
|
|
|
|
|
|
|
var dec []complex64 |
|
|
|
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 { |
|
|
|
// 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 { |
|
|
|
dec = fullSnip |
|
|
|
} |
|
|
|
|