mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-10 20:30:11 -06:00
Mix: delay compensation covers the master faders' inserts (#351)
The master faders process the main mix after every track and aux return has been summed, so a lookahead insert on them (the Maximizer: 71 samples at 48 kHz) delays the whole main mix. The mix's compensation latency, which a bounce drops from the head of the render, left it out, so the bounce came out late by the insert's latency (the voice was 71 samples later than in the same mix without the Maximizer). The latency of the master inserts is now part of the mix latency. With Delay Compensation off nothing changes.
This commit is contained in:
+14
-2
@@ -306,7 +306,8 @@ pub struct MixEngine {
|
||||
/// Snapshot key of the session the strips/order were synced to.
|
||||
synced: (usize, usize, usize),
|
||||
order: Vec<usize>,
|
||||
/// Current total compensation delay (samples) of the mix.
|
||||
/// Current total compensation delay (samples) of the main mix: the slowest track and aux
|
||||
/// returns, and the master faders' inserts.
|
||||
pub latency: usize,
|
||||
/// Delay already applied to audio written onto busses: the slowest track insert.
|
||||
/// A bus render drops this so the bus lines up with the timeline. The extra delay
|
||||
@@ -641,11 +642,22 @@ impl MixEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The master faders' inserts delay the whole main mix, so their latency is part of the
|
||||
// mix's too (the faders run one after the other: it adds up).
|
||||
let mut lmax_m = 0usize;
|
||||
if pdc {
|
||||
for t in s.tracks.iter().filter(|t| t.kind == TrackKind::Master && !t.inactive) {
|
||||
if let Some(st) = self.strips.get_mut(&t.id) {
|
||||
note_insert_latency(t, st, pos);
|
||||
lmax_m = lmax_m.saturating_add(st.latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
let lmax_t = if pdc { work.iter().map(|(_, st)| st.latency).max().unwrap_or(0) } else { 0 };
|
||||
let lmax_a =
|
||||
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;
|
||||
self.latency = lmax_t + lmax_a + lmax_m;
|
||||
for (i, st) in work.iter_mut() {
|
||||
let Some(t) = s.tracks.get(*i) else { continue };
|
||||
let ch = st.buf.len();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Delay compensation covers the inserts of the master faders: a bounce of the main mix lines up
|
||||
//! with the same mix without them.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use soundcraft_audio_io::AudioBuffer;
|
||||
use soundcraft_mix::{MixEngine, render_range};
|
||||
use soundcraft_model::{ChannelFormat, Clip, Insert, Route, Session, SourceAudio, SourceId, TrackKind};
|
||||
use soundcraft_time::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
const AT: usize = 2000;
|
||||
|
||||
/// One track with an impulse at `AT` (and a loud steady tone after it, so a limiter has work),
|
||||
/// routed to the main mix, plus a master fader carrying `insert` when given.
|
||||
fn session(insert: Option<Insert>) -> Session {
|
||||
let mut s = Session::default();
|
||||
s.edit.delay_compensation = true;
|
||||
let mut signal = vec![0.0f32; 12_000];
|
||||
signal[AT] = 0.9;
|
||||
s.pool.insert(SourceId(902), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![signal.clone(), signal] })));
|
||||
let t = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, Some("Voice"));
|
||||
let clip = s.new_clip_id();
|
||||
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(clip, "impulse", SourceId(902), 0, 0, 12_000));
|
||||
s.track_mut(t).unwrap().mixer.pan = vec![-1.0, 1.0];
|
||||
s.track_mut(t).unwrap().mixer.output = Route::Main;
|
||||
if let Some(insert) = insert {
|
||||
let m = s.add_track(TrackKind::Master, ChannelFormat::Stereo, Some("Master"));
|
||||
s.track_mut(m).unwrap().mixer.inserts[0] = Some(insert);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn peak_at(ch: &[f32]) -> usize {
|
||||
ch.iter().enumerate().max_by(|a, b| a.1.abs().total_cmp(&b.1.abs())).map(|(i, _)| i).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_master_insert_with_lookahead_does_not_delay_the_bounce() {
|
||||
let control = render_range(&session(None), Range::new(0, 12_000), 512);
|
||||
assert_eq!(peak_at(&control[0]), AT);
|
||||
let mut maximizer = Insert::new("maximizer");
|
||||
maximizer.params.insert("threshold".into(), -7.0);
|
||||
maximizer.params.insert("ceiling".into(), -1.0);
|
||||
let s = session(Some(maximizer));
|
||||
let out = render_range(&s, Range::new(0, 12_000), 512);
|
||||
assert_eq!(out[0].len(), 12_000);
|
||||
assert_eq!(peak_at(&out[0]), AT, "the limited impulse lines up with the mix without the master insert");
|
||||
assert_eq!(peak_at(&out[1]), AT);
|
||||
let mut probe = MixEngine::new(48_000.0, 512);
|
||||
let mut tmp = vec![vec![0.0f32; 512]; 2];
|
||||
probe.render(&s, 0, 512, &mut tmp);
|
||||
assert!(probe.latency > 0, "the Maximizer reports a lookahead");
|
||||
// With Delay Compensation off nothing is compensated (and nothing is dropped).
|
||||
let mut off = s.clone();
|
||||
off.edit.delay_compensation = false;
|
||||
let late = render_range(&off, Range::new(0, 12_000), 512);
|
||||
assert_eq!(peak_at(&late[0]), AT + probe.latency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn master_inserts_without_latency_change_nothing() {
|
||||
let control = render_range(&session(None), Range::new(0, 12_000), 512);
|
||||
let gain = render_range(&session(Some(Insert::new("gain"))), Range::new(0, 12_000), 512);
|
||||
assert_eq!(peak_at(&gain[0]), AT);
|
||||
assert_eq!(gain[0].len(), control[0].len());
|
||||
}
|
||||
Reference in New Issue
Block a user