Selaa lähdekoodia

Fix frontend corruption and recorder/classifier issues

master
Jan Svabenik 3 päivää sitten
vanhempi
commit
f654dfeb51
10 muutettua tiedostoa jossa 106 lisäystä ja 70 poistoa
  1. +26
    -2
      cmd/sdrd/main.go
  2. +5
    -8
      internal/classifier/features.go
  3. +21
    -12
      internal/classifier/rules.go
  4. +15
    -2
      internal/detector/detector.go
  5. +4
    -3
      internal/rds/rds.go
  6. +6
    -7
      internal/recorder/demod.go
  7. +6
    -4
      internal/recorder/demod_live.go
  8. +9
    -9
      internal/recorder/iqwriter.go
  9. +14
    -1
      internal/recorder/recorder.go
  10. +0
    -22
      web/app.js

+ 26
- 2
cmd/sdrd/main.go Näytä tiedosto

@@ -777,12 +777,14 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
}
now := time.Now()
finished, signals := det.Process(now, spectrum, cfg.CenterHz)
// enrich classification with temporal IQ features
// enrich classification with temporal IQ features on per-signal snippet
if len(iq) > 0 {
for i := range signals {
cls := classifier.Classify(classifier.SignalInput{FirstBin: signals[i].FirstBin, LastBin: signals[i].LastBin, SNRDb: signals[i].SNRDb}, spectrum, cfg.SampleRate, cfg.FFTSize, iq)
snip := extractSignalIQ(iq, cfg.SampleRate, cfg.CenterHz, signals[i].CenterHz, signals[i].BWHz)
cls := classifier.Classify(classifier.SignalInput{FirstBin: signals[i].FirstBin, LastBin: signals[i].LastBin, SNRDb: signals[i].SNRDb}, spectrum, cfg.SampleRate, cfg.FFTSize, snip)
signals[i].Class = cls
}
det.UpdateClasses(signals)
}
if sigSnap != nil {
sigSnap.set(signals)
@@ -850,6 +852,28 @@ func decoderKeys(cfg config.Config) []string {
return keys
}

func extractSignalIQ(iq []complex64, sampleRate int, centerHz float64, sigHz float64, bwHz float64) []complex64 {
if len(iq) == 0 || sampleRate <= 0 {
return nil
}
offset := sigHz - centerHz
shifted := dsp.FreqShift(iq, sampleRate, offset)
cutoff := bwHz / 2
if cutoff < 200 {
cutoff = 200
}
if cutoff > float64(sampleRate)/2-1 {
cutoff = float64(sampleRate)/2 - 1
}
taps := dsp.LowpassFIR(cutoff, sampleRate, 101)
filtered := dsp.ApplyFIR(shifted, taps)
decim := sampleRate / 200000
if decim < 1 {
decim = 1
}
return dsp.Decimate(filtered, decim)
}

func parseSince(raw string) (time.Time, error) {
if raw == "" {
return time.Time{}, nil


+ 5
- 8
internal/classifier/features.go Näytä tiedosto

@@ -16,7 +16,11 @@ func ExtractFeatures(s SignalInput, spectrum []float64, sampleRate int, fftSize
if s.LastBin >= len(spectrum) {
s.LastBin = len(spectrum) - 1
}
binHz := float64(sampleRate) / float64(max(1, fftSize))
den := fftSize
if den < 1 {
den = 1
}
binHz := float64(sampleRate) / float64(den)
// slice
start := s.FirstBin
end := s.LastBin
@@ -158,10 +162,3 @@ func clamp01(v float64) float64 {
}
return v
}

func max(a, b int) int {
if a > b {
return a
}
return b
}

+ 21
- 12
internal/classifier/rules.go Näytä tiedosto

@@ -26,20 +26,24 @@ func RuleClassify(feat Features) Classification {
if feat.InstFreqStd < 0.5 && feat.EnvVariance < 0.3 {
second = ClassDMR
}
case bw >= 2000 && bw < 3000:
// candidate for FT8
if feat.EnvVariance < 0.5 && feat.InstFreqStd < 0.7 {
best = ClassFT8
conf = 0.55
}
case bw >= 150 && bw < 500:
// candidate for WSPR
if feat.EnvVariance < 0.4 && feat.InstFreqStd < 0.5 {
best = ClassWSPR
conf = 0.55
case bw >= 3e3 && bw < 6e3:
// wider SSB/AM
if sym > 0.2 {
best = ClassSSBUSB
conf = 0.65
} else if sym < -0.2 {
best = ClassSSBLSB
conf = 0.65
} else if p2a > 2.5 && flat < 0.5 {
best = ClassAM
conf = 0.6
}
case bw >= 500 && bw < 3e3:
if sym > 0.2 {
// narrow SSB/AM + digital
if feat.EnvVariance < 0.6 && feat.InstFreqStd < 0.7 && bw >= 2000 && bw < 3000 {
best = ClassFT8
conf = 0.55
} else if sym > 0.2 {
best = ClassSSBUSB
conf = 0.7
} else if sym < -0.2 {
@@ -55,6 +59,11 @@ func RuleClassify(feat Features) Classification {
best = ClassPSK
conf = 0.5
}
case bw >= 150 && bw < 500:
if feat.EnvVariance < 0.4 && feat.InstFreqStd < 0.5 {
best = ClassWSPR
conf = 0.55
}
case bw < 150:
best = ClassCW
conf = 0.7


+ 15
- 2
internal/detector/detector.go Näytä tiedosto

@@ -82,6 +82,21 @@ func (d *Detector) Process(now time.Time, spectrum []float64, centerHz float64)
return finished, signals
}

// UpdateClasses refreshes active event classes from current signals.
func (d *Detector) UpdateClasses(signals []Signal) {
for _, s := range signals {
for _, ev := range d.active {
if overlapHz(s.CenterHz, s.BWHz, ev.centerHz, ev.bwHz) && math.Abs(s.CenterHz-ev.centerHz) < (s.BWHz+ev.bwHz)/2.0 {
if s.Class != nil {
if ev.class == nil || s.Class.Confidence >= ev.class.Confidence {
ev.class = s.Class
}
}
}
}
}
}

func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal {
n := len(spectrum)
if n == 0 {
@@ -122,7 +137,6 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise
centerFreq := centerHz + (centerBin-float64(d.nbins)/2.0)*d.binWidth
bw := float64(last-first+1) * d.binWidth
snr := peak - noise
cls := classifier.Classify(classifier.SignalInput{FirstBin: first, LastBin: last, SNRDb: snr}, spectrum, d.sampleRate, d.nbins, nil)
return Signal{
FirstBin: first,
LastBin: last,
@@ -130,7 +144,6 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise
BWHz: bw,
PeakDb: peak,
SNRDb: snr,
Class: cls,
}
}



+ 4
- 3
internal/rds/rds.go Näytä tiedosto

@@ -102,11 +102,12 @@ func decodeBlock(bits []int) (block, bool) {
}
data := uint16(raw >> 10)
synd := crcSyndrome(raw)
if synd == 0 {
switch synd {
case offA, offB, offC, offD:
return block{data: data, offset: uint16(synd)}, true
default:
return block{}, false
}
// use syndrome as offset word
return block{data: data, offset: uint16(synd)}, true
}

func crcSyndrome(raw uint32) uint16 {


+ 6
- 7
internal/recorder/demod.go Näytä tiedosto

@@ -2,6 +2,7 @@ package recorder

import (
"errors"
"math"
"path/filepath"

"sdr-visual-suite/internal/classifier"
@@ -32,21 +33,19 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f
}
taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101)
filtered := dsp.ApplyFIR(shifted, taps)
decim := m.sampleRate / d.OutputSampleRate()
if decim < 1 {
decim = 1
}
decim := int(math.Round(float64(m.sampleRate) / float64(d.OutputSampleRate())))
if decim < 1 {
decim = 1
}
dec := dsp.Decimate(filtered, decim)
audio := d.Demod(dec, m.sampleRate/decim)
inputRate := m.sampleRate / decim
audio := d.Demod(dec, inputRate)
wav := filepath.Join(dir, "audio.wav")
if err := writeWAV(wav, audio, d.OutputSampleRate(), d.Channels()); err != nil {
if err := writeWAV(wav, audio, inputRate, d.Channels()); err != nil {
return err
}
files["audio"] = "audio.wav"
files["audio_sample_rate"] = d.OutputSampleRate()
files["audio_sample_rate"] = inputRate
files["audio_channels"] = d.Channels()
files["audio_demod"] = name
if name == "WFM_STEREO" {


+ 6
- 4
internal/recorder/demod_live.go Näytä tiedosto

@@ -3,6 +3,7 @@ package recorder
import (
"bytes"
"errors"
"math"
"time"

"sdr-visual-suite/internal/demod"
@@ -47,15 +48,16 @@ func (m *Manager) DemodLive(centerHz float64, bw float64, mode string, seconds i
}
taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101)
filtered := dsp.ApplyFIR(shifted, taps)
decim := m.sampleRate / d.OutputSampleRate()
decim := int(math.Round(float64(m.sampleRate) / float64(d.OutputSampleRate())))
if decim < 1 {
decim = 1
}
dec := dsp.Decimate(filtered, decim)
audio := d.Demod(dec, m.sampleRate/decim)
inputRate := m.sampleRate / decim
audio := d.Demod(dec, inputRate)
buf := &bytes.Buffer{}
if err := writeWAVTo(buf, audio, d.OutputSampleRate(), d.Channels()); err != nil {
if err := writeWAVTo(buf, audio, inputRate, d.Channels()); err != nil {
return nil, 0, err
}
return buf.Bytes(), d.OutputSampleRate(), nil
return buf.Bytes(), inputRate, nil
}

+ 9
- 9
internal/recorder/iqwriter.go Näytä tiedosto

@@ -1,8 +1,9 @@
package recorder

import (
"encoding/binary"
"bufio"
"os"
"unsafe"
)

func writeCF32(path string, samples []complex64) error {
@@ -11,13 +12,12 @@ func writeCF32(path string, samples []complex64) error {
return err
}
defer f.Close()
for _, v := range samples {
if err := binary.Write(f, binary.LittleEndian, real(v)); err != nil {
return err
}
if err := binary.Write(f, binary.LittleEndian, imag(v)); err != nil {
return err
}
if len(samples) == 0 {
return nil
}
return nil
w := bufio.NewWriterSize(f, 1<<20)
defer w.Flush()
b := unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*8)
_, err = w.Write(b)
return err
}

+ 14
- 1
internal/recorder/recorder.go Näytä tiedosto

@@ -34,6 +34,7 @@ type Manager struct {
blockSize int
centerHz float64
decodeCommands map[string]string
queue chan detector.Event
}

func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) *Manager {
@@ -43,7 +44,9 @@ func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeC
if policy.RingSeconds <= 0 {
policy.RingSeconds = 8
}
return &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz, decodeCommands: decodeCommands}
m := &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz, decodeCommands: decodeCommands, queue: make(chan detector.Event, 64)}
go m.worker()
return m
}

func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
@@ -71,6 +74,16 @@ func (m *Manager) OnEvents(events []detector.Event) {
return
}
for _, ev := range events {
select {
case m.queue <- ev:
default:
// drop if queue full
}
}
}

func (m *Manager) worker() {
for ev := range m.queue {
_ = m.recordEvent(ev)
}
}


+ 0
- 22
web/app.js Näytä tiedosto

@@ -1307,26 +1307,4 @@ setInterval(() => fetchEvents(false), 2000);
setInterval(fetchRecordings, 5000);
setInterval(loadSignals, 1500);
setInterval(loadDecoders, 10000);
lse if (ev.key === 'ArrowLeft') {
pan = Math.max(-0.5, pan - 0.04);
followLive = false;
} else if (ev.key === 'ArrowRight') {
pan = Math.min(0.5, pan + 0.04);
followLive = false;
}
});

loadConfig();
loadStats();
loadGPU();
fetchEvents(true);
fetchRecordings();
connect();
requestAnimationFrame(renderLoop);
setInterval(loadStats, 1000);
setInterval(loadGPU, 1000);
setInterval(() => fetchEvents(false), 2000);
setInterval(fetchRecordings, 5000);
setInterval(loadSignals, 1500);
ordings, 5000);
setInterval(loadSignals, 1500);

Loading…
Peruuta
Tallenna