Browse / github.com/storytold/soundcraft / patch
Keep MIDI note spacing when moving notes left past tick 0
Keep MIDI note spacing when moving notes left past tick 0 Found while testing `crates/engine/src/cmd/midi.rs:128-131 (notes_move)` (no existing issue): Moving several MIDI notes left past tick 0 squeezes them together. ## Cause midi.notes_move added the tick offset to each selected note and clamped each start to 0 on its own. Notes that would land before tick 0 piled up there, so the gaps between them shrank. ## Fix notes_move now clamps the shared offset so the earliest selected note stops at tick 0, and the selection keeps its spacing. The clamp only applies to a left move that would overflow; zero and positive offsets are not changed. The regres
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #1138, open)
- Base
60087e0f46032118bf309ed9ea161a9b4a872713- Size
- +36 −0 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
9816ed5e8a368c1a6a07fec5
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/midi.rs
@@ -124,6 +124,12 @@ fn notes_move(e: &mut Engine, p: &Value) -> Result<Value> { let dt = i64_or(p, "ticks", 0); let ds = i64_or(p, "semitones", 0); let seq = seq_mut(e, id, "midi.notes_move")?;+ // Clamp the shared delta so the earliest selected note stops at tick 0 and the selection keeps its spacing.+ let earliest = seq.notes.iter().enumerate().filter(|(i, _)| idx.contains(i)).map(|(_, n)| n.start).min();+ let dt = match earliest {+ Some(m) => dt.max(0i64.saturating_sub(m).min(0)),+ None => dt,+ }; for (i, n) in seq.notes.iter_mut().enumerate() { if idx.contains(&i) { n.start = n.start.saturating_add(dt).max(0);@@ -168,6 +174,36 @@ mod tests { assert!(e.execute("midi.note_edit", &json!({"clip": clip, "index": 99})).is_err()); } + #[test]+ fn notes_move_left_past_zero_keeps_spacing() {+ let mut e = Engine::default();+ let t = e.session_mut().add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some("Keys"));+ let r = e.execute("midi.new_clip", &json!({"track": t.0})).unwrap();+ let clip = r["clip"].as_u64().unwrap();+ for s in [100, 200, 300] {+ e.execute("midi.note_add", &json!({"clip": clip, "pitch": 60, "start_ticks": s})).unwrap();+ }+ e.execute("midi.notes_move", &json!({"clip": clip, "indices": [0, 1], "ticks": -150})).unwrap();+ let n = e.execute("midi.notes", &json!({"clip": clip})).unwrap();+ let starts: Vec<i64> = n["notes"].as_array().unwrap().iter().map(|x| x["start"].as_i64().unwrap()).collect();+ assert_eq!(starts, [0, 100, 300]);+ }++ #[test]+ fn notes_move_without_left_overflow_does_not_shift_selection() {+ let mut e = Engine::default();+ let t = e.session_mut().add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some("Keys"));+ let clip = e.execute("midi.new_clip", &json!({"track": t.0})).unwrap()["clip"].as_u64().unwrap();+ for s in [0, 100] {+ e.execute("midi.note_add", &json!({"clip": clip, "pitch": 60, "start_ticks": s})).unwrap();+ }+ seq_mut(&mut e, ClipId(clip), "test").unwrap().notes[0].start = -50;+ e.execute("midi.notes_move", &json!({"clip": clip, "indices": [0, 1], "ticks": 0})).unwrap();+ let n = e.execute("midi.notes", &json!({"clip": clip})).unwrap();+ let starts: Vec<i64> = n["notes"].as_array().unwrap().iter().map(|x| x["start"].as_i64().unwrap()).collect();+ assert_eq!(starts, [0, 100]);+ }+ #[test] fn notes_move_huge_params_do_not_overflow() { let mut e = Engine::default();