From 5cd155bb647bcb4832280747f288b94cbcbe9cf2 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:57:23 +0200 Subject: [PATCH] Mix: a bus bounce compensates the aux returns onto it (#246) render_bus dropped only the track-insert latency from a bus, but an aux that returns onto a bus delays it by the slowest aux return as well. With Delay Compensation on, a bounce of such a bus came out late (a 5 ms lookahead left 240 leading silent frames) and its last impulse fell outside the requested end. The engine now knows which busses an aux returns onto. A bounce of one drops the aux latency too, and the tracks routed straight to that bus wait for it, as the tracks routed to the main mix already do, so the two kinds of signal on the bus arrive together. Other busses and the main mix are unchanged. --- crates/mix/src/lib.rs | 40 +++++++++++-- crates/mix/tests/bus_aux_interval.rs | 87 ++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 crates/mix/tests/bus_aux_interval.rs diff --git a/crates/mix/src/lib.rs b/crates/mix/src/lib.rs index a8c8b569..505a1ab3 100644 --- a/crates/mix/src/lib.rs +++ b/crates/mix/src/lib.rs @@ -313,6 +313,10 @@ pub struct MixEngine { /// that lines direct outputs up with aux returns stays in `latency` and is removed /// only from the main mix. track_latency: usize, + /// Busses an aux returns onto, and the latency of the slowest aux return: audio written onto + /// such a bus waits for it, so a bounce of the bus drops it as well. + aux_fed: Vec, + aux_latency: usize, /// Live input for the next block (planar), set by the audio host for input monitoring. pub input: Vec>, /// Transport stopped: only monitored inputs sound (no clips). @@ -393,6 +397,8 @@ impl MixEngine { order: Vec::new(), latency: 0, track_latency: 0, + aux_fed: Vec::new(), + aux_latency: 0, input: Vec::new(), monitor_only: false, recording: false, @@ -646,6 +652,19 @@ impl MixEngine { if pdc { self.strips.iter().filter(|(id, _)| s.track(**id).is_some_and(is_aux)).map(|(_, st)| st.latency).max().unwrap_or(0) } else { 0 }; self.track_latency = lmax_t; self.latency = lmax_t + lmax_a; + // An aux that returns onto a bus delays it by the slowest aux return, so the tracks routed + // to that bus wait as well (as the ones routed to the main mix do) and the bus lines up. + self.aux_latency = lmax_a; + self.aux_fed.clear(); + if pdc { + for t in s.tracks.iter().filter(|t| is_aux(t) && live(t)) { + if let Route::Bus(b) = &t.mixer.output + && !self.aux_fed.contains(b) + { + self.aux_fed.push(*b); + } + } + } for (i, st) in work.iter_mut() { let Some(t) = s.tracks.get(*i) else { continue }; let ch = st.buf.len(); @@ -655,7 +674,12 @@ impl MixEngine { st.align_pre.set(lmax_t.saturating_sub(st.latency), ch); st.align_pre.process(&mut st.pre, frames); } - st.out_extra.set(if t.mixer.output == Route::Main { lmax_a } else { 0 }, ch); + let waits = match &t.mixer.output { + Route::Main => true, + Route::Bus(b) => self.aux_fed.contains(b), + _ => false, + }; + st.out_extra.set(if waits { lmax_a } else { 0 }, ch); } for (i, st) in work.iter_mut() { if let Some(t) = s.tracks.get(*i) { @@ -737,6 +761,12 @@ impl MixEngine { self.main_meter.measure(&self.main, frames, 0.0); } + /// The delay-compensation latency written onto `bus`: the slowest track insert, plus the + /// slowest aux return when an aux returns onto this bus. + fn bus_latency(&self, bus: BusId) -> usize { + self.track_latency + if self.aux_fed.contains(&bus) { self.aux_latency } else { 0 } + } + /// Copy one rendered block of `bus` into `out`. A missing bus stays silent. fn copy_bus(&self, bus: BusId, frames: usize, out: &mut [Vec]) { for o in out.iter_mut() { @@ -780,7 +810,7 @@ impl MixEngine { apply_matrix(src, frames, &mut dst.buf, &m, gain); } let src: &[Vec] = match &t.mixer.output { - Route::Main if strip.out_extra.len > 0 => { + Route::Main | Route::Bus(_) if strip.out_extra.len > 0 => { if strip.outbuf.len() != strip.buf.len() { strip.outbuf = vec![Vec::new(); strip.buf.len()]; } @@ -1867,8 +1897,8 @@ pub fn render_range(s: &Session, range: Range, block: usize) -> Vec> { } /// Offline render of one bus over `range` (planar, the bus format's channel count). -/// Track-insert compensation is removed, so a bus fed by tracks lines up with the timeline. -/// An unknown bus is silent. +/// The compensation delay written onto the bus is removed (track inserts, and the aux returns +/// when an aux returns onto it), so the bus lines up with the timeline. An unknown bus is silent. pub fn render_bus(s: &Session, bus: BusId, range: Range, block: usize) -> Vec> { let nch = s.bus(bus).map(|b| b.format.channels()).unwrap_or(0); if nch == 0 { @@ -1879,7 +1909,7 @@ pub fn render_bus(s: &Session, bus: BusId, range: Range, block: usize) -> Vec (Session, BusId, BusId) { + let mut s = Session::default(); + s.edit.delay_compensation = true; + let mut signal = vec![0.0; 8192]; + for at in AT { + signal[at] = 0.25; + } + s.pool.insert(SourceId(901), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48000, channels: vec![signal.clone(), signal] }))); + let a = s.add_bus("A", ChannelFormat::Stereo); + let b = s.add_bus("B", ChannelFormat::Stereo); + let source = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, Some("Source")); + let aux = s.add_track(TrackKind::Aux, ChannelFormat::Stereo, Some("Return")); + let clip = s.new_clip_id(); + s.track_mut(source).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(clip, "impulses", SourceId(901), 0, 0, 8192)); + s.track_mut(source).unwrap().mixer.pan = vec![-1.0, 1.0]; + s.track_mut(source).unwrap().mixer.output = Route::Bus(a); + s.track_mut(aux).unwrap().mixer.pan = vec![-1.0, 1.0]; + s.track_mut(aux).unwrap().mixer.input = Route::Bus(a); + s.track_mut(aux).unwrap().mixer.output = if aux_to_main { Route::Main } else { Route::Bus(b) }; + if insert { + let mut compressor = Insert::new("compressor"); + compressor.params.insert("lookahead".into(), 5.0); + compressor.params.insert("ratio".into(), 1.0); + s.track_mut(aux).unwrap().mixer.inserts[0] = Some(compressor); + } + (s, a, b) +} + +fn positions(ch: &[f32]) -> Vec { + ch.iter().enumerate().filter(|(_, x)| x.abs() > 1e-4).map(|(i, _)| i).collect() +} + +#[test] +fn named_bus_after_aux_preserves_requested_timeline() { + let (s, a, b) = fixture(true, false); + let bus = render_bus(&s, b, Range::new(0, 8192), 1024); + let upstream = render_bus(&s, a, Range::new(0, 8192), 1024); + let (plain, _, plain_b) = fixture(false, false); + let control = render_bus(&plain, plain_b, Range::new(0, 8192), 1024); + let (main, _, _) = fixture(true, true); + let main = render_range(&main, Range::new(0, 8192), 1024); + for c in 0..2 { + // Controls: the bus before the aux, the same route without the insert, and the main mix. + assert_eq!(positions(&control[c]), AT); + assert_eq!(upstream[c], control[c]); + assert_eq!(main[c], control[c]); + // The bus after the aux: the whole interval, nothing late, nothing cut off. + assert_eq!(bus[c].len(), 8192); + assert_eq!(positions(&bus[c]), AT, "the named bus must preserve the requested interval"); + assert!(bus[c] == control[c], "the aligned samples must match the control"); + } + // A partial range lines up too. + let part = render_bus(&s, b, Range::new(2048, 4096), 1024); + assert_eq!(positions(&part[0]), vec![0]); +} + +#[test] +fn a_track_routed_to_the_same_bus_stays_aligned_with_the_aux_return() { + // A second track goes straight to bus B, where the aux (with its lookahead) also returns. + let (mut s, _, b) = fixture(true, false); + let direct = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, Some("Direct")); + let clip = s.new_clip_id(); + s.track_mut(direct).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(clip, "impulses", SourceId(901), 0, 0, 8192)); + s.track_mut(direct).unwrap().mixer.pan = vec![-1.0, 1.0]; + s.track_mut(direct).unwrap().mixer.output = Route::Bus(b); + let bus = render_bus(&s, b, Range::new(0, 8192), 1024); + for (c, ch) in bus.iter().enumerate() { + assert_eq!(positions(ch), AT, "direct and aux signals arrive together"); + for at in AT { + assert!((ch[at] - 0.5).abs() < 1e-3, "channel {c} at {at}: {}", ch[at]); + } + } +}