From 0dfb0748204e5b7d4942a140a1935f098e6c70c4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:07:39 +0000 Subject: [PATCH] Shift controller events by Delay in track.write_midi_rtp Found while testing crates/engine/src/cmd/track_more.rs:370-392 (write_rtp); playback side: crates/mix/src/lib.rs:1526-1527 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/track_more.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/engine/src/cmd/track_more.rs b/crates/engine/src/cmd/track_more.rs index 290a8d5a..9f547597 100644 --- a/crates/engine/src/cmd/track_more.rs +++ b/crates/engine/src/cmd/track_more.rs @@ -383,6 +383,10 @@ fn write_rtp(e: &mut Engine, p: &Value) -> Result { n.start = n.start.saturating_add(r.delay_ticks).max(0); notes += 1; } + // Playback delays controller events too, but ignores those before tick 0 (hidden by a start trim). + for c in sequence.ctrls.iter_mut().filter(|c| c.tick >= 0) { + c.tick = c.tick.saturating_add(r.delay_ticks).max(0); + } sequence.sort(); } let pre = format!("rtp.{}.", t.0); @@ -635,6 +639,31 @@ mod tests { assert!(!e.session().edit.values.keys().any(|k| k.starts_with(&format!("rtp.{}.", keys.0)))); } + #[test] + fn rtp_delay_shifts_controller_events() { + use soundcraft_midi::{CtrlEvent, CtrlKind}; + let mut e = crate::demo::demo_engine(); + let keys = e.session().track_by_name("Keys").unwrap().id; + let s = e.session_mut(); + if let Some(pl) = s.track_mut(keys).and_then(Track::playlist_mut) + && let ClipContent::Midi { sequence } = &mut pl.clips[0].content + { + sequence.ctrls.clear(); + sequence.ctrls.push(CtrlEvent { tick: 100, channel: 0, kind: CtrlKind::Cc(7), value: 90 }); + sequence.ctrls.push(CtrlEvent { tick: 200, channel: 0, kind: CtrlKind::PitchBend, value: 10 }); + sequence.ctrls.push(CtrlEvent { tick: -30, channel: 0, kind: CtrlKind::Cc(1), value: 5 }); + } + s.edit.values.insert(track_key("rtp", keys, "delay"), 50.0); + e.execute("edit.select", &json!({"tracks": [keys.0]})).unwrap(); + e.execute("track.write_midi_rtp", &json!({})).unwrap(); + let ClipContent::Midi { sequence } = &e.session().track(keys).unwrap().clips()[0].content else { panic!() }; + let tick_of = |k: CtrlKind| sequence.ctrls.iter().find(|c| c.kind == k).map(|c| c.tick); + assert_eq!(tick_of(CtrlKind::Cc(7)), Some(150), "playback delayed the CC by 50 ticks; baked result must match"); + assert_eq!(tick_of(CtrlKind::PitchBend), Some(250)); + // Hidden by a start trim: playback ignores it, so baking leaves it alone. + assert_eq!(tick_of(CtrlKind::Cc(1)), Some(-30)); + } + #[test] fn trim_and_vca_coalesce() { let mut e = Engine::default();