1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ClipEvent {
9 pub tick: i64,
11 pub status: u8,
12 pub data1: u8,
13 pub data2: u8,
14}
15
16#[derive(Debug, Clone)]
18pub struct MidiClip {
19 pub start_tick: i64,
21 pub length_ticks: i64,
23 pub events: Vec<ClipEvent>,
25}
26
27impl MidiClip {
28 pub fn new(start_tick: i64, length_ticks: i64, mut events: Vec<ClipEvent>) -> Self {
29 events.sort_by_key(|e| e.tick);
30 Self { start_tick, length_ticks, events }
31 }
32
33 pub fn end_tick(&self) -> i64 {
35 self.start_tick + self.length_ticks
36 }
37
38 pub fn events_between(
48 &self,
49 from_tick: i64,
50 to_tick: i64,
51 ) -> impl Iterator<Item = (i64, &ClipEvent)> {
52 let start = self.start_tick;
53 self.events
54 .iter()
55 .map(move |e| (start + e.tick, e))
56 .filter(move |(tick, _)| *tick >= from_tick && *tick < to_tick)
57 }
58
59 pub fn events_in_range(&self, from_tick: i64, to_tick: i64) -> Vec<(i64, &ClipEvent)> {
62 self.events_between(from_tick, to_tick)
63 .map(|(tick, e)| (tick - from_tick, e))
64 .collect()
65 }
66}
67
68pub struct RecordBuffer {
70 start_tick: i64,
71 events: Vec<ClipEvent>,
72 active: bool,
73}
74
75impl Default for RecordBuffer {
76 fn default() -> Self { Self::new() }
77}
78
79impl RecordBuffer {
80 pub fn new() -> Self {
81 Self { start_tick: 0, events: Vec::with_capacity(1024), active: false }
82 }
83
84 pub fn start(&mut self, tick: i64) {
86 self.start_tick = tick;
87 self.events.clear();
88 self.active = true;
89 }
90
91 pub fn record(&mut self, tick: i64, status: u8, data1: u8, data2: u8) {
93 if !self.active { return; }
94 self.events.push(ClipEvent {
95 tick: tick - self.start_tick, status,
97 data1,
98 data2,
99 });
100 }
101
102 pub fn is_active(&self) -> bool { self.active }
103 pub fn start_tick(&self) -> i64 { self.start_tick }
104
105 pub fn commit(&mut self, end_tick: i64) -> Option<MidiClip> {
108 self.active = false;
109 if self.events.is_empty() {
110 return None;
111 }
112 let length = (end_tick - self.start_tick).max(1);
113 let clip = MidiClip::new(self.start_tick, length, self.events.drain(..).collect());
114 Some(clip)
115 }
116
117 pub fn discard(&mut self) {
119 self.active = false;
120 self.events.clear();
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct ClipSnapshot {
127 pub track_id: usize,
128 pub clip_index: usize,
129 pub start_tick: i64,
130 pub length_ticks: i64,
131 pub event_count: usize,
132 pub notes: Vec<NoteSnapshot>,
134}
135
136#[derive(Debug, Clone, Copy)]
138pub struct NoteSnapshot {
139 pub note: u8,
140 pub velocity: u8,
141 pub start_frac: f64,
143 pub duration_frac: f64,
145}
146
147impl NoteSnapshot {
148 pub fn to_clip_events(notes: &[NoteSnapshot], length_ticks: i64) -> Vec<ClipEvent> {
151 let mut events = Vec::with_capacity(notes.len() * 2);
152 for n in notes {
153 let on_tick = (n.start_frac * length_ticks as f64) as i64;
154 let off_tick = ((n.start_frac + n.duration_frac) * length_ticks as f64) as i64;
155 events.push(ClipEvent {
156 tick: on_tick,
157 status: 0x90,
158 data1: n.note,
159 data2: n.velocity,
160 });
161 events.push(ClipEvent {
162 tick: off_tick.min(length_ticks),
163 status: 0x80,
164 data1: n.note,
165 data2: 0,
166 });
167 }
168 events.sort_by_key(|e| e.tick);
169 events
170 }
171}
172
173impl ClipSnapshot {
174 pub fn from_clip(track_id: usize, clip_index: usize, clip: &MidiClip) -> Self {
175 let len = clip.length_ticks as f64;
176 let mut notes = Vec::new();
177
178 let mut pending: Vec<(u8, u8, i64)> = Vec::new(); for event in &clip.events {
182 let status = event.status & 0xF0;
183 match status {
184 0x90 if event.data2 > 0 => {
185 pending.push((event.data1, event.data2, event.tick));
186 }
187 0x90 | 0x80 => {
188 if let Some(pos) = pending.iter().position(|(n, _, _)| *n == event.data1) {
190 let (note, vel, start) = pending.remove(pos);
191 let dur = (event.tick - start).max(1);
192 notes.push(NoteSnapshot {
193 note,
194 velocity: vel,
195 start_frac: start as f64 / len,
196 duration_frac: dur as f64 / len,
197 });
198 }
199 }
200 _ => {}
201 }
202 }
203
204 for (note, vel, start) in pending {
206 let dur = (clip.length_ticks - start).max(1);
207 notes.push(NoteSnapshot {
208 note,
209 velocity: vel,
210 start_frac: start as f64 / len,
211 duration_frac: dur as f64 / len,
212 });
213 }
214
215 Self {
216 track_id,
217 clip_index,
218 start_tick: clip.start_tick,
219 length_ticks: clip.length_ticks,
220 event_count: clip.events.len(),
221 notes,
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn record_buffer_captures_events() {
232 let mut buf = RecordBuffer::new();
233 buf.start(0);
234 buf.record(100, 0x90, 60, 100); buf.record(200, 0x80, 60, 0); assert!(buf.is_active());
237
238 let clip = buf.commit(960).unwrap();
239 assert_eq!(clip.events.len(), 2);
240 assert_eq!(clip.start_tick, 0);
241 assert_eq!(clip.length_ticks, 960);
242 assert!(!buf.is_active());
243 }
244
245 #[test]
246 fn record_buffer_empty_returns_none() {
247 let mut buf = RecordBuffer::new();
248 buf.start(0);
249 assert!(buf.commit(960).is_none());
250 }
251
252 #[test]
253 fn record_buffer_stores_relative_ticks() {
254 let mut buf = RecordBuffer::new();
255 buf.start(1000); buf.record(1500, 0x90, 60, 100);
257 let clip = buf.commit(2000).unwrap();
258 assert_eq!(clip.events[0].tick, 500); }
260
261 #[test]
262 fn clip_events_in_range() {
263 let clip = MidiClip::new(0, 960, vec![
264 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
265 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
266 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 100 },
267 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
268 ]);
269
270 let events = clip.events_in_range(0, 240);
272 assert_eq!(events.len(), 1);
273 assert_eq!(events[0].1.data1, 60); let events = clip.events_in_range(240, 480);
277 assert_eq!(events.len(), 1);
278 assert_eq!(events[0].1.status, 0x80); let events = clip.events_in_range(0, 960);
282 assert_eq!(events.len(), 4);
283 }
284
285 #[test]
286 fn clip_events_outside_range_excluded() {
287 let clip = MidiClip::new(1000, 960, vec![
288 ClipEvent { tick: 100, status: 0x90, data1: 60, data2: 100 },
289 ]);
290
291 let events = clip.events_in_range(0, 500);
293 assert_eq!(events.len(), 0);
294
295 let events = clip.events_in_range(1000, 1200);
297 assert_eq!(events.len(), 1);
298 }
299
300 #[test]
301 fn clip_snapshot_pairs_notes() {
302 let clip = MidiClip::new(0, 960, vec![
303 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
304 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
305 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
306 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
307 ]);
308
309 let snap = ClipSnapshot::from_clip(0, 0, &clip);
310 assert_eq!(snap.notes.len(), 2);
311 assert_eq!(snap.notes[0].note, 60);
312 assert!((snap.notes[0].start_frac - 0.0).abs() < 0.01);
313 assert!((snap.notes[0].duration_frac - 0.25).abs() < 0.01);
314 assert_eq!(snap.notes[1].note, 64);
315 }
316
317 #[test]
318 fn clip_snapshot_closes_pending_notes() {
319 let clip = MidiClip::new(0, 960, vec![
320 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
321 ]);
323
324 let snap = ClipSnapshot::from_clip(0, 0, &clip);
325 assert_eq!(snap.notes.len(), 1);
326 assert!((snap.notes[0].duration_frac - 1.0).abs() < 0.01);
327 }
328
329 #[test]
330 fn discard_clears_buffer() {
331 let mut buf = RecordBuffer::new();
332 buf.start(0);
333 buf.record(100, 0x90, 60, 100);
334 buf.discard();
335 assert!(!buf.is_active());
336 assert!(buf.commit(960).is_none());
337 }
338
339 #[test]
340 fn note_snapshot_to_clip_events_round_trip() {
341 let clip = MidiClip::new(0, 960, vec![
343 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
344 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
345 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
346 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
347 ]);
348
349 let snap = ClipSnapshot::from_clip(0, 0, &clip);
351 assert_eq!(snap.notes.len(), 2);
352
353 let events = NoteSnapshot::to_clip_events(&snap.notes, 960);
355 assert_eq!(events.len(), 4); let note_ons: Vec<_> = events.iter().filter(|e| e.status == 0x90).collect();
359 let note_offs: Vec<_> = events.iter().filter(|e| e.status == 0x80).collect();
360 assert_eq!(note_ons.len(), 2);
361 assert_eq!(note_offs.len(), 2);
362
363 assert_eq!(note_ons[0].data1, 60);
365 assert_eq!(note_ons[0].tick, 0);
366 assert_eq!(note_ons[1].data1, 64);
368 assert!((note_ons[1].tick - 480).abs() <= 1);
369 }
370
371 #[test]
372 fn edited_snapshot_produces_different_events() {
373 let mut notes = vec![
374 NoteSnapshot { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
375 ];
376
377 let original = NoteSnapshot::to_clip_events(¬es, 960);
378 assert_eq!(original[0].tick, 0); notes[0].start_frac = 0.5;
382 let edited = NoteSnapshot::to_clip_events(¬es, 960);
383 assert_eq!(edited[0].tick, 480); notes[0].duration_frac = 0.1;
387 let shorter = NoteSnapshot::to_clip_events(¬es, 960);
388 let off_tick = shorter.iter().find(|e| e.status == 0x80).unwrap().tick;
389 assert_eq!(off_tick, 576); }
391}