From e1c4987db54797931c31c2e36d1941db4da14cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 30 Jan 2026 21:50:00 +0100 Subject: [PATCH] Feat: fix scope / spectrum / vumeter --- crates/ratatui/src/scope.rs | 12 ++++++++++-- src/engine/audio.rs | 23 +++++++++++++++-------- src/state/audio.rs | 4 ++-- src/views/help_view.rs | 6 ------ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/crates/ratatui/src/scope.rs b/crates/ratatui/src/scope.rs index 299beb0..4bdbda8 100644 --- a/crates/ratatui/src/scope.rs +++ b/crates/ratatui/src/scope.rs @@ -64,6 +64,10 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g let fine_width = width * 2; let fine_height = height * 4; + // Auto-scale: find peak amplitude and normalize to fill height + let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max); + let auto_gain = if peak > 0.001 { gain / peak } else { gain }; + PATTERNS.with(|p| { let mut patterns = p.borrow_mut(); let size = width * height; @@ -72,7 +76,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g for fine_x in 0..fine_width { let sample_idx = (fine_x * data.len()) / fine_width; - let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); + let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0); let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize; let fine_y = fine_y.min(fine_height - 1); @@ -117,6 +121,10 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai let fine_width = width * 2; let fine_height = height * 4; + // Auto-scale: find peak amplitude and normalize to fill width + let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max); + let auto_gain = if peak > 0.001 { gain / peak } else { gain }; + PATTERNS.with(|p| { let mut patterns = p.borrow_mut(); let size = width * height; @@ -125,7 +133,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai for fine_y in 0..fine_height { let sample_idx = (fine_y * data.len()) / fine_height; - let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); + let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0); let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize; let fine_x = fine_x.min(fine_width - 1); diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 6deb8eb..7076126 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -11,7 +11,7 @@ use std::thread::{self, JoinHandle}; use super::AudioCommand; pub struct ScopeBuffer { - pub samples: [AtomicU32; 64], + pub samples: [AtomicU32; 256], peak_left: AtomicU32, peak_right: AtomicU32, } @@ -29,12 +29,19 @@ impl ScopeBuffer { let mut peak_l: f32 = 0.0; let mut peak_r: f32 = 0.0; + // Calculate peaks from ALL input frames for accurate VU metering + for chunk in data.chunks(2) { + if let [left, right] = chunk { + peak_l = peak_l.max(left.abs()); + peak_r = peak_r.max(right.abs()); + } + } + + // Downsample for scope display + let frames = data.len() / 2; for (i, atom) in self.samples.iter().enumerate() { - let idx = i * 2; - let left = data.get(idx).copied().unwrap_or(0.0); - let right = data.get(idx + 1).copied().unwrap_or(0.0); - peak_l = peak_l.max(left.abs()); - peak_r = peak_r.max(right.abs()); + let frame_idx = (i * frames) / self.samples.len(); + let left = data.get(frame_idx * 2).copied().unwrap_or(0.0); atom.store(left.to_bits(), Ordering::Relaxed); } @@ -42,7 +49,7 @@ impl ScopeBuffer { self.peak_right.store(peak_r.to_bits(), Ordering::Relaxed); } - pub fn read(&self) -> [f32; 64] { + pub fn read(&self) -> [f32; 256] { std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed))) } @@ -146,7 +153,7 @@ impl SpectrumAnalyzer { let hi = self.band_edges[band + 1].max(lo + 1); let sum: f32 = self.fft_buf[lo..hi].iter().map(|c| c.norm()).sum(); let avg = sum / (hi - lo) as f32; - let amplitude = avg / (FFT_SIZE as f32 / 2.0); + let amplitude = avg / (FFT_SIZE as f32 / 4.0); let db = 20.0 * amplitude.max(1e-10).log10(); *mag = ((db + 60.0) / 60.0).clamp(0.0, 1.0); } diff --git a/src/state/audio.rs b/src/state/audio.rs index af16112..8cf2a70 100644 --- a/src/state/audio.rs +++ b/src/state/audio.rs @@ -174,7 +174,7 @@ pub struct Metrics { pub peak_voices: usize, pub cpu_load: f32, pub schedule_depth: usize, - pub scope: [f32; 64], + pub scope: [f32; 256], pub peak_left: f32, pub peak_right: f32, pub spectrum: [f32; 32], @@ -190,7 +190,7 @@ impl Default for Metrics { peak_voices: 0, cpu_load: 0.0, schedule_depth: 0, - scope: [0.0; 64], + scope: [0.0; 256], peak_left: 0.0, peak_right: 0.0, spectrum: [0.0; 32], diff --git a/src/views/help_view.rs b/src/views/help_view.rs index b986e95..1a636fa 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -245,7 +245,6 @@ fn preprocess_underscores(md: &str) -> String { fn parse_markdown(md: &str) -> Vec> { let processed = preprocess_underscores(md); - eprintln!("DEBUG parse_markdown: processed={:?}", &processed[..100.min(processed.len())]); let text = minimad::Text::from(processed.as_str()); let mut lines = Vec::new(); @@ -316,11 +315,6 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec