diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs index f83d199f..9fdb49f0 100644 --- a/crates/engine/src/edit.rs +++ b/crates/engine/src/edit.rs @@ -177,28 +177,21 @@ pub fn clear_range(s: &mut Session, track: TrackId, range: Range, shuffle: bool) removed } -/// Remove the clips `ids` from every track. In Shuffle the later clips on each track close the gaps they leave, -/// the latest gap first so the earlier ones stay where they were. Returns how many clips went. +/// Remove the clips `ids` from every track. In Shuffle the later clips on each track close the holes the movable +/// ones leave, as a Shuffle move lifts them (see [`lift_plan`]), so a crossfade partner keeps its material. Returns +/// how many clips went. pub fn remove_clips(s: &mut Session, ids: &[ClipId], shuffle: bool) -> usize { - let mut gaps: Vec<(TrackId, Range)> = Vec::new(); - for t in &mut s.tracks { - let track = t.id; - if let Some(pl) = t.playlist_mut() { - pl.clips.retain(|c| { - let gone = ids.contains(&c.id); - if gone { - gaps.push((track, c.range())); - } - !gone - }); + let ids: HashSet = ids.iter().copied().collect(); + let mut n = 0; + for track in s.tracks.iter().map(|t| t.id).collect::>() { + if shuffle { + n += lift_all(s, track, &ids).len(); } - } - let n = gaps.len(); - if shuffle { - gaps.sort_by_key(|(t, r)| (t.0, std::cmp::Reverse(r.start))); - for (track, r) in gaps { - let moved = ripple(s, track, r.end, -r.len()); - follow_automation(s, track, r.end, moved); + // Locked clips don't lift; they go without closing up, as everything does outside Shuffle. + if let Some(pl) = s.track_mut(track).and_then(|t| t.playlist_mut()) { + let before = pl.clips.len(); + pl.clips.retain(|c| !ids.contains(&c.id)); + n += before - pl.clips.len(); } } n diff --git a/crates/engine/src/edit/tests.rs b/crates/engine/src/edit/tests.rs index 82d40a42..cd84d6f0 100644 --- a/crates/engine/src/edit/tests.rs +++ b/crates/engine/src/edit/tests.rs @@ -175,6 +175,24 @@ fn remove_clips_closes_each_gap_in_shuffle_only() { assert_eq!(split(true), vec![(1000, 1000, 2000), (2000, 1000, 4000)]); } +#[test] +fn remove_clips_in_shuffle_keeps_crossfade_partners_apart() { + // `(start, end)` of each clip; the second one goes. + let remove_second = |spans: &[(Samples, Samples)]| { + let (mut s, t) = session_with_clip(); + let ids: Vec = spans.iter().map(|_| s.new_clip_id()).collect(); + if let Some(pl) = s.track_mut(t).and_then(|t| t.playlist_mut()) { + pl.clips = ids.iter().zip(spans).map(|(id, (a, b))| Clip::audio(*id, "c", SourceId(500), *a, *a, b - a)).collect(); + } + assert_eq!(remove_clips(&mut s, &[ids[1]], true), 1); + clips(&s, t).into_iter().map(|(start, len, _)| (start, start + len)).collect::>() + }; + // Crossfaded with the clip before: the hole starts where that one ends. + assert_eq!(remove_second(&[(0, 1050), (950, 2000), (2000, 3000)]), vec![(0, 1050), (1050, 2050)]); + // Crossfaded with the clip after: the hole ends where that one starts. + assert_eq!(remove_second(&[(0, 1000), (1000, 2050), (1950, 3000), (4000, 5000)]), vec![(0, 1000), (1000, 2050), (3050, 4050)]); +} + #[test] fn copy_paste_overwrites() { let (mut s, t) = session_with_clip();