| @@ -6,6 +6,7 @@ type AM struct{} | |||||
| func (AM) Name() string { return "AM" } | func (AM) Name() string { return "AM" } | ||||
| func (AM) OutputSampleRate() int { return 48000 } | func (AM) OutputSampleRate() int { return 48000 } | ||||
| func (AM) Channels() int { return 1 } | |||||
| func (AM) Demod(iq []complex64, sampleRate int) []float32 { | func (AM) Demod(iq []complex64, sampleRate int) []float32 { | ||||
| if len(iq) == 0 { | if len(iq) == 0 { | ||||
| @@ -6,6 +6,7 @@ type CW struct{} | |||||
| func (CW) Name() string { return "CW" } | func (CW) Name() string { return "CW" } | ||||
| func (CW) OutputSampleRate() int { return 48000 } | func (CW) OutputSampleRate() int { return 48000 } | ||||
| func (CW) Channels() int { return 1 } | |||||
| func (CW) Demod(iq []complex64, sampleRate int) []float32 { | func (CW) Demod(iq []complex64, sampleRate int) []float32 { | ||||
| if len(iq) == 0 { | if len(iq) == 0 { | ||||
| @@ -4,6 +4,7 @@ type Demodulator interface { | |||||
| Name() string | Name() string | ||||
| Demod(iq []complex64, sampleRate int) []float32 | Demod(iq []complex64, sampleRate int) []float32 | ||||
| OutputSampleRate() int | OutputSampleRate() int | ||||
| Channels() int | |||||
| } | } | ||||
| var registry = map[string]Demodulator{} | var registry = map[string]Demodulator{} | ||||
| @@ -1,15 +1,28 @@ | |||||
| package demod | package demod | ||||
| import "math" | |||||
| import ( | |||||
| "math" | |||||
| "sdr-visual-suite/internal/dsp" | |||||
| ) | |||||
| type NFM struct{} | type NFM struct{} | ||||
| type WFM struct{} | type WFM struct{} | ||||
| type WFMStereo struct{} | |||||
| func (NFM) Name() string { return "NFM" } | func (NFM) Name() string { return "NFM" } | ||||
| func (WFM) Name() string { return "WFM" } | func (WFM) Name() string { return "WFM" } | ||||
| func (WFMStereo) Name() string { return "WFM_STEREO" } | |||||
| func (NFM) OutputSampleRate() int { return 48000 } | func (NFM) OutputSampleRate() int { return 48000 } | ||||
| func (WFM) OutputSampleRate() int { return 192000 } | 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 (NFM) Demod(iq []complex64, sampleRate int) []float32 { | func (NFM) Demod(iq []complex64, sampleRate int) []float32 { | ||||
| return fmDiscrim(iq) | return fmDiscrim(iq) | ||||
| @@ -19,6 +32,10 @@ func (WFM) Demod(iq []complex64, sampleRate int) []float32 { | |||||
| return fmDiscrim(iq) | return fmDiscrim(iq) | ||||
| } | } | ||||
| func (WFMStereo) Demod(iq []complex64, sampleRate int) []float32 { | |||||
| return wfmStereo(iq, sampleRate) | |||||
| } | |||||
| func fmDiscrim(iq []complex64) []float32 { | func fmDiscrim(iq []complex64) []float32 { | ||||
| if len(iq) < 2 { | if len(iq) < 2 { | ||||
| return nil | return nil | ||||
| @@ -34,7 +51,66 @@ func fmDiscrim(iq []complex64) []float32 { | |||||
| return out | 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 | |||||
| } | |||||
| // RDSBaseband returns a rough 57k baseband (not decoded). | |||||
| func RDSBaseband(iq []complex64, sampleRate int) []float32 { | |||||
| base := fmDiscrim(iq) | |||||
| if len(base) == 0 { | |||||
| return nil | |||||
| } | |||||
| bpHi := dsp.LowpassFIR(60000, sampleRate, 101) | |||||
| bpLo := dsp.LowpassFIR(54000, 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] | |||||
| } | |||||
| phase := 0.0 | |||||
| inc := 2 * math.Pi * 57000 / float64(sampleRate) | |||||
| out := make([]float32, len(base)) | |||||
| for i := range bpf { | |||||
| phase += inc | |||||
| out[i] = bpf[i] * float32(math.Cos(phase)) | |||||
| } | |||||
| lp := dsp.LowpassFIR(2400, sampleRate, 101) | |||||
| return dsp.ApplyFIRReal(out, lp) | |||||
| } | |||||
| func init() { | func init() { | ||||
| Register(NFM{}) | Register(NFM{}) | ||||
| Register(WFM{}) | Register(WFM{}) | ||||
| Register(WFMStereo{}) | |||||
| } | } | ||||
| @@ -10,6 +10,8 @@ func (USB) Name() string { return "USB" } | |||||
| func (LSB) Name() string { return "LSB" } | func (LSB) Name() string { return "LSB" } | ||||
| func (USB) OutputSampleRate() int { return 48000 } | func (USB) OutputSampleRate() int { return 48000 } | ||||
| func (LSB) OutputSampleRate() int { return 48000 } | func (LSB) OutputSampleRate() int { return 48000 } | ||||
| func (USB) Channels() int { return 1 } | |||||
| func (LSB) Channels() int { return 1 } | |||||
| func (USB) Demod(iq []complex64, sampleRate int) []float32 { return ssb(iq, sampleRate, true) } | func (USB) Demod(iq []complex64, sampleRate int) []float32 { return ssb(iq, sampleRate, true) } | ||||
| func (LSB) Demod(iq []complex64, sampleRate int) []float32 { return ssb(iq, sampleRate, false) } | func (LSB) Demod(iq []complex64, sampleRate int) []float32 { return ssb(iq, sampleRate, false) } | ||||
| @@ -0,0 +1,21 @@ | |||||
| package dsp | |||||
| // ApplyFIRReal applies real FIR taps to real signal. | |||||
| func ApplyFIRReal(x []float32, taps []float64) []float32 { | |||||
| if len(x) == 0 || len(taps) == 0 { | |||||
| return nil | |||||
| } | |||||
| out := make([]float32, len(x)) | |||||
| for i := 0; i < len(x); i++ { | |||||
| var acc float64 | |||||
| for k := 0; k < len(taps); k++ { | |||||
| idx := i - k | |||||
| if idx < 0 { | |||||
| break | |||||
| } | |||||
| acc += float64(x[idx]) * taps[k] | |||||
| } | |||||
| out[i] = float32(acc) | |||||
| } | |||||
| return out | |||||
| } | |||||
| @@ -39,12 +39,21 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f | |||||
| dec := dsp.Decimate(filtered, decim) | dec := dsp.Decimate(filtered, decim) | ||||
| audio := d.Demod(dec, m.sampleRate/decim) | audio := d.Demod(dec, m.sampleRate/decim) | ||||
| wav := filepath.Join(dir, "audio.wav") | wav := filepath.Join(dir, "audio.wav") | ||||
| if err := writeWAV(wav, audio, d.OutputSampleRate()); err != nil { | |||||
| if err := writeWAV(wav, audio, d.OutputSampleRate(), d.Channels()); err != nil { | |||||
| return err | return err | ||||
| } | } | ||||
| files["audio"] = "audio.wav" | files["audio"] = "audio.wav" | ||||
| files["audio_sample_rate"] = d.OutputSampleRate() | files["audio_sample_rate"] = d.OutputSampleRate() | ||||
| files["audio_channels"] = d.Channels() | |||||
| files["audio_demod"] = name | files["audio_demod"] = name | ||||
| if name == "WFM_STEREO" { | |||||
| if rds := demod.RDSBaseband(iq, m.sampleRate); len(rds) > 0 { | |||||
| rdsPath := filepath.Join(dir, "rds.wav") | |||||
| _ = writeWAV(rdsPath, rds, 2400, 1) | |||||
| files["rds_baseband"] = "rds.wav" | |||||
| files["rds_sample_rate"] = 2400 | |||||
| } | |||||
| } | |||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -5,13 +5,16 @@ import ( | |||||
| "os" | "os" | ||||
| ) | ) | ||||
| func writeWAV(path string, samples []float32, sampleRate int) error { | |||||
| func writeWAV(path string, samples []float32, sampleRate int, channels int) error { | |||||
| f, err := os.Create(path) | f, err := os.Create(path) | ||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| defer f.Close() | defer f.Close() | ||||
| if channels <= 0 { | |||||
| channels = 1 | |||||
| } | |||||
| // 16-bit PCM | // 16-bit PCM | ||||
| dataSize := uint32(len(samples) * 2) | dataSize := uint32(len(samples) * 2) | ||||
| // RIFF header | // RIFF header | ||||
| @@ -34,17 +37,17 @@ func writeWAV(path string, samples []float32, sampleRate int) error { | |||||
| if err := binary.Write(f, binary.LittleEndian, uint16(1)); err != nil { // PCM | if err := binary.Write(f, binary.LittleEndian, uint16(1)); err != nil { // PCM | ||||
| return err | return err | ||||
| } | } | ||||
| if err := binary.Write(f, binary.LittleEndian, uint16(1)); err != nil { // mono | |||||
| if err := binary.Write(f, binary.LittleEndian, uint16(channels)); err != nil { | |||||
| return err | return err | ||||
| } | } | ||||
| if err := binary.Write(f, binary.LittleEndian, uint32(sampleRate)); err != nil { | if err := binary.Write(f, binary.LittleEndian, uint32(sampleRate)); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| byteRate := uint32(sampleRate * 2) | |||||
| byteRate := uint32(sampleRate * channels * 2) | |||||
| if err := binary.Write(f, binary.LittleEndian, byteRate); err != nil { | if err := binary.Write(f, binary.LittleEndian, byteRate); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| if err := binary.Write(f, binary.LittleEndian, uint16(2)); err != nil { // block align | |||||
| if err := binary.Write(f, binary.LittleEndian, uint16(channels*2)); err != nil { | |||||
| return err | return err | ||||
| } | } | ||||
| if err := binary.Write(f, binary.LittleEndian, uint16(16)); err != nil { // bits | if err := binary.Write(f, binary.LittleEndian, uint16(16)); err != nil { // bits | ||||