|
- package demod
-
- import (
- "math"
-
- "sdr-wideband-suite/internal/dsp"
- "sdr-wideband-suite/internal/logging"
- )
-
- type NFM struct{}
-
- type WFM struct{}
-
- type WFMStereo struct{}
-
- func (NFM) Name() string { return "NFM" }
- func (WFM) Name() string { return "WFM" }
- func (WFMStereo) Name() string { return "WFM_STEREO" }
- func (NFM) OutputSampleRate() int { return 48000 }
- func (WFM) OutputSampleRate() int { return 192000 }
- func (WFMStereo) OutputSampleRate() int {
- return 192000
- }
- func (NFM) Channels() int { return 1 }
- func (WFM) Channels() int { return 1 }
- func (WFMStereo) Channels() int { return 2 }
-
- func wfmMonoBase(iq []complex64) []float32 {
- return fmDiscrim(iq)
- }
-
- func (NFM) Demod(iq []complex64, sampleRate int) []float32 {
- return fmDiscrim(iq)
- }
-
- func (WFM) Demod(iq []complex64, sampleRate int) []float32 {
- return wfmMonoBase(iq)
- }
-
- func (WFMStereo) Demod(iq []complex64, sampleRate int) []float32 {
- return wfmStereo(iq, sampleRate)
- }
-
- func fmDiscrim(iq []complex64) []float32 {
- if len(iq) < 2 {
- return nil
- }
- out := make([]float32, len(iq)-1)
- maxAbs := 0.0
- maxIdx := 0
- largeSteps := 0
- minMag := math.MaxFloat64
- maxMag := 0.0
- for i := 1; i < len(iq); i++ {
- p := iq[i-1]
- c := iq[i]
- pmag := math.Hypot(float64(real(p)), float64(imag(p)))
- cmag := math.Hypot(float64(real(c)), float64(imag(c)))
- if pmag < minMag {
- minMag = pmag
- }
- if cmag < minMag {
- minMag = cmag
- }
- if pmag > maxMag {
- maxMag = pmag
- }
- if cmag > maxMag {
- maxMag = cmag
- }
- num := float64(real(p))*float64(imag(c)) - float64(imag(p))*float64(real(c))
- den := float64(real(p))*float64(real(c)) + float64(imag(p))*float64(imag(c))
- step := math.Atan2(num, den)
- if a := math.Abs(step); a > maxAbs {
- maxAbs = a
- maxIdx = i - 1
- }
- if math.Abs(step) > 1.5 {
- largeSteps++
- }
- out[i-1] = float32(step)
- }
- if logging.EnabledCategory("discrim") {
- logging.Debug("discrim", "fm_meter", "iq_len", len(iq), "audio_len", len(out), "min_mag", minMag, "max_mag", maxMag, "max_abs_step", maxAbs, "max_idx", maxIdx, "large_steps", largeSteps)
- if largeSteps > 0 {
- logging.Warn("discrim", "fm_large_steps", "iq_len", len(iq), "large_steps", largeSteps, "max_abs_step", maxAbs, "max_idx", maxIdx, "min_mag", minMag, "max_mag", maxMag)
- }
- }
- return out
- }
-
- func wfmStereo(iq []complex64, sampleRate int) []float32 {
- base := fmDiscrim(iq)
- if len(base) == 0 {
- return nil
- }
- lp := dsp.LowpassFIR(15000, sampleRate, 101)
- lpr := dsp.ApplyFIRReal(base, lp)
- bpHi := dsp.LowpassFIR(53000, sampleRate, 101)
- bpLo := dsp.LowpassFIR(23000, sampleRate, 101)
- hi := dsp.ApplyFIRReal(base, bpHi)
- lo := dsp.ApplyFIRReal(base, bpLo)
- bpf := make([]float32, len(base))
- for i := range base {
- bpf[i] = hi[i] - lo[i]
- }
- lr := make([]float32, len(base))
- phase := 0.0
- inc := 2 * math.Pi * 38000 / float64(sampleRate)
- for i := range bpf {
- phase += inc
- lr[i] = bpf[i] * float32(2*math.Cos(phase))
- }
- lr = dsp.ApplyFIRReal(lr, lp)
- out := make([]float32, len(lpr)*2)
- for i := range lpr {
- l := 0.5 * (lpr[i] + lr[i])
- r := 0.5 * (lpr[i] - lr[i])
- out[i*2] = l
- out[i*2+1] = r
- }
- return out
- }
-
- type RDSBasebandResult struct {
- Samples []float32
- SampleRate int
- }
-
- // RDSBaseband returns a rough 57k baseband (not decoded).
- func RDSBaseband(iq []complex64, sampleRate int) []float32 {
- return RDSBasebandDecimated(iq, sampleRate).Samples
- }
-
- // RDSComplexResult holds complex baseband samples for the Costas loop RDS decoder.
- type RDSComplexResult struct {
- Samples []complex64
- SampleRate int
- }
-
- // RDSBasebandComplex extracts the RDS subcarrier as complex samples.
- // The Costas loop in the RDS decoder needs both I and Q to lock.
- func RDSBasebandComplex(iq []complex64, sampleRate int) RDSComplexResult {
- base := wfmMonoBase(iq)
- if len(base) == 0 || sampleRate <= 0 {
- return RDSComplexResult{}
- }
- cplx := make([]complex64, len(base))
- for i, v := range base {
- cplx[i] = complex(v, 0)
- }
- cplx = dsp.FreqShift(cplx, sampleRate, -57000)
- lpTaps := dsp.LowpassFIR(7500, sampleRate, 101)
- cplx = dsp.ApplyFIR(cplx, lpTaps)
- targetRate := 19000
- decim := sampleRate / targetRate
- if decim < 1 {
- decim = 1
- }
- cplx = dsp.Decimate(cplx, decim)
- actualRate := sampleRate / decim
- return RDSComplexResult{Samples: cplx, SampleRate: actualRate}
- }
-
- // RDSBasebandDecimated returns float32 baseband for WAV writing / recorder.
- func RDSBasebandDecimated(iq []complex64, sampleRate int) RDSBasebandResult {
- res := RDSBasebandComplex(iq, sampleRate)
- out := make([]float32, len(res.Samples))
- for i, c := range res.Samples {
- out[i] = real(c)
- }
- return RDSBasebandResult{Samples: out, SampleRate: res.SampleRate}
- }
-
- func init() {
- Register(NFM{})
- Register(WFM{})
- Register(WFMStereo{})
- }
|