mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 10:36:46 -06:00
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.
This commit is contained in:
+35
-5
@@ -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<BusId>,
|
||||
aux_latency: usize,
|
||||
/// Live input for the next block (planar), set by the audio host for input monitoring.
|
||||
pub input: Vec<Vec<f32>>,
|
||||
/// 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<f32>]) {
|
||||
for o in out.iter_mut() {
|
||||
@@ -780,7 +810,7 @@ impl MixEngine {
|
||||
apply_matrix(src, frames, &mut dst.buf, &m, gain);
|
||||
}
|
||||
let src: &[Vec<f32>] = 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<Vec<f32>> {
|
||||
}
|
||||
|
||||
/// 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<Vec<f32>> {
|
||||
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<Ve
|
||||
let mut probe = MixEngine::new(s.sample_rate.as_f64() as f32, block);
|
||||
let b = probe.max_block();
|
||||
probe.render(s, range.start, b.min(len.max(1)), &mut []);
|
||||
probe.track_latency
|
||||
probe.bus_latency(bus)
|
||||
};
|
||||
let total = len.saturating_add(latency);
|
||||
let mut out = vec![vec![0.0f32; total]; nch];
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! A bounce of a bus keeps the requested timeline interval when an aux returns onto the bus
|
||||
//! (its insert's lookahead is compensated like the main mix's).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use soundcraft_audio_io::AudioBuffer;
|
||||
use soundcraft_mix::{render_bus, render_range};
|
||||
use soundcraft_model::{BusId, ChannelFormat, Clip, Insert, Route, Session, SourceAudio, SourceId, TrackKind};
|
||||
use soundcraft_time::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
const AT: [usize; 3] = [0, 2048, 8000];
|
||||
|
||||
/// Impulses at 0, 2048 and 8000 on a track routed to bus A, and an aux reading bus A that returns
|
||||
/// onto bus B (or the main mix) with a unity-ratio compressor of 5 ms lookahead on it.
|
||||
fn fixture(insert: bool, aux_to_main: bool) -> (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<usize> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user