From f188b66209b70e54333518b9a58b143bd15272d0 Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:42:15 +0100 Subject: [PATCH] Leave controller events before a clip's start silent A MIDI clip whose start was trimmed may keep the events before its new start at negative ticks, hidden until the start is trimmed out again (midi-clip-trim does this). The controller loop clamped every event's tick to 0, so a hidden sustain pedal or pitch bend played at the clip start, and the chase after a seek counted hidden values as the state in effect. Both loops now skip events before tick 0. Nothing changes for clips without hidden events: notes and controllers made by the app never start before tick 0. Test: a sustain-pedal event at tick -480 is neither sent when the clip starts nor chased after a seek into the clip, while a visible controller is still chased. --- crates/mix/src/lib.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/mix/src/lib.rs b/crates/mix/src/lib.rs index bae8545..5510a83 100644 --- a/crates/mix/src/lib.rs +++ b/crates/mix/src/lib.rs @@ -1368,7 +1368,8 @@ fn schedule_notes(s: &Session, t: &Track, pos: Samples, frames: usize, inst: &mu } let near = s.tempo.samples_to_ticks(pos, sr).saturating_sub(base).saturating_sub(1) ..=s.tempo.samples_to_ticks(end, sr).saturating_sub(base).saturating_add(1); - for c in &sequence.ctrls { + // Events before tick 0 are what a start trim hid: they don't play. + for c in sequence.ctrls.iter().filter(|c| c.tick >= 0) { let tick = c.tick.saturating_add(delay).max(0); if !near.contains(&tick) { continue; @@ -1413,7 +1414,7 @@ fn chase_controllers(s: &Session, t: &Track, pos: Samples, inst: &mut PluginSlot } let base = s.tempo.samples_to_ticks(clip.start, sr); let until = pos.min(clip.end()); - for c in &sequence.ctrls { + for c in sequence.ctrls.iter().filter(|c| c.tick >= 0) { let slot = match c.kind { CtrlKind::Cc(n) => usize::from(n & 0x7F), CtrlKind::PitchBend => BEND, @@ -2380,6 +2381,27 @@ mod tests { assert!(heard.lock().unwrap().is_empty()); } + #[test] + fn controllers_hidden_before_a_clip_start_are_neither_played_nor_chased() { + let mut s = Session::default(); + let t = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, None); + s.track_mut(t).unwrap().instrument = Some(Insert::new("clap:test.ears")); + let cc = |tick, kind, value| soundcraft_midi::CtrlEvent { tick, channel: 0, kind, value }; + let ctrls = vec![cc(-480, CtrlKind::Cc(64), 127), cc(960, CtrlKind::Cc(1), 64)]; + let seq = soundcraft_midi::Sequence { ctrls, ..Default::default() }; + let cid = s.new_clip_id(); + s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::midi(cid, "m", 0, 48_000 * 4, seq)); + let heard = Arc::new(std::sync::Mutex::new(Vec::new())); + let ears = Ears { inner: soundcraft_dsp::create("gain").unwrap(), heard: Arc::clone(&heard) }; + let mut eng = MixEngine::new(48_000.0, 512); + eng.set_external_instances(true); + eng.adopt(vec![PreparedInstance { track: t, slot: INSTRUMENT_SLOT, id: "clap:test.ears".into(), ch: 2, plugin: Box::new(ears) }]); + render_block(&mut eng, &s, 0); + assert_eq!(std::mem::take(&mut *heard.lock().unwrap()), vec![(0, [0xB0, 121, 0])]); + render_block(&mut eng, &s, 36_000); + assert_eq!(std::mem::take(&mut *heard.lock().unwrap()), vec![(0, [0xB0, 121, 0]), (0, [0xB0, 1, 64])]); + } + fn render_block(eng: &mut MixEngine, s: &Session, pos: i64) -> f32 { let mut out = vec![vec![0.0f32; 512]; 2]; eng.render(s, pos, 512, &mut out);