Compensate a hosted instrument's latency (#1476)

* Compensate a hosted instrument's latency

Delay compensation summed only insert latency, so a synth reporting
latency played late against everything else in playback and bounces.
run_inserts now seeds the strip latency with the instrument's.

Fixes #1474. Added a mix test that fails without the change: a latent
hosted instrument now contributes its latency to the mix.

Signed-off-by: Vladimir Babin <vovababin@gmail.com>

* test(mix): verify rendered instrument latency alignment

---------

Signed-off-by: Vladimir Babin <vovababin@gmail.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
This commit is contained in:
Vladimir BabinandFredApps authored and GitHub committed 2026-11-11 18:08:21 +01:00
1 parent ad34dbf87e
commit e86d09ab2a
2 files changed
+79 -1

No files matched your search

+1 -1
View File
@@ -334,7 +334,7 @@ fn render_instrument(s: &Session, t: &Track, strip: &mut Strip, blk: Block, tran
fn run_inserts(t: &Track, strip: &mut Strip, blk: Block, transport: &Transport) {
let Block { pos, frames, .. } = blk;
let mut gr = 0.0f32;
let mut latency = 0usize;
let mut latency = strip.instrument.as_ref().filter(|i| !i.failed).map_or(0, |i| i.plugin.latency());
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 || slot.failed {
+78
View File
@@ -941,6 +941,84 @@ fn a_plugins_bogus_latency_is_capped() {
assert_eq!((eng.track_latency, eng.latency), (MAX_LATENCY, 2 * MAX_LATENCY));
}
#[test]
fn a_hosted_instruments_latency_is_compensated() {
// #1474: a synth that reports latency plays late. Its delay must count toward the mix
// latency and align, the way an insert's does.
struct LatentSynth {
inner: Box<dyn Plugin>,
n: usize,
t: usize,
pending: Option<usize>,
}
impl Plugin for LatentSynth {
fn info(&self) -> &'static soundcraft_dsp::PluginInfo {
self.inner.info()
}
fn prepare(&mut self, sr: f32, mb: usize, ch: usize) {
self.inner.prepare(sr, mb, ch);
}
fn reset(&mut self) {
self.inner.reset();
}
fn set_param(&mut self, id: &str, v: f32) -> bool {
self.inner.set_param(id, v)
}
fn param(&self, id: &str) -> Option<f32> {
self.inner.param(id)
}
fn note_on(&mut self, offset: usize, _: u8, _: u8) {
self.pending = Some(self.t + offset + self.n);
}
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
for c in io.iter_mut() {
for (i, x) in c.iter_mut().take(frames).enumerate() {
*x = if Some(self.t + i) == self.pending { 0.25 } else { 0.0 };
}
}
self.t += frames;
}
fn latency(&self) -> usize {
self.n
}
}
use soundcraft_midi::{Note, Sequence};
let n = 500;
let mut s = Session::default();
s.edit.delay_compensation = true;
let audio = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, None);
let mut click = vec![0.0f32; 48_000];
click[24_000] = 0.25;
s.pool.insert(SourceId(1474), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![click.clone(), click] })));
let cid = s.new_clip_id();
s.track_mut(audio).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(cid, "click", SourceId(1474), 0, 0, 48_000));
let inst = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, None);
let mut seq = Sequence::default();
seq.notes.push(Note { pitch: 60, velocity: 100, release_velocity: 64, channel: 0, start: 960, length: 240 });
let mcid = s.new_clip_id();
s.track_mut(inst).unwrap().playlist_mut().unwrap().clips.push(Clip::midi(mcid, "note", 0, 48_000, seq));
s.track_mut(inst).unwrap().instrument = Some(Insert::new("clap:test.latent"));
let mut eng = MixEngine::new(48_000.0, 1024);
eng.set_external_instances(true);
eng.adopt(vec![PreparedInstance {
track: inst,
slot: INSTRUMENT_SLOT,
id: "clap:test.latent".into(),
ch: 2,
plugin: Box::new(LatentSynth { inner: soundcraft_dsp::create("gain").unwrap(), n, t: 0, pending: None }),
}]);
let mut out = vec![vec![0.0; 1024]; 2];
let mut rendered = Vec::new();
for b in 0..48 {
eng.render(&s, b * 1024, 1024, &mut out);
rendered.extend_from_slice(&out[0]);
}
assert_eq!(eng.latency, n, "the hosted instrument's latency is part of the mix latency");
let impulses: Vec<_> = rendered.iter().enumerate().filter(|(_, v)| v.abs() > 1e-6).map(|(i, _)| i).collect();
assert_eq!(impulses, vec![24_000 + n], "both the audio click and instrument impulse align");
assert!((rendered[24_000 + n] - 0.5).abs() < 1e-6, "the aligned impulses add, neither is missing");
}
#[test]
fn input_monitoring_replaces_clips() {
let (mut s, t) = session_with_dc(0.5, 4800);