diff --git a/internal/recorder/streamer.go b/internal/recorder/streamer.go index 55af71d..73ee258 100644 --- a/internal/recorder/streamer.go +++ b/internal/recorder/streamer.go @@ -669,6 +669,8 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] sess.stereoEnabled = false } } + prevPlayback := sess.playbackMode + prevStereo := sess.stereoState if sess.stereoEnabled && len(stereoAudio) > 0 { sess.stereoState = "locked" audio = stereoAudio @@ -681,6 +683,9 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] } audio = dual } + if (prevPlayback != sess.playbackMode || prevStereo != sess.stereoState) && len(sess.audioSubs) > 0 { + sendAudioInfo(sess.audioSubs, sess.audioInfo()) + } } // --- Polyphase resample to exact 48kHz --- @@ -1096,6 +1101,19 @@ func (sess *streamSession) audioInfo() AudioInfo { } } +func sendAudioInfo(subs []audioSub, info AudioInfo) { + infoJSON, _ := json.Marshal(info) + tagged := make([]byte, 1+len(infoJSON)) + tagged[0] = 0x00 // tag: audio_info + copy(tagged[1:], infoJSON) + for _, sub := range subs { + select { + case sub.ch <- tagged: + default: + } + } +} + func defaultAudioInfoForMode(mode string) AudioInfo { demodName := "NFM" if requested := normalizeRequestedMode(mode); requested != "" { diff --git a/web/app.js b/web/app.js index 0e67a79..1ff44f4 100644 --- a/web/app.js +++ b/web/app.js @@ -1509,17 +1509,17 @@ function _createSignalItem(s) { btn.dataset.bw = s.bw_hz || 0; btn.dataset.class = s.class?.mod_type || ''; btn.dataset.id = s.id || 0; - const mod = s.class?.mod_type || ''; const primaryMode = getSignalPrimaryMode(s); const mc = modColor(primaryMode); - const rds = s.class?.pll?.rds_station || ''; - const runtime = getSignalRuntimeSummary(s); - const audio = getSignalAudioSummary(s); 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 decMeta = decText || decFlags ? `${decFlags}${decFlags && decText ? ' · ' : ''}${decText}` : ''; - btn.innerHTML = `
${fmtMHz(s.center_hz, 6)}${(s.snr_db || 0).toFixed(1)} dB
${primaryMode}${runtime ? `${runtime}` : ''}BW ${fmtKHz(s.bw_hz || 0)}${audio ? `${audio}` : ''}${rds ? `${rds}` : ''}${decMeta}
`; + 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.style.borderLeftColor = mc.label; btn.style.borderLeftWidth = '3px'; btn.style.borderLeftStyle = 'solid'; @@ -1530,41 +1530,21 @@ function _createSignalItem(s) { function _patchSignalItem(el, s) { const freqEl = el.querySelector('[data-field="freq"]'); const snrEl = el.querySelector('[data-field="snr"]'); - const bwEl = el.querySelector('[data-field="bw"]'); const modeEl = el.querySelector('[data-field="mode"]'); - const runtimeEl = el.querySelector('[data-field="runtime"]'); - const audioEl = el.querySelector('[data-field="audio"]'); - const rdsEl = el.querySelector('[data-field="rds"]'); const mod = s.class?.mod_type || ''; const primaryMode = getSignalPrimaryMode(s); const mc = modColor(primaryMode); - const runtime = getSignalRuntimeSummary(s); - const audio = getSignalAudioSummary(s); - 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}`); + el.title = metaBits.join(' · '); if (freqEl) freqEl.textContent = fmtMHz(s.center_hz, 6); if (snrEl) { snrEl.textContent = `${(s.snr_db || 0).toFixed(1)} dB`; snrEl.style.color = snrColor(s.snr_db || 0); } - if (bwEl) bwEl.textContent = `BW ${fmtKHz(s.bw_hz || 0)}`; if (modeEl) { modeEl.textContent = primaryMode; modeEl.style.color = mc.label; } - if (runtimeEl) { - runtimeEl.textContent = runtime; - runtimeEl.style.display = runtime ? '' : 'none'; - } - if (audioEl) { - audioEl.textContent = audio; - audioEl.style.display = audio ? '' : 'none'; - } - if (rdsEl) { - rdsEl.textContent = rds; - rdsEl.style.display = rds ? '' : 'none'; - } else if (rds && !rdsEl) { - const span = document.createElement('span'); - span.className = 'item-meta'; - span.dataset.field = 'rds'; - span.style.color = '#fff'; - span.style.fontWeight = '700'; - span.textContent = rds; - el.querySelector('.signal-secondary')?.appendChild(span); - } el.dataset.center = s.center_hz; el.dataset.bw = s.bw_hz || 0; el.dataset.class = mod; diff --git a/web/style.css b/web/style.css index 28a1644..4f0fe67 100644 --- a/web/style.css +++ b/web/style.css @@ -367,7 +367,7 @@ input[type="range"]::-moz-range-thumb { .list-item:hover, .list-item.active { border-color: rgba(0, 255, 200, 0.28); } .list-item.listening { border-color: rgba(255, 92, 92, 0.55); box-shadow: 0 0 0 1px rgba(255, 92, 92, 0.35) inset; } .item-top, .item-bottom { display: flex; align-items: center; justify-content: space-between; gap: 8px; } -.item-top { margin-bottom: 3px; } +.item-top { margin-bottom: 2px; } .item-title { font-family: var(--mono); font-size: 0.82rem; font-weight: 700; color: #e7f1ff; } .item-meta { font-family: var(--mono); font-size: 0.68rem; color: var(--text-dim); } .item-badge { @@ -377,13 +377,14 @@ input[type="range"]::-moz-range-thumb { } .listen-meta { - margin-top: 2px; - padding: 8px 10px; + margin-top: 4px; + padding: 10px 12px; border-radius: var(--r); - border: 1px solid var(--line); - background: rgba(5, 8, 14, 0.72); + border: 1px solid rgba(50, 78, 116, 0.55); + background: linear-gradient(180deg, rgba(12, 18, 30, 0.92), rgba(7, 11, 18, 0.88)); display: grid; - gap: 5px; + gap: 7px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.03); } .listen-meta__head { display: flex; @@ -393,9 +394,9 @@ input[type="range"]::-moz-range-thumb { } .listen-meta__title { font-family: var(--mono); - font-size: 0.56rem; - font-weight: 600; - letter-spacing: 0.08em; + font-size: 0.58rem; + font-weight: 700; + letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-mute); } @@ -404,21 +405,21 @@ input[type="range"]::-moz-range-thumb { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.04em; - padding: 2px 8px; + padding: 3px 8px; border-radius: 999px; border: 1px solid var(--line); - background: rgba(15, 25, 40, 0.8); + background: rgba(15, 25, 40, 0.9); color: var(--text-dim); } .listen-meta__primary { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; - gap: 10px; + gap: 12px; } .listen-meta__mode { font-family: var(--mono); - font-size: 0.82rem; + font-size: 0.9rem; font-weight: 700; color: var(--text); } @@ -432,36 +433,15 @@ input[type="range"]::-moz-range-thumb { .listen-meta__secondary { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 10px; font-family: var(--mono); - font-size: 0.63rem; + font-size: 0.64rem; color: var(--text-dim); } - -.signal-primary { - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 0; -} -.signal-secondary { - display: inline-flex; - align-items: center; - gap: 8px; - min-width: 0; - flex-wrap: wrap; -} .item-meta--runtime { color: var(--accent); font-weight: 700; } -.item-meta--classifier { - opacity: 0.72; -} -.item-meta--live { - color: #fff; - font-weight: 700; -} /* Health Grid */ .health-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }