From 49b822da8c133fd876abeef902c8b1b06d20fd1c Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 1 Mar 2026 15:04:58 -0500 Subject: [PATCH] Add final mix VU meters --- daw-backend/src/audio/engine.rs | 56 ++++++++++++------- daw-backend/src/command/types.rs | 4 +- .../lightningbeam-editor/src/main.rs | 35 +++--------- .../lightningbeam-editor/src/panes/mod.rs | 3 +- .../src/panes/timeline.rs | 36 ++++++++++++ 5 files changed, 82 insertions(+), 52 deletions(-) diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index d6f4713..b07f4ac 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -76,7 +76,8 @@ pub struct Engine { input_gain: f32, input_level_peak: f32, input_level_counter: usize, - output_level_peak: f32, + output_level_peak_l: f32, + output_level_peak_r: f32, output_level_counter: usize, track_level_counter: usize, @@ -151,7 +152,8 @@ impl Engine { input_gain: 1.0, input_level_peak: 0.0, input_level_counter: 0, - output_level_peak: 0.0, + output_level_peak_l: 0.0, + output_level_peak_r: 0.0, output_level_counter: 0, track_level_counter: 0, debug_audio: std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"), @@ -361,25 +363,6 @@ impl Engine { self.channels, ); - // Compute output peak for master VU meter - let output_peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max); - self.output_level_peak = self.output_level_peak.max(output_peak); - self.output_level_counter += output.len(); - let meter_interval = self.sample_rate as usize / 20; // ~50ms - if self.output_level_counter >= meter_interval { - let _ = self.event_tx.push(AudioEvent::OutputLevel(self.output_level_peak)); - self.output_level_peak = 0.0; - self.output_level_counter = 0; - } - - // Send per-track peak levels periodically (~50ms) - self.track_level_counter += output.len(); - if self.track_level_counter >= meter_interval { - let levels = self.project.collect_track_peaks(); - let _ = self.event_tx.push(AudioEvent::TrackLevels(levels)); - self.track_level_counter = 0; - } - // Update playhead (convert total samples to frames) self.playhead += (output.len() / self.channels as usize) as u64; @@ -415,6 +398,37 @@ impl Engine { self.process_live_midi(output); } + // Compute stereo output peaks for master VU meter (independent of playback state) + { + let channels = self.channels as usize; + for frame in output.chunks(channels) { + if channels >= 2 { + self.output_level_peak_l = self.output_level_peak_l.max(frame[0].abs()); + self.output_level_peak_r = self.output_level_peak_r.max(frame[1].abs()); + } else { + let v = frame[0].abs(); + self.output_level_peak_l = self.output_level_peak_l.max(v); + self.output_level_peak_r = self.output_level_peak_r.max(v); + } + } + self.output_level_counter += output.len(); + let meter_interval = self.sample_rate as usize / 20; // ~50ms + if self.output_level_counter >= meter_interval { + let _ = self.event_tx.push(AudioEvent::OutputLevel(self.output_level_peak_l, self.output_level_peak_r)); + self.output_level_peak_l = 0.0; + self.output_level_peak_r = 0.0; + self.output_level_counter = 0; + } + + // Send per-track peak levels periodically + self.track_level_counter += output.len(); + if self.track_level_counter >= meter_interval { + let levels = self.project.collect_track_peaks(); + let _ = self.event_tx.push(AudioEvent::TrackLevels(levels)); + self.track_level_counter = 0; + } + } + // Process input monitoring and/or recording (independent of playback state) let is_recording = self.recording_state.is_some(); if is_recording || self.input_monitoring { diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index 75a1560..a5611c9 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -341,8 +341,8 @@ pub enum AudioEvent { /// Peak amplitude of mic input (for input monitoring meter) InputLevel(f32), - /// Peak amplitude of mix output (for master meter) - OutputLevel(f32), + /// Peak amplitude of mix output (for master meter), stereo (left, right) + OutputLevel(f32, f32), /// Per-track playback peak levels TrackLevels(Vec<(TrackId, f32)>), diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index a418258..cbe5393 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -816,7 +816,7 @@ struct EditorApp { // VU meter levels input_level: f32, - output_level: f32, + output_level: (f32, f32), track_levels: HashMap, /// Cache for MIDI event data (keyed by backend midi_clip_id) @@ -1063,7 +1063,7 @@ impl EditorApp { region_selection: None, region_select_mode: lightningbeam_core::tool::RegionSelectMode::default(), input_level: 0.0, - output_level: 0.0, + output_level: (0.0, 0.0), track_levels: HashMap::new(), midi_event_cache: HashMap::new(), // Initialize empty MIDI event cache audio_duration_cache: HashMap::new(), // Initialize empty audio duration cache @@ -4683,8 +4683,9 @@ impl eframe::App for EditorApp { AudioEvent::InputLevel(peak) => { self.input_level = self.input_level.max(peak); } - AudioEvent::OutputLevel(peak) => { - self.output_level = self.output_level.max(peak); + AudioEvent::OutputLevel(peak_l, peak_r) => { + self.output_level.0 = self.output_level.0.max(peak_l); + self.output_level.1 = self.output_level.1.max(peak_r); } AudioEvent::TrackLevels(levels) => { for (track_id, peak) in levels { @@ -4725,13 +4726,14 @@ impl eframe::App for EditorApp { { let decay = 0.97f32; self.input_level *= decay; - self.output_level *= decay; + self.output_level.0 *= decay; + self.output_level.1 *= decay; for level in self.track_levels.values_mut() { *level *= decay; } // Request repaint while any level is visible let any_active = self.input_level > 0.001 - || self.output_level > 0.001 + || self.output_level.0 > 0.001 || self.output_level.1 > 0.001 || self.track_levels.values().any(|&v| v > 0.001); if any_active { ctx.request_repaint(); @@ -4977,27 +4979,6 @@ impl eframe::App for EditorApp { } }); - // Mix output VU meter (thin bar below menu) - if self.app_mode != AppMode::StartScreen && self.output_level > 0.001 { - egui::TopBottomPanel::top("mix_meter").exact_height(4.0).show(ctx, |ui| { - let rect = ui.available_rect_before_wrap(); - let level = self.output_level.min(1.0); - let filled_width = rect.width() * level; - let color = if level > 0.9 { - egui::Color32::from_rgb(220, 50, 50) - } else if level > 0.7 { - egui::Color32::from_rgb(220, 200, 50) - } else { - egui::Color32::from_rgb(50, 200, 80) - }; - let filled_rect = egui::Rect::from_min_size( - rect.left_top(), - egui::vec2(filled_width, rect.height()), - ); - ui.painter().rect_filled(filled_rect, 0.0, color); - }); - } - // Render start screen or editor based on app mode if self.app_mode == AppMode::StartScreen { self.render_start_screen(ctx); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index 5983c55..eee3aaf 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -246,8 +246,7 @@ pub struct SharedPaneState<'a> { pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager, // VU meter levels pub input_level: f32, - #[allow(dead_code)] // Used by mix meter in main.rs, available to panes - pub output_level: f32, + pub output_level: (f32, f32), pub track_levels: &'a std::collections::HashMap, #[allow(dead_code)] // Available for panes that need reverse track->layer lookup pub track_to_layer_map: &'a std::collections::HashMap, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index dd4b44a..dc6322a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -4234,6 +4234,42 @@ impl PaneRenderer for TimelinePane { ui.separator(); + // Stereo mix output VU meter (two stacked bars: L on top, R on bottom) + { + let meter_width = 80.0; + let meter_height = 14.0; // total height for both bars + gap + let bar_height = 6.0; + let gap = 2.0; + let (meter_rect, _) = ui.allocate_exact_size( + egui::vec2(meter_width, meter_height), + egui::Sense::hover(), + ); + // Background + ui.painter().rect_filled(meter_rect, 2.0, egui::Color32::from_gray(30)); + + let levels = [shared.output_level.0.min(1.0), shared.output_level.1.min(1.0)]; + for (i, &level) in levels.iter().enumerate() { + let bar_y = meter_rect.min.y + i as f32 * (bar_height + gap); + if level > 0.001 { + let filled_width = meter_rect.width() * level; + let color = if level > 0.9 { + egui::Color32::from_rgb(220, 50, 50) + } else if level > 0.7 { + egui::Color32::from_rgb(220, 200, 50) + } else { + egui::Color32::from_rgb(50, 200, 80) + }; + let filled_rect = egui::Rect::from_min_size( + egui::pos2(meter_rect.min.x, bar_y), + egui::vec2(filled_width, bar_height), + ); + ui.painter().rect_filled(filled_rect, 1.0, color); + } + } + } + + ui.separator(); + // BPM control let mut bpm_val = bpm; ui.label("BPM:");