diff --git a/crates/engine/src/cmd/midi.rs b/crates/engine/src/cmd/midi.rs index 10bcd767..129fd070 100644 --- a/crates/engine/src/cmd/midi.rs +++ b/crates/engine/src/cmd/midi.rs @@ -126,8 +126,8 @@ fn notes_move(e: &mut Engine, p: &Value) -> Result { let seq = seq_mut(e, id, "midi.notes_move")?; for (i, n) in seq.notes.iter_mut().enumerate() { if idx.contains(&i) { - n.start = (n.start + dt).max(0); - n.pitch = (i64::from(n.pitch) + ds).clamp(0, 127) as u8; + n.start = n.start.saturating_add(dt).max(0); + n.pitch = i64::from(n.pitch).saturating_add(ds).clamp(0, 127) as u8; } } seq.sort(); @@ -157,6 +157,21 @@ mod tests { assert!(e.execute("midi.note_edit", &json!({"clip": clip, "index": 99})).is_err()); } + #[test] + fn notes_move_huge_params_do_not_overflow() { + 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(); + e.execute("midi.note_add", &json!({"clip": clip, "pitch": 60, "start_ticks": 480})).unwrap(); + e.execute("midi.notes_move", &json!({"clip": clip, "indices": [0], "ticks": i64::MAX})).unwrap(); + let n = e.execute("midi.notes", &json!({"clip": clip})).unwrap(); + assert_eq!(n["notes"][0]["start"], i64::MAX); + e.execute("midi.notes_move", &json!({"clip": clip, "indices": [0], "semitones": i64::MAX})).unwrap(); + let n = e.execute("midi.notes", &json!({"clip": clip})).unwrap(); + assert_eq!(n["notes"][0]["pitch"], 127); + } + #[test] fn rejects_audio_tracks() { let mut e = Engine::default();