From ef65ceafb833c8687e9bc8e816696179d02417d6 Mon Sep 17 00:00:00 2001 From: reoniazx <76652649+reoniazx@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:21:31 +0800 Subject: [PATCH] fix(mix): exclude failed inserts from aux and master latency --- ROADMAP.md | 4 ++- crates/mix/src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++- docs/gaps.md | 8 ++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 73197650..2508cbe6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (readiness by audience with hours to ~95 %) · **Target:** Avid Pro Tools Ultimate 2026.4.1 +> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** trivial (failed insert latency compensation) · **Target:** Avid Pro Tools Ultimate 2026.4.1 SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This page is @@ -122,6 +122,7 @@ mutated and truncated movies. Field reports from the first public week are track ## Progress log +- **2026-10-16:** Aux, folder and master latency calculations exclude inserts already taken out for producing NaN or infinity, matching audio tracks. Subsequent blocks no longer compensate for an insert that is bypassed after failure. Regression tests check impulse timing, healthy insert latency and reset/retry at 64- and 512-sample blocks. No readiness percentage change. - **2026-10-13:** `mix.instrument` takes the built-in instrument's parameters in `params` (validated, clamped) and refuses any other field, so a synth setting written beside `plugin` is an error instead of being accepted and ignored (#265) - **2026-10-13:** A bounce of a bus that an aux returns onto lines up with the timeline: the aux insert's lookahead is compensated like the main mix's (it left the bus late and cut the end off), and tracks routed to that bus wait for it too (#246) - **2026-10-13:** Delay compensation covers the inserts of the master faders: a bounce of the main mix with a Maximizer on the master lines up with the same mix without it (it came out 71 samples late) (#351) @@ -155,6 +156,7 @@ mutated and truncated movies. Field reports from the first public week are track | Date | Change | Summary | |---|---|---| +| 2026-10-16 | trivial | Failed inserts no longer contribute aux, folder or master latency after containment; readiness estimates unchanged | | 2026-10-13 | trivial | Progress log: mix.instrument params, unknown fields refused (#265) | | 2026-10-13 | trivial | Progress log: bus bounce compensates aux returns (#246) | | 2026-10-13 | trivial | Progress log: master insert latency compensated (#351) | diff --git a/crates/mix/src/lib.rs b/crates/mix/src/lib.rs index e8c4fbad..670af3c4 100644 --- a/crates/mix/src/lib.rs +++ b/crates/mix/src/lib.rs @@ -1638,7 +1638,7 @@ fn note_insert_latency(t: &Track, strip: &mut Strip, pos: Samples) { let mut latency = 0usize; for (i, ins) in t.mixer.inserts.iter().enumerate() { let (Some(ins), Some(Some(slot))) = (ins, strip.plugins.get_mut(i)) else { continue }; - if !ins.active || ins.bypass { + if !ins.active || ins.bypass || slot.failed { continue; } apply_params(slot, ins, t, i, pos); @@ -2740,6 +2740,9 @@ mod tests { fn process(&mut self, io: &mut [Vec], frames: usize) { io.iter_mut().for_each(|c| c.iter_mut().take(frames).for_each(|x| *x = f32::NAN)); } + fn latency(&self) -> usize { + self.0.latency() + } } fn nan(track: TrackId, slot: usize, ch: usize) -> Vec { @@ -2786,6 +2789,71 @@ mod tests { assert_eq!(render_block(&mut eng, &s, 1024), only_b); } + #[test] + fn failed_inserts_stop_contributing_latency_until_reset() { + for kind in [TrackKind::Audio, TrackKind::Aux, TrackKind::Folder, TrackKind::Master] { + for block in [64, 512] { + let mut s = Session::default(); + s.edit.delay_compensation = true; + let source = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, None); + let mut impulse = vec![0.0; block * 6]; + for at in [block, block * 2, block * 4] { + impulse[at] = 0.25; + } + s.pool + .insert(SourceId(900), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![impulse.clone(), impulse] }))); + let cid = s.new_clip_id(); + s.track_mut(source).unwrap().playlist_mut().unwrap().clips.push(Clip::audio( + cid, + "impulses", + SourceId(900), + 0, + 0, + (block * 6) as i64, + )); + let target = if kind == TrackKind::Audio { source } else { s.add_track(kind, ChannelFormat::Stereo, None) }; + if matches!(kind, TrackKind::Aux | TrackKind::Folder) { + let bus = s.add_bus("Return", ChannelFormat::Stereo); + s.track_mut(source).unwrap().mixer.output = Route::Bus(bus); + s.track_mut(target).unwrap().mixer.input = Route::Bus(bus); + } + let mut failed = Insert::new("clap:test.nan"); + failed.params.insert("lookahead".into(), 5.0); + let mut healthy = Insert::new("compressor"); + healthy.params.insert("lookahead".into(), 1.0); + healthy.params.insert("ratio".into(), 1.0); + let tr = s.track_mut(target).unwrap(); + tr.mixer.inserts[0] = Some(failed); + tr.mixer.inserts[1] = Some(healthy); + let mut plugin = soundcraft_dsp::create("compressor").unwrap(); + plugin.prepare(48_000.0, block, 2); + let mut eng = MixEngine::new(48_000.0, block); + eng.set_external_instances(true); + eng.adopt(vec![PreparedInstance { track: target, slot: 0, id: "clap:test.nan".into(), ch: 2, plugin: Box::new(Nan(plugin)) }]); + let mut out = vec![vec![0.0; block]; 2]; + eng.render(&s, 0, block, &mut out); + assert!(out.iter().flatten().all(|x| *x == 0.0)); + assert_eq!(eng.meters[&target].plugin_failures.count, 1); + + for b in [1, 2, 4] { + if b == 4 { + // Reset retries the failed plugin once; the next block must drop its + // latency again, while keeping the healthy insert's 48-sample delay. + eng.reset(); + eng.render(&s, (block * 3) as i64, block, &mut out); + assert_eq!(eng.meters[&target].plugin_failures.count, 2); + } + eng.render(&s, (block * b) as i64, block, &mut out); + assert_eq!(eng.latency, 48, "{kind:?}, block {block}, pass {b}: only the healthy insert delays audio"); + for channel in &out { + assert_eq!(impulse_at(channel), vec![48], "{kind:?}, block {block}, pass {b}: no compensation for the failed insert"); + assert!((channel[48] - 0.25).abs() < 1e-5); + } + } + } + } + } + #[test] fn a_new_instance_replaces_a_matching_one_in_its_slot() { // The host removed and re-added the plugin, and both session updates reached the audio diff --git a/docs/gaps.md b/docs/gaps.md index f8c39b8a..3de6ea4b 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where SoundCraft falls short of Pro Tools -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G-01: audio device settings and opt-in ASIO) · **Target:** Avid Pro Tools Ultimate 2026.4.1 +> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** trivial (G-04: failed insert latency compensation) · **Target:** Avid Pro Tools Ultimate 2026.4.1 Every known shortfall, one entry each, ranked by how much it stops a Pro Tools user from doing real work in SoundCraft. This is the work list: pick from the top. Rows marked **alpha blocker** @@ -80,6 +80,11 @@ When you close a gap, delete its entry (or shrink it) in the same PR, and bump t ### G-04. Correctness bugs in "done" features +**Fixed (2026-10-16):** Aux, folder and master latency calculations now skip inserts already +disabled for producing NaN or infinity. Subsequent blocks compensate only for healthy inserts; +regression tests verify rendered impulse timing, retained healthy latency and reset/retry at +64- and 512-sample blocks, with audio tracks as the control. + Open on 2026-10-10 (several have PRs open): editing #3, #5, #140, #144; clip/session #86 (PR #123), #88 (PR #130), #91 (PR #124), #92 (PR #132); automation #104 (PR #134); mix/bounce #59 (PR #131), #103 (PR #133), #152; playback #101 (PR #129); @@ -199,6 +204,7 @@ missing. Out of scope unless the owner decides otherwise. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | trivial | G-04: failed aux/folder/master inserts excluded from latency compensation after containment; estimates unchanged | | 2026-10-12 | minor | G-01 shrunk: audio device, rate and buffer choice; opt-in ASIO | | 2026-10-10 | minor | Marked the alpha blockers (G-01, G-02, G-03; parts of G-05 and G-06) from the core-workflow gate | | 2026-10-10 | major | Created from the full re-measure against Pro Tools Ultimate 2026.4.1: 22 ranked gaps with evidence and estimates |