Kaynağa Gözat

Stream live stereo status updates and simplify signals list

master
Jan Svabenik 21 saat önce
ebeveyn
işleme
fcb18fa468
3 değiştirilmiş dosya ile 49 ekleme ve 71 silme
  1. +18
    -0
      internal/recorder/streamer.go
  2. +14
    -34
      web/app.js
  3. +17
    -37
      web/style.css

+ 18
- 0
internal/recorder/streamer.go Dosyayı Görüntüle

@@ -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 != "" {


+ 14
- 34
web/app.js Dosyayı Görüntüle

@@ -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 ? `<span class="item-meta" data-field="decision" style="opacity:0.75">${decFlags}${decFlags && decText ? ' · ' : ''}${decText}</span>` : '';
btn.innerHTML = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="signal-primary"><span class="item-meta item-meta--runtime" data-field="mode" style="color:${mc.label}">${primaryMode}</span>${runtime ? `<span class="item-meta item-meta--live" data-field="runtime">${runtime}</span>` : ''}</span><span class="signal-secondary"><span class="item-meta" data-field="bw" style="opacity:0.6">BW ${fmtKHz(s.bw_hz || 0)}</span>${audio ? `<span class="item-meta" data-field="audio">${audio}</span>` : ''}${rds ? `<span class="item-meta" data-field="rds" style="color:#fff;font-weight:700">${rds}</span>` : ''}${decMeta}</span></div>`;
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 = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="item-meta item-meta--runtime" data-field="mode" style="color:${mc.label}">${primaryMode}</span></div>`;
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;


+ 17
- 37
web/style.css Dosyayı Görüntüle

@@ -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; }


Yükleniyor…
İptal
Kaydet