From 40f62f85798151e1f37180956254e8bbbee36fc4 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Wed, 18 Mar 2026 07:08:15 +0100 Subject: [PATCH] Add live demod endpoint for recent IQ --- cmd/sdrd/main.go | 19 +++++++++++ internal/recorder/demod_live.go | 56 +++++++++++++++++++++++++++++++++ internal/recorder/wavwriter.go | 32 ++++++++++--------- 3 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 internal/recorder/demod_live.go diff --git a/cmd/sdrd/main.go b/cmd/sdrd/main.go index 35f52dc..cfeb18f 100644 --- a/cmd/sdrd/main.go +++ b/cmd/sdrd/main.go @@ -525,6 +525,25 @@ func main() { http.ServeFile(w, r, filepath.Join(base, "meta.json")) }) + http.HandleFunc("/api/demod", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + q := r.URL.Query() + freq, _ := strconv.ParseFloat(q.Get("freq"), 64) + bw, _ := strconv.ParseFloat(q.Get("bw"), 64) + sec, _ := strconv.Atoi(q.Get("sec")) + mode := q.Get("mode") + data, _, err := recMgr.DemodLive(freq, bw, mode, sec) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "audio/wav") + _, _ = w.Write(data) + }) + http.Handle("/", http.FileServer(http.Dir(cfg.WebRoot))) server := &http.Server{Addr: cfg.WebAddr} diff --git a/internal/recorder/demod_live.go b/internal/recorder/demod_live.go new file mode 100644 index 0000000..7503c4e --- /dev/null +++ b/internal/recorder/demod_live.go @@ -0,0 +1,56 @@ +package recorder + +import ( + "bytes" + "errors" + "time" + + "sdr-visual-suite/internal/demod" + "sdr-visual-suite/internal/dsp" +) + +// DemodLive demodulates a recent window and returns WAV bytes. +func (m *Manager) DemodLive(centerHz float64, bw float64, mode string, seconds int) ([]byte, int, error) { + if m == nil || m.ring == nil { + return nil, 0, errors.New("recorder not ready") + } + if seconds <= 0 { + seconds = 2 + } + end := time.Now() + start := end.Add(-time.Duration(seconds) * time.Second) + segment := m.ring.Slice(start, end) + if len(segment) == 0 { + return nil, 0, errors.New("no iq in ring") + } + name := mode + if name == "" { + name = "NFM" + } + d := demod.Get(name) + if d == nil { + return nil, 0, errors.New("demodulator not found") + } + offset := centerHz - m.centerHz + shifted := dsp.FreqShift(segment, m.sampleRate, offset) + if bw <= 0 { + bw = 12000 + } + cutoff := bw / 2 + if cutoff < 200 { + cutoff = 200 + } + taps := dsp.LowpassFIR(cutoff, m.sampleRate, 101) + filtered := dsp.ApplyFIR(shifted, taps) + decim := m.sampleRate / (d.OutputSampleRate() * 4) + if decim < 1 { + decim = 1 + } + dec := dsp.Decimate(filtered, decim) + audio := d.Demod(dec, m.sampleRate/decim) + buf := &bytes.Buffer{} + if err := writeWAVTo(buf, audio, d.OutputSampleRate(), d.Channels()); err != nil { + return nil, 0, err + } + return buf.Bytes(), d.OutputSampleRate(), nil +} diff --git a/internal/recorder/wavwriter.go b/internal/recorder/wavwriter.go index 696faf8..3172dc2 100644 --- a/internal/recorder/wavwriter.go +++ b/internal/recorder/wavwriter.go @@ -2,6 +2,7 @@ package recorder import ( "encoding/binary" + "io" "os" ) @@ -11,59 +12,62 @@ func writeWAV(path string, samples []float32, sampleRate int, channels int) erro return err } defer f.Close() + return writeWAVTo(f, samples, sampleRate, channels) +} +func writeWAVTo(w io.Writer, samples []float32, sampleRate int, channels int) error { if channels <= 0 { channels = 1 } // 16-bit PCM dataSize := uint32(len(samples) * 2) // RIFF header - if _, err := f.Write([]byte("RIFF")); err != nil { + if _, err := w.Write([]byte("RIFF")); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint32(36)+dataSize); err != nil { + if err := binary.Write(w, binary.LittleEndian, uint32(36)+dataSize); err != nil { return err } - if _, err := f.Write([]byte("WAVE")); err != nil { + if _, err := w.Write([]byte("WAVE")); err != nil { return err } // fmt chunk - if _, err := f.Write([]byte("fmt ")); err != nil { + if _, err := w.Write([]byte("fmt ")); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint32(16)); err != nil { + if err := binary.Write(w, binary.LittleEndian, uint32(16)); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint16(1)); err != nil { // PCM + if err := binary.Write(w, binary.LittleEndian, uint16(1)); err != nil { // PCM return err } - if err := binary.Write(f, binary.LittleEndian, uint16(channels)); err != nil { + if err := binary.Write(w, binary.LittleEndian, uint16(channels)); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint32(sampleRate)); err != nil { + if err := binary.Write(w, binary.LittleEndian, uint32(sampleRate)); err != nil { return err } byteRate := uint32(sampleRate * channels * 2) - if err := binary.Write(f, binary.LittleEndian, byteRate); err != nil { + if err := binary.Write(w, binary.LittleEndian, byteRate); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint16(channels*2)); err != nil { + if err := binary.Write(w, binary.LittleEndian, uint16(channels*2)); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, uint16(16)); err != nil { // bits + if err := binary.Write(w, binary.LittleEndian, uint16(16)); err != nil { // bits return err } // data chunk - if _, err := f.Write([]byte("data")); err != nil { + if _, err := w.Write([]byte("data")); err != nil { return err } - if err := binary.Write(f, binary.LittleEndian, dataSize); err != nil { + if err := binary.Write(w, binary.LittleEndian, dataSize); err != nil { return err } // samples for _, s := range samples { v := int16(clip(s * 32767)) - if err := binary.Write(f, binary.LittleEndian, v); err != nil { + if err := binary.Write(w, binary.LittleEndian, v); err != nil { return err } }