package recorder import ( "errors" "log" "path/filepath" "sdr-visual-suite/internal/classifier" "sdr-visual-suite/internal/demod" "sdr-visual-suite/internal/detector" ) func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, files map[string]any) error { if ev.Class == nil { return nil } name := mapClassToDemod(ev.Class.ModType) if name == "" { return nil } d := demod.Get(name) if d == nil { return errors.New("demodulator not found") } bw := ev.Bandwidth offset := ev.CenterHz - m.centerHz var audio []float32 var inputRate int gpu := m.gpuEngine() if gpu != nil { gpuMode, useGPU := gpuModeFor(name) if useGPU { if gpuAudio, gpuRate, ok := tryGPUAudio(gpu, name, iq, offset, bw, gpuMode); ok { audio = gpuAudio inputRate = gpuRate } } } if audio == nil { if name == "WFM_STEREO" { log.Printf("gpudemod: WFM_STEREO using CPU stereo/RDS post-process for event %d", ev.ID) } else { log.Printf("gpudemod: CPU demod fallback used for event %d (%s)", ev.ID, name) } audio, inputRate = demodAudioCPU(d, iq, m.sampleRate, offset, bw) } wav := filepath.Join(dir, "audio.wav") if err := writeWAV(wav, audio, inputRate, d.Channels()); err != nil { return err } files["audio"] = "audio.wav" files["audio_sample_rate"] = inputRate 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 dec := rdsdecoder{} res := dec.Decode(rds, 2400) if res.PI != 0 { files["rds_pi"] = res.PI } if res.PS != "" { files["rds_ps"] = res.PS } if res.RT != "" { files["rds_rt"] = res.RT } } } return nil } func mapClassToDemod(c classifier.SignalClass) string { switch c { case classifier.ClassAM: return "AM" case classifier.ClassNFM: return "NFM" case classifier.ClassWFM: return "WFM" case classifier.ClassSSBUSB: return "USB" case classifier.ClassSSBLSB: return "LSB" case classifier.ClassCW: return "CW" case classifier.ClassFT8, classifier.ClassWSPR, classifier.ClassFSK, classifier.ClassPSK: return "USB" default: return "" } }