collective

Browse / github.com/storytold/soundcraft / patch

Scale trim_clip file bounds by clip stretch

Scale trim_clip file bounds by clip stretch Found while testing `crates/engine/src/edit.rs:756-758,770 (trim_clip, file closure)` (no existing issue): Trim tool lets a stretched clip run past the end of its audio file. ## Cause `trim_clip` bounded trimming to the file with `start - offset .. start - offset + material_len`. That treats source samples as timeline samples and ignores `clip.stretch`. The mixer plays a clip from `offset + rel / stretch`, so the bound was wrong for any stretched clip. ## Fix In the `file` closure of `trim_clip`, the source offset and the material length are now multiplied by the clip's stretch before they a

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #1057, open)
Base
ee050db59c82316433a2bfa61ba5b8461c9339e8
Size
+24 −2 in 1 file
Review
pending
Shared
2026-10-28
Id
7340f4c666dcad9fe4d72e84

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/edit.rs

diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs--- a/crates/engine/src/edit.rs+++ b/crates/engine/src/edit.rs@@ -754,8 +754,11 @@ pub fn trim_clip(s: &mut Session, id: ClipId, start: Option<Samples>, end: Optio         lo = lo.max(0);     }     let file = |c: &Clip| {-        let start = c.start.saturating_sub(c.source_offset());-        material_len(s, c).map(|n| (start.min(c.start), start.saturating_add(n).max(c.end())))+        // The mixer plays from `offset + rel / stretch`, so file samples span `stretch` times as many timeline samples.+        let stretch = if c.stretch.is_finite() && c.stretch > 0.0 { c.stretch } else { 1.0 };+        let scale = |n: Samples| if (stretch - 1.0).abs() >= 1e-9 { (n as f64 * stretch).round() as Samples } else { n };+        let start = c.start.saturating_sub(scale(c.source_offset()));+        material_len(s, c).map(|n| (start.min(c.start), start.saturating_add(scale(n)).max(c.end())))     };     let t = TimeCtx::of(s);     let mut c = old.clone();@@ -906,6 +909,25 @@ mod tests {         s.track(t).unwrap().clips().iter().map(|c| (c.start, c.length, c.source_offset())).collect()     } +    #[test]+    fn trim_clip_bounds_stretched_clip_to_its_file() {+        for (stretch, length, want) in [(0.5, 5000, 5000), (2.0, 1000, 20_000)] {+            let (mut s, t) = session_with_clip();+            let id = {+                let c = s.track_mut(t).and_then(|t| t.playlist_mut()).and_then(|p| p.clips.first_mut()).unwrap();+                c.stretch = stretch;+                c.start = 0;+                c.length = length;+                if let ClipContent::Audio { offset, .. } = &mut c.content {+                    *offset = 0;+                }+                c.id+            };+            let c = trim_clip(&mut s, id, None, Some(99_999), false).unwrap();+            assert_eq!(c.end(), want, "stretch {stretch}");+        }+    }+     #[test]     fn copy_range_resets_fades_at_cut_edges() {         let (mut s, t) = session_with_clip();