diff --git a/cmd/sdrd/dsp_loop.go b/cmd/sdrd/dsp_loop.go index db8d05f..3f9bd67 100644 --- a/cmd/sdrd/dsp_loop.go +++ b/cmd/sdrd/dsp_loop.go @@ -106,6 +106,16 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * } else { displaySignals = rt.det.StableSignals() } + if rec != nil && len(displaySignals) > 0 { + runtimeInfo := rec.RuntimeInfoBySignalID() + for i := range displaySignals { + if info, ok := runtimeInfo[displaySignals[i].ID]; ok { + displaySignals[i].DemodName = info.DemodName + displaySignals[i].PlaybackMode = info.PlaybackMode + displaySignals[i].StereoState = info.StereoState + } + } + } state.arbitration = rt.arbitration state.presentation = state.surveillance.DisplayLevel if phaseSnap != nil { diff --git a/internal/detector/detector.go b/internal/detector/detector.go index 675022f..5ff9b92 100644 --- a/internal/detector/detector.go +++ b/internal/detector/detector.go @@ -68,16 +68,19 @@ type activeEvent struct { } type Signal struct { - ID int64 `json:"id"` - FirstBin int `json:"first_bin"` - LastBin int `json:"last_bin"` - CenterHz float64 `json:"center_hz"` - BWHz float64 `json:"bw_hz"` - PeakDb float64 `json:"peak_db"` - SNRDb float64 `json:"snr_db"` - NoiseDb float64 `json:"noise_db,omitempty"` - Class *classifier.Classification `json:"class,omitempty"` - PLL *classifier.PLLResult `json:"pll,omitempty"` + ID int64 `json:"id"` + FirstBin int `json:"first_bin"` + LastBin int `json:"last_bin"` + CenterHz float64 `json:"center_hz"` + BWHz float64 `json:"bw_hz"` + PeakDb float64 `json:"peak_db"` + SNRDb float64 `json:"snr_db"` + NoiseDb float64 `json:"noise_db,omitempty"` + Class *classifier.Classification `json:"class,omitempty"` + PLL *classifier.PLLResult `json:"pll,omitempty"` + DemodName string `json:"demod,omitempty"` + PlaybackMode string `json:"playback_mode,omitempty"` + StereoState string `json:"stereo_state,omitempty"` } func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector { diff --git a/internal/recorder/recorder.go b/internal/recorder/recorder.go index 1213e18..e2893e4 100644 --- a/internal/recorder/recorder.go +++ b/internal/recorder/recorder.go @@ -356,6 +356,13 @@ func (m *Manager) StreamerRef() *Streamer { return m.streamer } +func (m *Manager) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo { + if m == nil || m.streamer == nil { + return nil + } + return m.streamer.RuntimeInfoBySignalID() +} + // ActiveStreams returns info about currently active streaming sessions. func (m *Manager) ActiveStreams() int { if m == nil || m.streamer == nil { diff --git a/internal/recorder/streamer.go b/internal/recorder/streamer.go index 73ee258..a6eab4b 100644 --- a/internal/recorder/streamer.go +++ b/internal/recorder/streamer.go @@ -115,6 +115,14 @@ type audioSub struct { ch chan []byte } +type RuntimeSignalInfo struct { + DemodName string + PlaybackMode string + StereoState string + Channels int + SampleRate int +} + // AudioInfo describes the audio format of a live-listen subscription. // Sent to the WebSocket client as the first message. type AudioInfo struct { @@ -457,6 +465,22 @@ func (st *Streamer) attachPendingListeners(sess *streamSession) { } // CloseAll finalises all sessions and stops the worker goroutine. +func (st *Streamer) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo { + st.mu.RLock() + defer st.mu.RUnlock() + out := make(map[int64]RuntimeSignalInfo, len(st.sessions)) + for _, sess := range st.sessions { + out[sess.signalID] = RuntimeSignalInfo{ + DemodName: sess.demodName, + PlaybackMode: sess.playbackMode, + StereoState: sess.stereoState, + Channels: sess.channels, + SampleRate: sess.sampleRate, + } + } + return out +} + func (st *Streamer) CloseAll() { close(st.feedCh) <-st.done diff --git a/web/app.js b/web/app.js index dbd5750..5d64731 100644 --- a/web/app.js +++ b/web/app.js @@ -307,6 +307,8 @@ function isListeningSignal(signal) { } function getSignalPrimaryMode(signal) { + if (signal?.playback_mode) return signal.playback_mode; + if (signal?.demod) return signal.demod; if (isListeningSignal(signal) && liveListenInfo?.playback_mode && liveListenInfo.playback_mode !== '-') { return liveListenInfo.playback_mode; } @@ -315,10 +317,12 @@ function getSignalPrimaryMode(signal) { } function getSignalRuntimeSummary(signal) { - if (!isListeningSignal(signal)) return ''; const bits = []; - if (liveListenInfo?.status && !['Idle', '-'].includes(liveListenInfo.status)) bits.push(liveListenInfo.status); - if (liveListenInfo?.stereo_state && liveListenInfo.stereo_state !== '-') bits.push(liveListenInfo.stereo_state); + if (signal?.stereo_state) bits.push(signal.stereo_state); + if (!bits.length && isListeningSignal(signal)) { + if (liveListenInfo?.status && !['Idle', '-'].includes(liveListenInfo.status)) bits.push(liveListenInfo.status); + if (liveListenInfo?.stereo_state && liveListenInfo.stereo_state !== '-') bits.push(liveListenInfo.stereo_state); + } return bits.join(' · '); } @@ -1511,15 +1515,15 @@ function _createSignalItem(s) { btn.dataset.id = s.id || 0; const primaryMode = getSignalPrimaryMode(s); const mc = modColor(primaryMode); + const rds = s.class?.pll?.rds_station || ''; const dec = decisionIndex.get(String(s.id || 0)); const decText = dec?.reason ? `${dec.reason}` : ''; const decFlags = dec ? `${dec.record ? 'REC' : ''}${dec.decode ? (dec.record ? '+DEC' : 'DEC') : ''}` : ''; const metaBits = []; if (decFlags) metaBits.push(decFlags); if (decText) metaBits.push(decText); - if (s.class?.pll?.rds_station) metaBits.push(`RDS ${s.class.pll.rds_station}`); btn.title = metaBits.join(' · '); - btn.innerHTML = `
${fmtMHz(s.center_hz, 6)}${(s.snr_db || 0).toFixed(1)} dB
${primaryMode}
`; + btn.innerHTML = `
${fmtMHz(s.center_hz, 6)}${(s.snr_db || 0).toFixed(1)} dB
${primaryMode}${rds ? `${rds}` : ''}
`; btn.style.borderLeftColor = mc.label; btn.style.borderLeftWidth = '3px'; btn.style.borderLeftStyle = 'solid';