mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-09 03:40:07 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d593b28bff |
@@ -125,43 +125,6 @@ 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)) {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
//! 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`), without regard to case on Windows.
|
||||
//! (`/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).
|
||||
//! - 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
|
||||
@@ -266,24 +267,12 @@ fn resolve(path: &Path) -> Result<PathBuf, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `path` (resolved) `root` or inside it, component by component (without regard to case on
|
||||
/// Windows, whose file systems ignore it)?
|
||||
/// 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.
|
||||
fn inside(path: &Path, root: &Path) -> bool {
|
||||
let mut path = path.components();
|
||||
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,
|
||||
}
|
||||
root.components().all(|r| path.next().is_some_and(|p| p == r))
|
||||
}
|
||||
|
||||
/// A Windows device name (`CON`, `NUL`, `COM1`… also with an extension), which names no file.
|
||||
@@ -476,7 +465,7 @@ mod tests {
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_ignores_case_and_refuses_devices() {
|
||||
fn windows_resolves_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}");
|
||||
|
||||
@@ -117,7 +117,9 @@ 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();
|
||||
crate::edit::separate_at_points(s, t, &cut_points);
|
||||
for p in &cut_points {
|
||||
crate::edit::separate_at_if(s, t, *p, |_| false);
|
||||
}
|
||||
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)
|
||||
@@ -142,24 +144,10 @@ 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() } };
|
||||
let sorted = ordered(remove);
|
||||
pl.clips.retain(|c| holder(remove, sorted, c).is_none());
|
||||
pl.clips.retain(|c| !remove.iter().any(|g| inside(c, g)));
|
||||
if mode == StripMode::Extract {
|
||||
let sorted = ordered(&gaps);
|
||||
let held: std::collections::HashSet<usize> = pl.clips.iter().filter_map(|c| holder(&gaps, sorted, c)).collect();
|
||||
return held.len();
|
||||
return gaps.iter().filter(|g| pl.clips.iter().any(|c| inside(c, g))).count();
|
||||
}
|
||||
keep.len()
|
||||
}
|
||||
@@ -306,26 +294,6 @@ 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");
|
||||
|
||||
Reference in New Issue
Block a user