Make the spectrum bars actually visible

Three fixes so the spectrum shows for real audio: the analyzer now
takes the peak magnitude per band with single-sided (2/N) scaling
instead of the mean (averaging diluted a strong component into the
quiet bins around it, leaving near-zero bars that render as blank
spaces); the now-playing rows use a hard Length(1) so the spectrum row
cannot be squeezed out by the info block; and the server logs when it
starts/stops streaming bars so the live path is diagnosable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-22 00:00:58 +02:00
parent 572be04206
commit 0b9970b550
3 changed files with 21 additions and 7 deletions

View File

@ -100,14 +100,16 @@ impl NowPlaying {
}
pub fn render(&self, f: &mut Frame, area: Rect) {
// Info block, progress row, then (when enabled) a spectrum row.
// Info block fills, then a fixed progress row and (when enabled)
// a fixed spectrum row. Length(1) is hard-fixed so the rows are
// never squeezed out by the block above them.
let spectrum_row = if self.spectrum_enabled { 1 } else { 0 };
let now_playing_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(6),
Constraint::Max(1),
Constraint::Max(spectrum_row),
Constraint::Min(3),
Constraint::Length(1),
Constraint::Length(spectrum_row),
])
.split(area);

View File

@ -172,11 +172,15 @@ fn spawn_spectrum_task(
let count = tap.frame_count();
if count != last_count {
last_count = count;
if !was_active {
debug!("spectrum: audio flowing, streaming bars");
}
was_active = true;
let bins = analyzer.analyze(&tap.snapshot());
let _ = update_tx.send(Update::Spectrum(SpectrumFrame { bins }));
} else if was_active {
// Playback just went idle: drop the bars to the floor once.
debug!("spectrum: audio idle, bars to zero");
was_active = false;
let _ = update_tx.send(Update::Spectrum(SpectrumFrame {
bins: vec![0.0; spectrum::SPECTRUM_BINS],

View File

@ -60,14 +60,22 @@ impl SpectrumAnalyzer {
return vec![0.0; SPECTRUM_BINS];
}
let n = self.window.len() as f32;
let magnitude = |c: &Complex<f32>| c.norm() / n;
// Single-sided amplitude (2/N): a full-scale tone reads near 1.0
// at its bin, so the bars fill for ordinary listening levels.
let magnitude = |c: &Complex<f32>| c.norm() * 2.0 / n;
(0..SPECTRUM_BINS)
.map(|bar| {
let lo = self.edges[bar];
let hi = self.edges[bar + 1].max(lo + 1).min(self.output.len());
let avg = self.output[lo..hi].iter().map(magnitude).sum::<f32>() / (hi - lo) as f32;
// Peak within the band, not the mean: a strong component
// must light its bar instead of being diluted by the
// quiet bins around it (which averaging does).
let peak = self.output[lo..hi]
.iter()
.map(magnitude)
.fold(0.0_f32, f32::max);
// Log-compress: dBFS mapped onto [0, 1].
let db = 20.0 * (avg + 1e-9).log10();
let db = 20.0 * (peak + 1e-9).log10();
((db - MIN_DB) / -MIN_DB).clamp(0.0, 1.0)
})
.collect()