Compare commits

..
3 changed files with 92 additions and 12 deletions
+37
View File
@@ -125,6 +125,43 @@ pub fn separate_at_if(s: &mut Session, track: TrackId, at: Samples, auto_name: i
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.
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<ClipId> = (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<u64, soundcraft_model::AutoName>, c: &mut Clip) {
if let Some(scheme) = c.auto_name.and_then(|k| schemes.get_mut(&k)) {
+18 -7
View File
@@ -10,8 +10,7 @@
//! folder, the rest of the path being plain names (`..` below a missing folder is refused, and so
//! is a link that leads nowhere).
//! - The result must lie inside one of the roots of its kind, compared by whole components
//! (`/work2` isn't inside `/work`), by the case the disk stores (links are resolved first, so a
//! differently cased spelling of a root still lands inside it).
//! (`/work2` isn't inside `/work`), without regard to case on Windows.
//! - Read and write access are separate: a root given alone grants only its own kind of access.
//!
//! Roots are in force on a thread inside [`confine`], and in the whole process after
@@ -267,12 +266,24 @@ fn resolve(path: &Path) -> Result<PathBuf, String> {
}
}
/// Is `path` (resolved) `root` or inside it, component by component? Both come from
/// `canonicalize`, which gives the name's case as the disk stores it, so equal means the same
/// folder; folding case would equate two folders on a case-sensitive Windows directory.
/// Is `path` (resolved) `root` or inside it, component by component (without regard to case on
/// Windows, whose file systems ignore it)?
fn inside(path: &Path, root: &Path) -> bool {
let mut path = path.components();
root.components().all(|r| path.next().is_some_and(|p| p == r))
root.components().all(|r| path.next().is_some_and(|p| same_component(p, r)))
}
fn same_component(a: Component, b: Component) -> bool {
if a == b {
return true;
}
if !cfg!(windows) {
return false;
}
match (a.as_os_str().to_str(), b.as_os_str().to_str()) {
(Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
_ => false,
}
}
/// A Windows device name (`CON`, `NUL`, `COM1`… also with an extension), which names no file.
@@ -465,7 +476,7 @@ mod tests {
#[cfg(windows)]
#[test]
fn windows_resolves_case_and_refuses_devices() {
fn windows_ignores_case_and_refuses_devices() {
let (base, roots) = folders("case");
let upper = s(base.join("IN/a.scraft"));
assert_eq!(roots.check(&upper, Access::Read), Ok(()), "{upper}");
+37 -5
View File
@@ -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<Range> = 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<usize> = pl.clips.iter().filter_map(|c| holder(&gaps, sorted, c)).collect();
return held.len();
}
keep.len()
}
@@ -294,6 +306,26 @@ 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 strip_keeps_the_audio_and_removes_the_silence() {
let (r, clips) = strip_mode("strip");