From b2a3b0f861a5fec4e2cc34d5eeb1bcc46b9be79a Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:10:06 +0100 Subject: [PATCH 1/2] Keep armed tracks' meters live while the transport is stopped Record-enabled and input-monitored tracks were heard while stopped, but their meters froze (#42). The audio thread returned before copying the meters whenever it wasn't playing. The UI faded every meter to zero while stopped and stopped redrawing once they reached zero. After a selection had played to its end, the stale end point also silenced input monitoring until the next Play. The audio thread now copies the meters for every block it renders and forgets the end point and loop when playback stops. While a track is armed or monitoring input, the UI keeps feeding and redrawing its meters, and the Mix window's per-channel meters include armed tracks. --- crates/playback/src/lib.rs | 38 +++++++++++++++++++++++++++----- crates/ui-egui/src/lib.rs | 10 +++++++-- crates/ui-egui/src/mix_window.rs | 6 +---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs index eb1db9b..a48934d 100644 --- a/crates/playback/src/lib.rs +++ b/crates/playback/src/lib.rs @@ -171,6 +171,8 @@ impl AudioState { } Cmd::Stop => { self.playing = false; + self.end = None; + self.looped = None; self.mix.reset(); } Cmd::Locate(at) => { @@ -209,6 +211,7 @@ impl AudioState { } else if let Some(e) = self.end { if self.pos >= e { self.playing = false; + self.end = None; self.shared.playing.store(false, Ordering::Relaxed); n = 0; } else { @@ -239,11 +242,6 @@ impl AudioState { } } self.pending_len = n; - if !self.playing { - return; - } - self.pos += n as i64; - self.shared.position.store(self.pos, Ordering::Relaxed); if let Ok(mut m) = self.shared.meters.try_lock() { m.main.copy_from(&self.mix.main_meter); for (k, v) in &self.mix.meters { @@ -255,6 +253,10 @@ impl AudioState { } } } + if self.playing { + self.pos += n as i64; + self.shared.position.store(self.pos, Ordering::Relaxed); + } } /// Fill an interleaved device buffer. @@ -896,4 +898,30 @@ mod tests { assert!(p <= 1000, "position {p} should stay inside the loop"); assert!(shared.playing.load(Ordering::Relaxed)); } + + #[test] + fn armed_tracks_are_heard_and_metered_after_playback_stops() { + use soundcraft_model::{ChannelFormat, TrackKind}; + let mut s = soundcraft_model::Session::default(); + let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None); + s.track_mut(t).unwrap().mixer.record_arm = true; + let shared = Arc::new(Shared { + position: AtomicI64::new(0), + playing: AtomicBool::new(false), + speed: AtomicU32::new(1000), + meters: Mutex::new(MeterSnapshot::default()), + }); + let (tx, rx) = channel(); + let mut st = AudioState::new(rx, Arc::clone(&shared), Arc::new(s), 48_000.0, channel().0); + let ring = record::InputRing::new(1, 48_000); + tx.send(Cmd::Input(Arc::clone(&ring))).unwrap(); + tx.send(Cmd::Play { from: 0, end: Some(1000), looped: None }).unwrap(); + let mut out = vec![0.0f32; 4096]; + st.fill(&mut out, 2); + assert!(!shared.playing.load(Ordering::Relaxed)); + ring.push(&[0.5; 4096]); + st.fill(&mut out, 2); + assert!(out.iter().any(|x| x.abs() > 0.1), "the input is heard"); + assert!(shared.meters.lock().unwrap().tracks.get(&t).is_some_and(|m| m.peak[0] > 0.1), "and metered"); + } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 955bd5a..72f9c6d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -512,6 +512,12 @@ impl SoundApp { best.map(|(_, p)| p) } + /// Whether the meters are live: while playing, and while armed or input-monitored tracks are heard. + fn meters_live(&self) -> bool { + self.engine.transport.playing + || (self.recorder.is_some() && self.engine.session().tracks.iter().any(|t| t.mixer.input_monitor || t.mixer.record_arm)) + } + /// Open the input device once (for recording and input monitoring) and feed the player. fn ensure_input(&mut self) { if self.recorder.is_some() || self.recorder_failed || self.player.is_none() { @@ -749,7 +755,7 @@ impl SoundApp { self.stop_play(); } } - if !self.engine.transport.playing { + if !self.meters_live() { for d in self.meters.values_mut() { feed_meter(d, [0.0, 0.0], 0.0, dt); } @@ -805,7 +811,7 @@ 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.meters_live() || self.meters.values().any(|m| m.level[0] > 0.0001) || !self.pending_shots.is_empty() || !self.synthetic.is_empty() { ctx.request_repaint(); } control::collect_screenshots(self, ctx); diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 0a6b84e..1173d6b 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -22,11 +22,7 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) { pub(crate) fn content(app: &mut SoundApp, ui: &mut Ui) { let ids: Vec = app.engine.session().tracks.iter().filter(|x| !x.hidden && x.kind != TrackKind::Video).map(|x| x.id).collect(); // Per-channel peaks for multichannel meters (read once per frame). - let snap = app - .player - .as_ref() - .filter(|_| app.engine.transport.playing || app.engine.session().tracks.iter().any(|t| t.mixer.input_monitor)) - .map(|p| p.meters()); + let snap = app.player.as_ref().filter(|_| app.meters_live()).map(|p| p.meters()); egui::ScrollArea::horizontal().auto_shrink([false, false]).show(ui, |ui| { ui.horizontal_top(|ui| { ui.spacing_mut().item_spacing.x = 1.0; From 7208c610cd2618eaef2e59608e7f438d9c3bbd42 Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:14:15 +0100 Subject: [PATCH 2/2] Make Clear All Clip Indicators clear them Track > Clear All Clip Indicators (Alt+C) was a stub: the red clip indicators on the meters stayed lit until another session was opened. The command now clears them on every track's meter and the main meter. --- crates/ui-egui/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 72f9c6d..fe62e80 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -329,6 +329,9 @@ impl SoundApp { // Re-selecting a scrolling mode resumes the playhead follow. self.edit_layout.follow_hold = false; } + if id == "track.clear_clip_indicators" { + self.meters.values_mut().chain([&mut self.main_meter]).for_each(|m| m.clip = false); + } Ok(v) } Err(e) => { @@ -985,6 +988,16 @@ mod tests { assert!(!app.engine.is_dirty()); } + #[test] + fn clear_all_clip_indicators_clears_every_meter() { + let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default()); + let kick = app.engine.session().track_by_name("Kick").unwrap().id; + app.meters.entry(kick).or_default().clip = true; + app.main_meter.clip = true; + app.run("track.clear_clip_indicators", json!({})).unwrap(); + assert!(!app.meters[&kick].clip && !app.main_meter.clip); + } + #[test] fn automation_first_pass_after_start_is_due_without_overflow() { // `last_write_at` starts (and resets) at `i64::MIN`: the previous