collective

Browse / github.com/storytold/soundcraft / patch

Export only the MIDI notes a trimmed clip still plays

Export only the MIDI notes a trimmed clip still plays Found while testing `crates/engine/src/io.rs:350-362 (export_midi)` (no existing issue): MIDI export writes notes that a trimmed clip no longer plays. ## Cause `export_midi` copied every note and controller of each MIDI clip's sequence and ignored the clip's length. After `Clip::trim_end_to`, notes past the new end stayed in the sequence. The mixer never plays them, but the exported file contained them. ## Fix `export_midi` now skips notes and controllers that start at or after the clip end. It also cuts the length of a note that runs past the end so it stops at the clip end. ## Verified -

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #1032, open)
Base
aa74bffac1aebfeedee0a3b60729ad8ceefc2195
Size
+27 −2 in 2 files
Review
pending
Shared
2026-10-28
Id
ce5af3e7efa0a8fe8b6f916c

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/io.rs crates/engine/tests/it/midi_offset.rs

diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs--- a/crates/engine/src/io.rs+++ b/crates/engine/src/io.rs@@ -405,12 +405,15 @@ pub fn export_midi(e: &Engine, only: Option<&[TrackId]>) -> Result<Vec<u8>> {         for c in t.clips() {             if let ClipContent::Midi { sequence } = &c.content {                 let base = s.tempo.samples_to_ticks(c.start, s.sample_rate);-                for n in &sequence.notes {+                // Only what plays: notes starting inside the clip, cut at its end.+                let clip_end = s.tempo.samples_to_ticks(c.end(), s.sample_rate);+                for n in sequence.notes.iter().filter(|n| n.start.saturating_add(base) < clip_end) {                     let mut n = *n;                     n.start = n.start.saturating_add(base);+                    n.length = n.length.min(clip_end - n.start).max(1);                     seq.notes.push(n);                 }-                for ev in &sequence.ctrls {+                for ev in sequence.ctrls.iter().filter(|ev| ev.tick.saturating_add(base) < clip_end) {                     let mut ev = *ev;                     ev.tick = ev.tick.saturating_add(base);                     seq.ctrls.push(ev);diff --git a/crates/engine/tests/it/midi_offset.rs b/crates/engine/tests/it/midi_offset.rs--- a/crates/engine/tests/it/midi_offset.rs+++ b/crates/engine/tests/it/midi_offset.rs@@ -28,3 +28,25 @@ fn midi_track_offset_moves_the_notes_like_moving_the_clip() {     e.execute("edit.move_clips", &json!({"clip": clip, "by": 24_000})).unwrap();     assert!(render(&e) == offset, "the offset must match moving the clip by 24000 samples"); }++#[test]+fn export_midi_leaves_out_what_a_trimmed_clip_no_longer_plays() {+    let mut e = Engine::default();+    let r = e.execute("track.new", &json!({"kind": "instrument", "name": "Trim", "instrument": "subtractive_synth"})).unwrap();+    let track = r["tracks"][0].as_u64().unwrap();+    let r = e.execute("midi.new_clip", &json!({"track": track, "start": 0, "length": 96_000})).unwrap();+    let clip = r["clip"].as_u64().unwrap();+    e.execute("midi.note_add", &json!({"clip": clip, "pitch": 60, "start_ticks": 0, "length_ticks": 960, "velocity": 100})).unwrap();+    e.execute("midi.note_add", &json!({"clip": clip, "pitch": 62, "start_ticks": 800, "length_ticks": 960, "velocity": 100})).unwrap();+    e.execute("midi.note_add", &json!({"clip": clip, "pitch": 64, "start_ticks": 2000, "length_ticks": 960, "velocity": 100})).unwrap();+    // 24000 samples is 960 ticks at 120 bpm.+    let id = e.session().tracks[0].clips()[0].id;+    e.session_mut().find_clip_mut(id).unwrap().trim_end_to(24_000);++    let bytes = soundcraft_engine::io::export_midi(&e, None).unwrap();+    let smf = soundcraft_midi::read_smf(&bytes).unwrap();+    let notes = &smf.tracks[0].sequence.notes;+    assert_eq!(notes.len(), 2, "only notes starting inside the clip are exported");+    let late = notes.iter().find(|n| n.pitch == 62).unwrap();+    assert_eq!(late.start + late.length, 960, "a note running past the clip end is cut at it");+}