From 296bfbb2e1f34b30016f65a204a8c460251d0ecb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 16:14:42 -0500 Subject: [PATCH] Strip Silence: cut and remove pieces in one pass over the playlist (#1349) * Strip Silence: cut and remove pieces in one pass over the playlist * address review: document the sorted-points order and test stretched audio offsets --- crates/engine/src/edit.rs | 39 +++++++++++++++++++ crates/engine/src/io/strip.rs | 73 ++++++++++++++++++++++++++++++++--- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs index 636c9c55..6faa03b1 100644 --- a/crates/engine/src/edit.rs +++ b/crates/engine/src/edit.rs @@ -136,6 +136,45 @@ fn split_at(s: &mut Session, track: TrackId, at: Samples, auto_name: impl Fn(&Cl count } +/// Split every clip on a track's active playlist at each of `points`, in one pass over the +/// playlist: [`separate_at_if`] once per point with `|_| false` (no piece takes an Auto Rename +/// name here), without rebuilding the playlist per point. Returns the number of splits. +/// `points` are sorted and deduplicated first, so the source offsets of stretched audio can differ +/// from per-point calls given in another order (each trim rounds its own adjustment). +pub fn separate_at_points(s: &mut Session, track: TrackId, points: &[Samples]) -> usize { + let mut pts = points.to_vec(); + pts.sort_unstable(); + pts.dedup(); + let tc_tempo = s.tempo.clone(); + let t = TimeCtx { tempo: &tc_tempo, sr: s.sample_rate }; + // The points strictly inside a clip. + let within = |c: &Clip| { + let (lo, hi) = (pts.partition_point(|p| *p <= c.start), pts.partition_point(|p| *p < c.end())); + lo..hi.max(lo) + }; + let n_split: usize = s.track(track).map_or(0, |tr| tr.clips().iter().map(|c| within(c).len()).sum()); + let mut ids: Vec = (0..n_split).map(|_| s.new_clip_id()).collect(); + let Some(pl) = s.track_mut(track).and_then(|t| t.playlist_mut()) else { return 0 }; + let mut out = Vec::with_capacity(pl.clips.len() + n_split); + for c in pl.clips.drain(..) { + let mut rest = c; + for p in &pts[within(&rest)] { + let Some(id) = ids.pop() else { break }; + match split_clip(&rest, *p, id, t) { + Some((l, r)) => { + out.push(l); + rest = r; + } + None => break, + } + } + out.push(rest); + } + pl.clips = out; + pl.sort(); + n_split +} + /// Gives `c` the next name of its Auto Rename scheme, when it has one, and counts the scheme up. fn take_auto_name(schemes: &mut std::collections::BTreeMap, c: &mut Clip) { if let Some(scheme) = c.auto_name.and_then(|k| schemes.get_mut(&k)) { diff --git a/crates/engine/src/io/strip.rs b/crates/engine/src/io/strip.rs index ec808241..981bc0db 100644 --- a/crates/engine/src/io/strip.rs +++ b/crates/engine/src/io/strip.rs @@ -117,9 +117,7 @@ pub fn strip_silence( } cut_points.push(r.end); let old: std::collections::HashSet<_> = s.track(t).map(|x| x.clips().iter().map(|c| c.id).collect()).unwrap_or_default(); - for p in &cut_points { - crate::edit::separate_at_if(s, t, *p, |_| false); - } + crate::edit::separate_at_points(s, t, &cut_points); let kept = strip_pieces(s, t, r, &keep, mode); crate::edit::auto_name_new_clips(s, t, &old, |c| !(named && c.is_audio() && c.start >= r.start && c.end() <= r.end)); Ok(kept) @@ -144,10 +142,24 @@ fn strip_pieces(s: &mut Session, t: TrackId, r: Range, keep: &[(Samples, Samples let audio: Vec = keep.iter().map(|(a, b)| Range::new(*a, *b)).collect(); let remove = if mode == StripMode::Extract { &audio } else { &gaps }; let inside = |c: &Clip, g: &Range| c.start >= g.start && c.end() <= g.end; + // The range of `rs` holding `c`: by binary search when `rs` is ascending and disjoint (the + // usual case, checked once by the caller), by scan otherwise. + let holder = |rs: &[Range], sorted: bool, c: &Clip| { + if sorted { + let i = rs.partition_point(|g| g.end < c.end()); + rs.get(i).filter(|g| inside(c, g)).map(|_| i) + } else { + rs.iter().position(|g| inside(c, g)) + } + }; + let ordered = |rs: &[Range]| rs.windows(2).all(|w| w[0].end <= w[1].start); let Some(pl) = s.track_mut(t).and_then(|x| x.playlist_mut()) else { return if mode == StripMode::Extract { 0 } else { keep.len() } }; - pl.clips.retain(|c| !remove.iter().any(|g| inside(c, g))); + let sorted = ordered(remove); + pl.clips.retain(|c| holder(remove, sorted, c).is_none()); if mode == StripMode::Extract { - return gaps.iter().filter(|g| pl.clips.iter().any(|c| inside(c, g))).count(); + let sorted = ordered(&gaps); + let held: std::collections::HashSet = pl.clips.iter().filter_map(|c| holder(&gaps, sorted, c)).collect(); + return held.len(); } keep.len() } @@ -294,6 +306,57 @@ mod strip_tests { (r, e.session().track(t).unwrap().clips().iter().map(|c| (c.start, c.end())).collect()) } + #[test] + fn cutting_at_many_points_in_one_pass_equals_cutting_at_each() { + let spans = |e: &Engine, t: TrackId| { + let mut v: Vec<_> = e.session().track(t).unwrap().clips().iter().map(|c| (c.start, c.end())).collect(); + v.sort_unstable(); + v + }; + let points = [30_000, 10, 5_000, 30_000, 120_000, 0, 20_000, 1_000_000]; + let (mut one, t) = tones(&[0, 20_000, 100_000], 40_000); + let (mut each, _) = tones(&[0, 20_000, 100_000], 40_000); + crate::edit::separate_at_points(one.session_mut(), t, &points); + for p in points { + crate::edit::separate_at_if(each.session_mut(), t, p, |_| false); + } + assert_eq!(spans(&one, t), spans(&each, t)); + assert!(spans(&one, t).len() > 3); + let ids: std::collections::HashSet<_> = one.session().track(t).unwrap().clips().iter().map(|c| c.id).collect(); + assert_eq!(ids.len(), spans(&one, t).len(), "new pieces have distinct ids"); + } + + #[test] + fn cutting_stretched_audio_at_ascending_points_matches_cutting_at_each() { + let pieces = |e: &Engine, t: TrackId| { + let mut v: Vec<_> = e + .session() + .track(t) + .unwrap() + .clips() + .iter() + .map(|c| (c.start, c.end(), if let soundcraft_model::ClipContent::Audio { offset, .. } = c.content { offset } else { -1 })) + .collect(); + v.sort_unstable(); + v + }; + let stretched = || { + let (mut e, t) = tones(&[0], 40_000); + for c in &mut e.session_mut().track_mut(t).unwrap().playlist_mut().unwrap().clips { + c.stretch = 2.0; + } + (e, t) + }; + let points = [1, 2, 3_001, 12_345]; + let ((mut one, t), (mut each, _)) = (stretched(), stretched()); + crate::edit::separate_at_points(one.session_mut(), t, &points); + for p in points { + crate::edit::separate_at_if(each.session_mut(), t, p, |_| false); + } + assert_eq!(pieces(&one, t), pieces(&each, t)); + assert_eq!(pieces(&one, t).len(), points.len() + 1); + } + #[test] fn strip_keeps_the_audio_and_removes_the_silence() { let (r, clips) = strip_mode("strip");