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]); + } + } +}