Consolidate: run clip effects over the clip and a bounded tail

render_clips allocated and ran a clip-effects workspace spanning the
whole selection for every clip with effects, however short the clip.
Many tiny effect clips in a long selection multiplied allocation and
effect processing by the selection length.

Process each clip's effects in reused 4096-frame blocks from the clip's
start (the effects start from silence, so nothing comes out before it)
through its end, then keep rendering the tail until a block falls below
-180 dBFS or 10 s past the clip's end.
This commit is contained in:
Dylan M. Taylor
2026-11-02 21:55:59 -05:00
parent b35dd0f9eb
commit 292071eeff
2 changed files with 104 additions and 6 deletions
+33 -6
View File
@@ -131,33 +131,60 @@ pub fn fold_down_stereo(fmt: ChannelFormat, ch: &[Vec<f32>]) -> Vec<Vec<f32>> {
out
}
/// Block size for clip effects in [`render_clips`].
const CLIP_FX_BLOCK: usize = 4096;
/// Most a clip effect keeps rendering past its clip's end in [`render_clips`], in seconds.
const MAX_CLIP_FX_TAIL_SECONDS: f64 = 10.0;
/// A tail block whose peak is below this (-180 dBFS) ends a clip effect's tail.
const CLIP_FX_TAIL_FLOOR: f32 = 1e-9;
/// Render only one track's clips (no mixer processing) — Consolidate.
pub fn render_clips(s: &Session, track: TrackId, range: Range) -> Vec<Vec<f32>> {
let Some(t) = s.track(track) else { return Vec::new() };
let len = usize::try_from(range.len().max(0)).unwrap_or(0).min(MAX_RENDER_SAMPLES / t.channels().max(1));
let mut out = vec![vec![0.0f32; len]; t.channels().max(1)];
let mut scratch = Vec::new();
// One reusable block for every clip with effects.
let mut block: Vec<Vec<f32>> = Vec::new();
let max_tail = s.sample_rate.samples(MAX_CLIP_FX_TAIL_SECONDS).max(0);
for c in t.clips().iter().filter(|c| !c.muted && c.range().overlaps(&range)) {
match clip_fx_params(s, c.id.0) {
None => render_clip(s, c, range.start, len, &mut out, &mut scratch),
Some(params) => {
let mut tmp = vec![vec![0.0f32; len]; out.len()];
render_clip(s, c, range.start, len, &mut tmp, &mut scratch);
let mut fx = ClipFx::new(s.sample_rate.as_f64() as f32, tmp.len());
if block.len() != out.len() {
block = vec![vec![0.0f32; CLIP_FX_BLOCK]; out.len()];
}
let mut fx = ClipFx::new(s.sample_rate.as_f64() as f32, out.len());
fx.configure(params);
let mut done = 0;
// The effects start from silence, so nothing comes out before the clip starts:
// process from there through the clip and a bounded tail, not the whole selection.
let offset = |at: Samples| usize::try_from(at.saturating_sub(range.start).max(0)).unwrap_or(usize::MAX).min(len);
let first = offset(c.start);
let clip_end = offset(c.end());
let tail_end = offset(c.end().saturating_add(max_tail));
let mut done = first;
while done < len {
let n = (len - done).min(4096);
let mut block: Vec<Vec<f32>> = tmp.iter().map(|ch| ch.get(done..done + n).map(<[f32]>::to_vec).unwrap_or_default()).collect();
let n = (len - done).min(CLIP_FX_BLOCK);
for ch in &mut block {
ch.fill(0.0);
}
if done < clip_end {
render_clip(s, c, range.start.saturating_add(done as i64), n, &mut block, &mut scratch);
}
fx.process(&mut block, n);
let mut peak = 0.0f32;
for (o, b) in out.iter_mut().zip(block.iter()) {
if let Some(dst) = o.get_mut(done..done + n) {
for (x, y) in dst.iter_mut().zip(b.iter()) {
*x += *y;
peak = peak.max(y.abs());
}
}
}
done += n;
if done >= clip_end && (done >= tail_end || peak < CLIP_FX_TAIL_FLOOR) {
break;
}
}
}
}
+71
View File
@@ -1208,3 +1208,74 @@ fn gain_ramp_matches_converting_both_ends_every_sample() {
}
}
}
/// Consolidate's clip-effect pass, processing every clip's effects across the whole selection
/// (what `render_clips` did before it limited them to the clip and its tail).
fn render_clips_full_span(s: &Session, track: TrackId, range: Range) -> Vec<Vec<f32>> {
let t = s.track(track).unwrap();
let len = range.len() as usize;
let mut out = vec![vec![0.0f32; len]; t.channels().max(1)];
let mut scratch = Vec::new();
for c in t.clips().iter().filter(|c| !c.muted && c.range().overlaps(&range)) {
let params = crate::clips::clip_fx_params(s, c.id.0).unwrap();
let mut tmp = vec![vec![0.0f32; len]; out.len()];
crate::clips::render_clip(s, c, range.start, len, &mut tmp, &mut scratch);
let mut fx = crate::clips::ClipFx::new(48_000.0, tmp.len());
fx.configure(params);
for done in (0..len).step_by(4096) {
let n = (len - done).min(4096);
let mut block: Vec<Vec<f32>> = tmp.iter().map(|ch| ch[done..done + n].to_vec()).collect();
fx.process(&mut block, n);
for (o, b) in out.iter_mut().zip(&block) {
o[done..done + n].iter_mut().zip(b).for_each(|(x, y)| *x += y);
}
}
}
out
}
#[test]
fn consolidate_clip_effects_cover_the_clip_and_its_tail() {
let fx_sets: [&[(&str, f64)]; 2] = [&[("comp.lookahead", 5.0), ("comp.threshold", -20.0), ("gain", -6.0)], &[("eq.hpf_on", 1.0), ("gain", 3.0)]];
for (k, params) in fx_sets.iter().enumerate() {
let mut s = Session::default();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
s.pool.insert(SourceId(900), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.5; 4800]] })));
let cid = s.new_clip_id();
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(cid, "dc", SourceId(900), 0, 20_000, 4800));
for (param, value) in params.iter() {
s.edit.values.insert(format!("clip_fx.{}.{param}", cid.0), *value);
}
let range = Range::new(5_000, 100_000);
let got = render_clips(&s, t, range);
let want = render_clips_full_span(&s, t, range);
assert_eq!(got.len(), want.len());
assert!(want[0].iter().any(|x| x.abs() > 0.05), "the fixture should sound");
if k == 0 {
// The compressor's lookahead output after the clip's end is kept.
assert!(got[0][20_000 + 4800 - 5_000 + 10].abs() > 1e-3);
}
for (i, (a, b)) in got[0].iter().zip(&want[0]).enumerate() {
assert!((a - b).abs() < 1e-5, "set {k}, sample {i}: {a} vs {b}");
}
}
}
#[test]
fn consolidate_many_tiny_effect_clips_stays_fast() {
// Thousands of one-sample clips with effects across a long selection: each one is processed
// over its own span, not the whole selection.
let mut s = Session::default();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
s.pool.insert(SourceId(900), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.5; 1]] })));
for i in 0..2000i64 {
let cid = s.new_clip_id();
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(cid, "x", SourceId(900), 0, i * 1000, 1));
s.edit.values.insert(format!("clip_fx.{}.gain", cid.0), -6.0);
}
let start = std::time::Instant::now();
let out = render_clips(&s, t, Range::new(0, 4_000_000));
assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed());
assert!((out[0][1000] - 0.5 * db_to_gain(-6.0)).abs() < 1e-6);
assert_eq!(out[0][1001], 0.0);
}