Add final mix VU meters
This commit is contained in:
parent
a6e04ae89b
commit
49b822da8c
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)>),
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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:");
|
||||
|
|
|
|||
Loading…
Reference in New Issue