diff --git a/crates/midi/src/sequence.rs b/crates/midi/src/sequence.rs index e50c6614..c5336f06 100644 --- a/crates/midi/src/sequence.rs +++ b/crates/midi/src/sequence.rs @@ -1,4 +1,6 @@ use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::collections::hash_map::Entry; /// A MIDI note. Times are in model ticks ([`crate::PPQ`] per quarter note). #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] @@ -108,14 +110,28 @@ impl Sequence { self.ctrls.sort_by_key(|c| c.tick); let mut kept: Vec = Vec::with_capacity(self.ctrls.len()); + // The latest tick and `kept` index of each channel+kind, so a same-tick repeat is found + // without rescanning the tick's events (a tick can hold many thousands of distinct keys). + // An entry from an earlier tick counts as absent; keeping the tick instead of clearing the + // map per tick keeps the work per event constant after a dense tick. + let mut last_at: HashMap<(u8, (u8, u8)), (i64, usize)> = HashMap::new(); for c in self.ctrls.drain(..) { // Within a tick, replace an earlier event of the same channel+kind. - // Only events at the same tick (the tail of `kept`) can match. - let slot = - kept.iter_mut().rev().take_while(|k| k.tick == c.tick).find(|k| k.channel == c.channel && kind_key(k.kind) == kind_key(c.kind)); - match slot { - Some(slot) => *slot = c, - None => kept.push(c), + match last_at.entry((c.channel, kind_key(c.kind))) { + Entry::Occupied(mut e) => { + let (tick, idx) = *e.get(); + match kept.get_mut(idx) { + Some(slot) if tick == c.tick => *slot = c, + _ => { + e.insert((c.tick, kept.len())); + kept.push(c); + } + } + } + Entry::Vacant(e) => { + e.insert((c.tick, kept.len())); + kept.push(c); + } } } self.ctrls = kept; diff --git a/crates/midi/tests/it/ops.rs b/crates/midi/tests/it/ops.rs index 3a367ee0..431790ff 100644 --- a/crates/midi/tests/it/ops.rs +++ b/crates/midi/tests/it/ops.rs @@ -222,3 +222,72 @@ fn sequence_serde_round_trip() { let json = serde_json::to_string(&seq).unwrap(); assert_eq!(serde_json::from_str::(&json).unwrap(), seq); } + +#[test] +fn remove_duplicates_handles_a_large_same_tick_controller_group() { + // Every channel x controller number at one tick, twice: the second copy of each wins. A + // rescan of the tick's kept events per incoming event would make this quadratic. + let keys: Vec<(u8, u8)> = (0..=255u8).flat_map(|ch| (0..=255u8).map(move |cc| (ch, cc))).collect(); + let ctrls = [0, 1] + .iter() + .flat_map(|&pass| keys.iter().map(move |&(channel, cc)| CtrlEvent { tick: 7, channel, kind: CtrlKind::Cc(cc), value: pass })) + .collect(); + let mut seq = Sequence { notes: Vec::new(), ctrls }; + let start = std::time::Instant::now(); + assert_eq!(seq.remove_duplicates(), keys.len()); + assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed()); + assert_eq!(seq.ctrls.len(), keys.len()); + assert!(seq.ctrls.iter().all(|c| c.value == 1)); +} + +#[test] +fn remove_duplicates_matches_a_per_tick_rescan_on_mixed_ticks() { + // The straightforward per-tick rescan the indexed version replaced, kept as an oracle. + fn reference(mut ctrls: Vec) -> Vec { + ctrls.sort_by_key(|c| c.tick); + let key = |k: CtrlKind| match k { + CtrlKind::Cc(n) => (0, n), + CtrlKind::PitchBend => (1, 0), + CtrlKind::ChannelPressure => (2, 0), + CtrlKind::PolyPressure(p) => (3, p), + CtrlKind::Program => (4, 0), + }; + let mut kept: Vec = Vec::new(); + for c in ctrls { + match kept.iter_mut().rev().take_while(|k| k.tick == c.tick).find(|k| k.channel == c.channel && key(k.kind) == key(c.kind)) { + Some(slot) => *slot = c, + None => kept.push(c), + } + } + kept + } + + // Few ticks, channels and kinds, so the same key recurs both within a tick (interleaved with + // other keys) and across ticks; unsorted input from a fixed xorshift stream. + let mut state = 0x9E37_79B9_7F4A_7C15u64; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + let kinds = |r: u64| match r % 5 { + 0 => CtrlKind::Cc((r >> 8) as u8 % 4), + 1 => CtrlKind::PitchBend, + 2 => CtrlKind::ChannelPressure, + 3 => CtrlKind::PolyPressure((r >> 8) as u8 % 4), + _ => CtrlKind::Program, + }; + for round in 0..200 { + let mut ctrls = Vec::new(); + for i in 0..(next() % 64) { + let r = next(); + ctrls.push(CtrlEvent { tick: (r % 6) as i64 - 2, channel: (r >> 16) as u8 % 3, kind: kinds(r >> 24), value: (round * 100 + i) as i32 }); + } + let mut seq = Sequence { notes: Vec::new(), ctrls: ctrls.clone() }; + let removed = seq.remove_duplicates(); + let expected = reference(ctrls.clone()); + assert_eq!(seq.ctrls, expected, "round {round}"); + assert_eq!(removed, ctrls.len() - expected.len(), "round {round}"); + } +}