1#[derive(Debug, Clone, Copy)]
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_in_range(&self, from_tick: i64, to_tick: i64) -> Vec<(i64, &ClipEvent)> {
41 let local_from = from_tick - self.start_tick;
43 let local_to = to_tick - self.start_tick;
44
45 self.events
46 .iter()
47 .filter(|e| e.tick >= local_from && e.tick < local_to)
48 .map(|e| (e.tick - local_from, e)) .collect()
50 }
51}
52
53pub struct RecordBuffer {
55 start_tick: i64,
56 events: Vec<ClipEvent>,
57 active: bool,
58}
59
60impl Default for RecordBuffer {
61 fn default() -> Self { Self::new() }
62}
63
64impl RecordBuffer {
65 pub fn new() -> Self {
66 Self { start_tick: 0, events: Vec::with_capacity(1024), active: false }
67 }
68
69 pub fn start(&mut self, tick: i64) {
71 self.start_tick = tick;
72 self.events.clear();
73 self.active = true;
74 }
75
76 pub fn record(&mut self, tick: i64, status: u8, data1: u8, data2: u8) {
78 if !self.active { return; }
79 self.events.push(ClipEvent {
80 tick: tick - self.start_tick, status,
82 data1,
83 data2,
84 });
85 }
86
87 pub fn is_active(&self) -> bool { self.active }
88 pub fn start_tick(&self) -> i64 { self.start_tick }
89
90 pub fn commit(&mut self, end_tick: i64) -> Option<MidiClip> {
93 self.active = false;
94 if self.events.is_empty() {
95 return None;
96 }
97 let length = (end_tick - self.start_tick).max(1);
98 let clip = MidiClip::new(self.start_tick, length, self.events.drain(..).collect());
99 Some(clip)
100 }
101
102 pub fn discard(&mut self) {
104 self.active = false;
105 self.events.clear();
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct ClipSnapshot {
112 pub track_id: usize,
113 pub clip_index: usize,
114 pub start_tick: i64,
115 pub length_ticks: i64,
116 pub event_count: usize,
117 pub notes: Vec<NoteSnapshot>,
119}
120
121#[derive(Debug, Clone, Copy)]
123pub struct NoteSnapshot {
124 pub note: u8,
125 pub velocity: u8,
126 pub start_frac: f64,
128 pub duration_frac: f64,
130}
131
132impl NoteSnapshot {
133 pub fn to_clip_events(notes: &[NoteSnapshot], length_ticks: i64) -> Vec<ClipEvent> {
136 let mut events = Vec::with_capacity(notes.len() * 2);
137 for n in notes {
138 let on_tick = (n.start_frac * length_ticks as f64) as i64;
139 let off_tick = ((n.start_frac + n.duration_frac) * length_ticks as f64) as i64;
140 events.push(ClipEvent {
141 tick: on_tick,
142 status: 0x90,
143 data1: n.note,
144 data2: n.velocity,
145 });
146 events.push(ClipEvent {
147 tick: off_tick.min(length_ticks),
148 status: 0x80,
149 data1: n.note,
150 data2: 0,
151 });
152 }
153 events.sort_by_key(|e| e.tick);
154 events
155 }
156}
157
158impl ClipSnapshot {
159 pub fn from_clip(track_id: usize, clip_index: usize, clip: &MidiClip) -> Self {
160 let len = clip.length_ticks as f64;
161 let mut notes = Vec::new();
162
163 let mut pending: Vec<(u8, u8, i64)> = Vec::new(); for event in &clip.events {
167 let status = event.status & 0xF0;
168 match status {
169 0x90 if event.data2 > 0 => {
170 pending.push((event.data1, event.data2, event.tick));
171 }
172 0x90 | 0x80 => {
173 if let Some(pos) = pending.iter().position(|(n, _, _)| *n == event.data1) {
175 let (note, vel, start) = pending.remove(pos);
176 let dur = (event.tick - start).max(1);
177 notes.push(NoteSnapshot {
178 note,
179 velocity: vel,
180 start_frac: start as f64 / len,
181 duration_frac: dur as f64 / len,
182 });
183 }
184 }
185 _ => {}
186 }
187 }
188
189 for (note, vel, start) in pending {
191 let dur = (clip.length_ticks - 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 Self {
201 track_id,
202 clip_index,
203 start_tick: clip.start_tick,
204 length_ticks: clip.length_ticks,
205 event_count: clip.events.len(),
206 notes,
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn record_buffer_captures_events() {
217 let mut buf = RecordBuffer::new();
218 buf.start(0);
219 buf.record(100, 0x90, 60, 100); buf.record(200, 0x80, 60, 0); assert!(buf.is_active());
222
223 let clip = buf.commit(960).unwrap();
224 assert_eq!(clip.events.len(), 2);
225 assert_eq!(clip.start_tick, 0);
226 assert_eq!(clip.length_ticks, 960);
227 assert!(!buf.is_active());
228 }
229
230 #[test]
231 fn record_buffer_empty_returns_none() {
232 let mut buf = RecordBuffer::new();
233 buf.start(0);
234 assert!(buf.commit(960).is_none());
235 }
236
237 #[test]
238 fn record_buffer_stores_relative_ticks() {
239 let mut buf = RecordBuffer::new();
240 buf.start(1000); buf.record(1500, 0x90, 60, 100);
242 let clip = buf.commit(2000).unwrap();
243 assert_eq!(clip.events[0].tick, 500); }
245
246 #[test]
247 fn clip_events_in_range() {
248 let clip = MidiClip::new(0, 960, vec![
249 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
250 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
251 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 100 },
252 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
253 ]);
254
255 let events = clip.events_in_range(0, 240);
257 assert_eq!(events.len(), 1);
258 assert_eq!(events[0].1.data1, 60); let events = clip.events_in_range(240, 480);
262 assert_eq!(events.len(), 1);
263 assert_eq!(events[0].1.status, 0x80); let events = clip.events_in_range(0, 960);
267 assert_eq!(events.len(), 4);
268 }
269
270 #[test]
271 fn clip_events_outside_range_excluded() {
272 let clip = MidiClip::new(1000, 960, vec![
273 ClipEvent { tick: 100, status: 0x90, data1: 60, data2: 100 },
274 ]);
275
276 let events = clip.events_in_range(0, 500);
278 assert_eq!(events.len(), 0);
279
280 let events = clip.events_in_range(1000, 1200);
282 assert_eq!(events.len(), 1);
283 }
284
285 #[test]
286 fn clip_snapshot_pairs_notes() {
287 let clip = MidiClip::new(0, 960, vec![
288 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
289 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
290 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
291 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
292 ]);
293
294 let snap = ClipSnapshot::from_clip(0, 0, &clip);
295 assert_eq!(snap.notes.len(), 2);
296 assert_eq!(snap.notes[0].note, 60);
297 assert!((snap.notes[0].start_frac - 0.0).abs() < 0.01);
298 assert!((snap.notes[0].duration_frac - 0.25).abs() < 0.01);
299 assert_eq!(snap.notes[1].note, 64);
300 }
301
302 #[test]
303 fn clip_snapshot_closes_pending_notes() {
304 let clip = MidiClip::new(0, 960, vec![
305 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
306 ]);
308
309 let snap = ClipSnapshot::from_clip(0, 0, &clip);
310 assert_eq!(snap.notes.len(), 1);
311 assert!((snap.notes[0].duration_frac - 1.0).abs() < 0.01);
312 }
313
314 #[test]
315 fn discard_clears_buffer() {
316 let mut buf = RecordBuffer::new();
317 buf.start(0);
318 buf.record(100, 0x90, 60, 100);
319 buf.discard();
320 assert!(!buf.is_active());
321 assert!(buf.commit(960).is_none());
322 }
323
324 #[test]
325 fn note_snapshot_to_clip_events_round_trip() {
326 let clip = MidiClip::new(0, 960, vec![
328 ClipEvent { tick: 0, status: 0x90, data1: 60, data2: 100 },
329 ClipEvent { tick: 240, status: 0x80, data1: 60, data2: 0 },
330 ClipEvent { tick: 480, status: 0x90, data1: 64, data2: 80 },
331 ClipEvent { tick: 720, status: 0x80, data1: 64, data2: 0 },
332 ]);
333
334 let snap = ClipSnapshot::from_clip(0, 0, &clip);
336 assert_eq!(snap.notes.len(), 2);
337
338 let events = NoteSnapshot::to_clip_events(&snap.notes, 960);
340 assert_eq!(events.len(), 4); let note_ons: Vec<_> = events.iter().filter(|e| e.status == 0x90).collect();
344 let note_offs: Vec<_> = events.iter().filter(|e| e.status == 0x80).collect();
345 assert_eq!(note_ons.len(), 2);
346 assert_eq!(note_offs.len(), 2);
347
348 assert_eq!(note_ons[0].data1, 60);
350 assert_eq!(note_ons[0].tick, 0);
351 assert_eq!(note_ons[1].data1, 64);
353 assert!((note_ons[1].tick - 480).abs() <= 1);
354 }
355
356 #[test]
357 fn edited_snapshot_produces_different_events() {
358 let mut notes = vec![
359 NoteSnapshot { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
360 ];
361
362 let original = NoteSnapshot::to_clip_events(¬es, 960);
363 assert_eq!(original[0].tick, 0); notes[0].start_frac = 0.5;
367 let edited = NoteSnapshot::to_clip_events(¬es, 960);
368 assert_eq!(edited[0].tick, 480); notes[0].duration_frac = 0.1;
372 let shorter = NoteSnapshot::to_clip_events(¬es, 960);
373 let off_tick = shorter.iter().find(|e| e.status == 0x80).unwrap().tick;
374 assert_eq!(off_tick, 576); }
376}