Merge pull request #187 from grovebit/meter-fixes

Keep armed tracks' meters live while stopped; make Clear All Clip Indicators work
This commit is contained in:
Brandon Thomas 2026-10-11 20:13:31 -04:00 committed by GitHub
commit a10eb47032
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 12 deletions

View File

@ -168,6 +168,8 @@ impl AudioState {
}
Cmd::Stop => {
self.playing = false;
self.end = None;
self.looped = None;
self.mix.reset();
}
Cmd::Locate(at) => {
@ -206,6 +208,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 {
@ -231,11 +234,6 @@ impl AudioState {
self.mix.render(&self.session, self.pos, n, &mut self.pending);
self.send_garbage();
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 {
@ -247,6 +245,10 @@ impl AudioState {
}
}
}
if self.playing {
self.pos += n as i64;
self.shared.position.store(self.pos, Ordering::Relaxed);
}
}
/// Fill an interleaved device buffer.
@ -942,4 +944,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");
}
}

View File

@ -334,6 +334,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) => {
@ -519,6 +522,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() {
@ -762,7 +771,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);
}
@ -818,7 +827,7 @@ impl SoundApp {
{
self.ensure_input();
}
if self.is_playing() {
if self.meters_live() {
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) {
@ -997,6 +1006,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

View File

@ -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<TrackId> = 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;