#[derive(Debug, Clone, Copy)]
pub struct ClipEvent {
pub tick: i64,
pub status: u8,
pub data1: u8,
pub data2: u8,
}
#[derive(Debug, Clone)]
pub struct MidiClip {
pub start_tick: i64,
pub length_ticks: i64,
pub events: Vec<ClipEvent>,
}
impl MidiClip {
pub fn new(start_tick: i64, length_ticks: i64, mut events: Vec<ClipEvent>) -> Self {
events.sort_by_key(|e| e.tick);
Self { start_tick, length_ticks, events }
}
pub fn end_tick(&self) -> i64 {
self.start_tick + self.length_ticks
}
pub fn events_in_range(&self, from_tick: i64, to_tick: i64) -> Vec<(i64, &ClipEvent)> {
let local_from = from_tick - self.start_tick;
let local_to = to_tick - self.start_tick;
self.events
.iter()
.filter(|e| e.tick >= local_from && e.tick < local_to)
.map(|e| (e.tick - local_from, e)) .collect()
}
}
pub struct RecordBuffer {
start_tick: i64,
events: Vec<ClipEvent>,
active: bool,
}
impl Default for RecordBuffer {
fn default() -> Self { Self::new() }
}
impl RecordBuffer {
pub fn new() -> Self {
Self { start_tick: 0, events: Vec::with_capacity(1024), active: false }
}
pub fn start(&mut self, tick: i64) {
self.start_tick = tick;
self.events.clear();
self.active = true;
}
pub fn record(&mut self, tick: i64, status: u8, data1: u8, data2: u8) {
if !self.active { return; }
self.events.push(ClipEvent {
tick: tick - self.start_tick, status,
data1,
data2,
});
}
pub fn is_active(&self) -> bool { self.active }
pub fn start_tick(&self) -> i64 { self.start_tick }
pub fn commit(&mut self, end_tick: i64) -> Option<MidiClip> {
self.active = false;
if self.events.is_empty() {
return None;
}
let length = (end_tick - self.start_tick).max(1);
let clip = MidiClip::new(self.start_tick, length, self.events.drain(..).collect());
Some(clip)
}
pub fn discard(&mut self) {
self.active = false;
self.events.clear();
}
}
#[derive(Debug, Clone)]
pub struct ClipSnapshot {
pub track_id: usize,
pub clip_index: usize,
pub start_tick: i64,
pub length_ticks: i64,
pub event_count: usize,
pub notes: Vec<NoteSnapshot>,
}
#[derive(Debug, Clone, Copy)]
pub struct NoteSnapshot {
pub note: u8,
pub velocity: u8,
pub start_frac: f64,
pub duration_frac: f64,
}
impl NoteSnapshot {
pub fn to_clip_events(notes: &[NoteSnapshot], length_ticks: i64) -> Vec<ClipEvent> {
let mut events = Vec::with_capacity(notes.len() * 2);
for n in notes {
let on_tick = (n.start_frac * length_ticks as f64) as i64;
let off_tick = ((n.start_frac + n.duration_frac) * length_ticks as f64) as i64;
events.push(ClipEvent {
tick: on_tick,
status: 0x90,
data1: n.note,
data2: n.velocity,
});
events.push(ClipEvent {
tick: off_tick.min(length_ticks),
status: 0x80,
data1: n.note,
data2: 0,
});
}
events.sort_by_key(|e| e.tick);
events
}
}
impl ClipSnapshot {
pub fn from_clip(track_id: usize, clip_index: usize, clip: &MidiClip) -> Self {
let len = clip.length_ticks as f64;
let mut notes = Vec::new();
let mut pending: Vec<(u8, u8, i64)> = Vec::new();
for event in &clip.events {
let status = event.status & 0xF0;
match status {
0x90 if event.data2 > 0 => {
pending.push((event.data1, event.data2, event.tick));
}
0x90 | 0x80 => {
if let Some(pos) = pending.iter().position(|(n, _, _)| *n == event.data1) {
let (note, vel, start) = pending.remove(pos);
let dur = (event.tick - start).max(1);
notes.push(NoteSnapshot {
note,
velocity: vel,
start_frac: start as f64 / len,
duration_frac: dur as f64 / len,
});
}
}
_ => {}
}
}
for (note, vel, start) in pending {
let dur = (clip.length_ticks - start).max(1);
notes.push(NoteSnapshot {
note,
velocity: vel,
start_frac: start as f64 / len,
duration_frac: dur as f64 / len,
});
}
Self {
track_id,
clip_index,
start_tick: clip.start_tick,
length_ticks: clip.length_ticks,
event_count: clip.events.len(),
notes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_buffer_captures_events() {
let mut buf = RecordBuffer::new();
buf.start(0);
buf.record(100, 0x90, 60, 100); buf.record(200, 0x80, 60, 0); assert!(buf.is_active());
let clip = buf.commit(960).unwrap();
assert_eq!(clip.events.len(), 2);
assert_eq!(clip.start_tick, 0);
assert_eq!(clip.length_ticks, 960);
assert!(!buf.is_active());
}
#[test]
fn record_buffer_empty_returns_none() {
let mut buf = RecordBuffer::new();
buf.start(0);
assert!(buf.commit(960).is_none());
}
#[test]
fn record_buffer_stores_relative_ticks() {
let mut buf = RecordBuffer::new();
buf.start(1000); buf.record(1500, 0x90, 60, 100);
let clip = buf.commit(2000).unwrap();
assert_eq!(clip.events[0].tick, 500); }
#[test]
fn clip_events_in_range() {
let clip = MidiClip::new(0, 960, vec![
ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 100 },
ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
]);
let events = clip.events_in_range(0, 240);
assert_eq!(events.len(), 1);
assert_eq!(events[0].1.data1, 60);
let events = clip.events_in_range(240, 480);
assert_eq!(events.len(), 1);
assert_eq!(events[0].1.status, 0x80);
let events = clip.events_in_range(0, 960);
assert_eq!(events.len(), 4);
}
#[test]
fn clip_events_outside_range_excluded() {
let clip = MidiClip::new(1000, 960, vec![
ClipEvent { tick: 100, status: 0x90, data1: 60, data2: 100 },
]);
let events = clip.events_in_range(0, 500);
assert_eq!(events.len(), 0);
let events = clip.events_in_range(1000, 1200);
assert_eq!(events.len(), 1);
}
#[test]
fn clip_snapshot_pairs_notes() {
let clip = MidiClip::new(0, 960, vec![
ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
]);
let snap = ClipSnapshot::from_clip(0, 0, &clip);
assert_eq!(snap.notes.len(), 2);
assert_eq!(snap.notes[0].note, 60);
assert!((snap.notes[0].start_frac - 0.0).abs() < 0.01);
assert!((snap.notes[0].duration_frac - 0.25).abs() < 0.01);
assert_eq!(snap.notes[1].note, 64);
}
#[test]
fn clip_snapshot_closes_pending_notes() {
let clip = MidiClip::new(0, 960, vec![
ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
]);
let snap = ClipSnapshot::from_clip(0, 0, &clip);
assert_eq!(snap.notes.len(), 1);
assert!((snap.notes[0].duration_frac - 1.0).abs() < 0.01);
}
#[test]
fn discard_clears_buffer() {
let mut buf = RecordBuffer::new();
buf.start(0);
buf.record(100, 0x90, 60, 100);
buf.discard();
assert!(!buf.is_active());
assert!(buf.commit(960).is_none());
}
#[test]
fn note_snapshot_to_clip_events_round_trip() {
let clip = MidiClip::new(0, 960, vec![
ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
]);
let snap = ClipSnapshot::from_clip(0, 0, &clip);
assert_eq!(snap.notes.len(), 2);
let events = NoteSnapshot::to_clip_events(&snap.notes, 960);
assert_eq!(events.len(), 4);
let note_ons: Vec<_> = events.iter().filter(|e| e.status == 0x90).collect();
let note_offs: Vec<_> = events.iter().filter(|e| e.status == 0x80).collect();
assert_eq!(note_ons.len(), 2);
assert_eq!(note_offs.len(), 2);
assert_eq!(note_ons[0].data1, 60);
assert_eq!(note_ons[0].tick, 0);
assert_eq!(note_ons[1].data1, 64);
assert!((note_ons[1].tick - 480).abs() <= 1);
}
#[test]
fn edited_snapshot_produces_different_events() {
let mut notes = vec![
NoteSnapshot { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
];
let original = NoteSnapshot::to_clip_events(¬es, 960);
assert_eq!(original[0].tick, 0);
notes[0].start_frac = 0.5;
let edited = NoteSnapshot::to_clip_events(¬es, 960);
assert_eq!(edited[0].tick, 480);
notes[0].duration_frac = 0.1;
let shorter = NoteSnapshot::to_clip_events(¬es, 960);
let off_tick = shorter.iter().find(|e| e.status == 0x80).unwrap().tick;
assert_eq!(off_tick, 576); }
}