Add final mix VU meters

This commit is contained in:
Skyler Lehmkuhl 2026-03-01 15:04:58 -05:00
parent a6e04ae89b
commit 49b822da8c
5 changed files with 82 additions and 52 deletions

View File

@ -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 {

View File

@ -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)>),

View File

@ -816,7 +816,7 @@ struct EditorApp {
// VU meter levels
input_level: f32,
output_level: f32,
output_level: (f32, f32),
track_levels: HashMap<daw_backend::TrackId, f32>,
/// 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);

View File

@ -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<daw_backend::TrackId, f32>,
#[allow(dead_code)] // Available for panes that need reverse track->layer lookup
pub track_to_layer_map: &'a std::collections::HashMap<daw_backend::TrackId, Uuid>,

View File

@ -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:");