Quellcode durchsuchen

Add WFM stereo decode and RDS baseband output

master
Jan Svabenik vor 4 Tagen
Ursprung
Commit
6ef106577c
8 geänderte Dateien mit 120 neuen und 6 gelöschten Zeilen
  1. +1
    -0
      internal/demod/am.go
  2. +1
    -0
      internal/demod/cw.go
  3. +1
    -0
      internal/demod/demod.go
  4. +77
    -1
      internal/demod/fm.go
  5. +2
    -0
      internal/demod/ssb.go
  6. +21
    -0
      internal/dsp/fir_real.go
  7. +10
    -1
      internal/recorder/demod.go
  8. +7
    -4
      internal/recorder/wavwriter.go

+ 1
- 0
internal/demod/am.go Datei anzeigen

@@ -6,6 +6,7 @@ type AM struct{}

func (AM) Name() string { return "AM" }
func (AM) OutputSampleRate() int { return 48000 }
func (AM) Channels() int { return 1 }

func (AM) Demod(iq []complex64, sampleRate int) []float32 {
if len(iq) == 0 {


+ 1
- 0
internal/demod/cw.go Datei anzeigen

@@ -6,6 +6,7 @@ type CW struct{}

func (CW) Name() string { return "CW" }
func (CW) OutputSampleRate() int { return 48000 }
func (CW) Channels() int { return 1 }

func (CW) Demod(iq []complex64, sampleRate int) []float32 {
if len(iq) == 0 {


+ 1
- 0
internal/demod/demod.go Datei anzeigen

@@ -4,6 +4,7 @@ type Demodulator interface {
Name() string
Demod(iq []complex64, sampleRate int) []float32
OutputSampleRate() int
Channels() int
}

var registry = map[string]Demodulator{}


+ 77
- 1
internal/demod/fm.go Datei anzeigen

@@ -1,15 +1,28 @@
package demod

import "math"
import (
"math"

"sdr-visual-suite/internal/dsp"
)

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 (NFM) Demod(iq []complex64, sampleRate int) []float32 {
return fmDiscrim(iq)
@@ -19,6 +32,10 @@ func (WFM) Demod(iq []complex64, sampleRate int) []float32 {
return fmDiscrim(iq)
}

func (WFMStereo) Demod(iq []complex64, sampleRate int) []float32 {
return wfmStereo(iq, sampleRate)
}

func fmDiscrim(iq []complex64) []float32 {
if len(iq) < 2 {
return nil
@@ -34,7 +51,66 @@ func fmDiscrim(iq []complex64) []float32 {
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() {
Register(NFM{})
Register(WFM{})
Register(WFMStereo{})
}

+ 2
- 0
internal/demod/ssb.go Datei anzeigen

@@ -10,6 +10,8 @@ func (USB) Name() string { return "USB" }
func (LSB) Name() string { return "LSB" }
func (USB) 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 (LSB) Demod(iq []complex64, sampleRate int) []float32 { return ssb(iq, sampleRate, false) }


+ 21
- 0
internal/dsp/fir_real.go Datei anzeigen

@@ -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
}

+ 10
- 1
internal/recorder/demod.go Datei anzeigen

@@ -39,12 +39,21 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f
dec := dsp.Decimate(filtered, decim)
audio := d.Demod(dec, m.sampleRate/decim)
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
}
files["audio"] = "audio.wav"
files["audio_sample_rate"] = d.OutputSampleRate()
files["audio_channels"] = d.Channels()
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
}



+ 7
- 4
internal/recorder/wavwriter.go Datei anzeigen

@@ -5,13 +5,16 @@ import (
"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)
if err != nil {
return err
}
defer f.Close()

if channels <= 0 {
channels = 1
}
// 16-bit PCM
dataSize := uint32(len(samples) * 2)
// 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
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
}
if err := binary.Write(f, binary.LittleEndian, uint32(sampleRate)); err != nil {
return err
}
byteRate := uint32(sampleRate * 2)
byteRate := uint32(sampleRate * channels * 2)
if err := binary.Write(f, binary.LittleEndian, byteRate); err != nil {
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
}
if err := binary.Write(f, binary.LittleEndian, uint16(16)); err != nil { // bits


Laden…
Abbrechen
Speichern