diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs index 5d22e7aa..ccaf145f 100644 --- a/crates/playback/src/lib.rs +++ b/crates/playback/src/lib.rs @@ -638,6 +638,21 @@ impl Player { pub fn meters(&self) -> MeterSnapshot { self.shared.meters.lock().unwrap_or_else(PoisonError::into_inner).clone() } + + /// Copy meters into reusable UI storage. Once track/channel counts are stable this performs + /// no allocation, unlike cloning a new snapshot every video frame. + pub fn copy_meters(&self, out: &mut MeterSnapshot) { + let source = self.shared.meters.lock().unwrap_or_else(PoisonError::into_inner); + copy_meter_snapshot(&source, out); + } +} + +fn copy_meter_snapshot(source: &MeterSnapshot, out: &mut MeterSnapshot) { + out.tracks.retain(|id, _| source.tracks.contains_key(id)); + for (id, meter) in &source.tracks { + out.tracks.entry(*id).or_default().copy_from(meter); + } + out.main.copy_from(&source.main); } impl Side { @@ -735,6 +750,21 @@ fn open_device(want_rate: u32, want_ch: u16) -> Result<(cpal::Device, cpal::Stre mod tests { use super::*; + #[test] + fn reusable_meter_snapshot_updates_and_drops_stale_tracks() { + let keep = TrackId(1); + let stale = TrackId(2); + let mut source = MeterSnapshot::default(); + source.tracks.insert(keep, StripMeter { peak: [0.5, 0.25], peaks: vec![0.5, 0.25], gain_reduction_db: 3.0 }); + source.main.peaks = vec![0.75, 0.5]; + let mut out = MeterSnapshot::default(); + out.tracks.insert(stale, StripMeter::default()); + copy_meter_snapshot(&source, &mut out); + assert_eq!(out.tracks.len(), 1); + assert_eq!(out.tracks[&keep], source.tracks[&keep]); + assert_eq!(out.main, source.main); + } + #[test] fn audio_state_plays_and_stops_at_end() { let s = Arc::new(soundcraft_model::Session::default()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 672af387..ad952341 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -220,6 +220,8 @@ pub struct SoundApp { pub dialogs: dialogs::Dialogs, pub meters: HashMap, pub main_meter: MeterDisplay, + /// Reused multichannel snapshot; populated at an adaptive rate by `logic`. + meter_snapshot: soundcraft_playback::MeterSnapshot, pub gesture: Option, pub edit_layout: edit_window::EditLayout, pub midi: midi_editor::MidiEditorState, @@ -247,6 +249,7 @@ pub struct SoundApp { last_write_at: Samples, pub frame_ms: f32, last_frame: Option, + last_meter_poll: f64, /// Movies for the Video window and Video track (decoded in the background). pub video: video_track::VideoPool, } @@ -280,6 +283,7 @@ impl SoundApp { dialogs: dialogs::Dialogs::default(), meters: HashMap::new(), main_meter: MeterDisplay::default(), + meter_snapshot: soundcraft_playback::MeterSnapshot::default(), gesture: None, edit_layout: edit_window::EditLayout::default(), midi: midi_editor::MidiEditorState::default(), @@ -302,6 +306,7 @@ impl SoundApp { last_write_at: i64::MIN, frame_ms: 0.0, last_frame: None, + last_meter_poll: f64::NEG_INFINITY, video: video_track::VideoPool::default(), } } @@ -727,7 +732,13 @@ impl SoundApp { if self.engine.transport.playing { self.engine.transport.position = p.position(); } - let snap = p.meters(); + let now = self.last_frame.unwrap_or_default(); + let meter_interval = if self.ui.window == MainWindow::Mix { 1.0 / 60.0 } else { 1.0 / 20.0 }; + if now - self.last_meter_poll >= meter_interval { + p.copy_meters(&mut self.meter_snapshot); + self.last_meter_poll = now; + } + let snap = &self.meter_snapshot; for (id, m) in &snap.tracks { let d = self.meters.entry(*id).or_default(); feed_meter(d, m.peak, m.gain_reduction_db, dt); @@ -807,7 +818,12 @@ impl SoundApp { { self.ensure_input(); } - if self.is_playing() || self.meters.values().any(|m| m.level[0] > 0.0001) || !self.pending_shots.is_empty() || !self.synthetic.is_empty() { + if self.is_playing() { + let hz = if self.ui.window == MainWindow::Mix { 60 } else { 30 }; + ctx.request_repaint_after(std::time::Duration::from_millis(1000 / hz)); + } else if self.meters.values().any(|m| m.level[0] > 0.0001) { + ctx.request_repaint_after(std::time::Duration::from_millis(80)); + } else if !self.pending_shots.is_empty() || !self.synthetic.is_empty() { ctx.request_repaint(); } control::collect_screenshots(self, ctx);