1#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum RtEvent {
10 ClockTick {
12 subdivision: u8,
14 beat: u64,
16 tempo_bpm: f64,
18 timestamp_ns: u64,
20 },
21
22 Transport(TransportEvent),
24
25 MidiInput {
27 input_port_index: u8,
29 timestamp_ns: u64,
31 message: MidiMessage,
33 },
34
35 SongPosition {
37 position: u16,
39 },
40
41 NonFatalError(RtErrorCode),
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum TransportEvent {
48 Start,
50 Stop,
52 Continue,
54}
55
56pub type MidiMessage = oxurack_midi::MidiWire;
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum EcsCommand {
66 SendMidi {
68 output_port_index: u8,
70 message: MidiMessage,
72 },
73
74 SetTempo {
76 bpm: f64,
78 },
79
80 SendTransport(TransportEvent),
82
83 SendSongPosition {
85 position: u16,
87 },
88
89 Shutdown,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
104pub(crate) enum MidiClassification {
105 Clock,
107 Start,
109 Stop,
111 Continue,
113 SongPosition {
115 position: u16,
117 },
118 ActiveSensing,
120 SystemReset,
122 Channel(MidiMessage),
124}
125
126pub(crate) fn classify_midi(bytes: &[u8]) -> Option<MidiClassification> {
150 let &status = bytes.first()?;
151
152 if status < 0x80 {
153 return None; }
155
156 match status {
157 0xF8 => Some(MidiClassification::Clock),
158 0xFA => Some(MidiClassification::Start),
159 0xFB => Some(MidiClassification::Continue),
160 0xFC => Some(MidiClassification::Stop),
161 0xFE => Some(MidiClassification::ActiveSensing),
162 0xFF => Some(MidiClassification::SystemReset),
163 0xF2 => {
164 let lsb = *bytes.get(1).unwrap_or(&0);
166 let msb = *bytes.get(2).unwrap_or(&0);
167 let position = (lsb as u16) | ((msb as u16) << 7);
168 Some(MidiClassification::SongPosition { position })
169 }
170 0x80..=0xEF => {
171 let msg = MidiMessage::from_bytes(bytes)?;
173 Some(MidiClassification::Channel(msg))
174 }
175 _ => None, }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum RtErrorCode {
182 OutputPortLost,
184 InputPortLost,
186 QueueOverflow,
188 ClockNotLocked,
190 ClockDropout,
192 PriorityElevationFailed,
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use pretty_assertions::assert_eq;
201
202 #[test]
205 fn test_midi_message_size() {
206 assert_eq!(std::mem::size_of::<MidiMessage>(), 4);
207 }
208
209 #[test]
210 fn test_rt_event_fits_cache_line() {
211 assert!(std::mem::size_of::<RtEvent>() <= 64);
212 }
213
214 #[test]
215 fn test_ecs_command_fits_cache_line() {
216 assert!(std::mem::size_of::<EcsCommand>() <= 64);
217 }
218
219 fn _assert_rt_event_is_copy_send()
222 where
223 RtEvent: Copy + Send + 'static,
224 {
225 }
226
227 fn _assert_ecs_command_is_copy_send()
228 where
229 EcsCommand: Copy + Send + 'static,
230 {
231 }
232
233 #[test]
236 fn test_note_on() {
237 let msg = MidiMessage::note_on(0, 60, 100);
238 assert_eq!(msg.status, 0x90);
239 assert_eq!(msg.data1, 60);
240 assert_eq!(msg.data2, 100);
241 assert_eq!(msg.length, 3);
242 }
243
244 #[test]
245 fn test_note_off() {
246 let msg = MidiMessage::note_off(1, 64, 0);
247 assert_eq!(msg.status, 0x81);
248 assert_eq!(msg.data1, 64);
249 assert_eq!(msg.data2, 0);
250 assert_eq!(msg.length, 3);
251 }
252
253 #[test]
254 fn test_cc() {
255 let msg = MidiMessage::cc(2, 74, 127);
256 assert_eq!(msg.status, 0xB2);
257 assert_eq!(msg.data1, 74);
258 assert_eq!(msg.data2, 127);
259 assert_eq!(msg.length, 3);
260 }
261
262 #[test]
263 fn test_program_change() {
264 let msg = MidiMessage::program_change(5, 42);
265 assert_eq!(msg.status, 0xC5);
266 assert_eq!(msg.data1, 42);
267 assert_eq!(msg.data2, 0);
268 assert_eq!(msg.length, 2);
269 }
270
271 #[test]
272 fn test_pitch_bend() {
273 let msg = MidiMessage::pitch_bend(0, 0, 64);
274 assert_eq!(msg.status, 0xE0);
275 assert_eq!(msg.data1, 0);
276 assert_eq!(msg.data2, 64);
277 assert_eq!(msg.length, 3);
278 }
279
280 #[test]
283 fn test_note_on_roundtrip() {
284 let original = MidiMessage::note_on(3, 72, 110);
285 let bytes = original.to_bytes();
286 let reconstructed = MidiMessage::from_bytes(&bytes);
287 assert_eq!(Some(original), reconstructed);
288 }
289
290 #[test]
291 fn test_program_change_roundtrip() {
292 let original = MidiMessage::program_change(7, 99);
293 let bytes = original.to_bytes();
294 let reconstructed = MidiMessage::from_bytes(&bytes);
295 assert_eq!(Some(original), reconstructed);
296 }
297
298 #[test]
299 fn test_from_bytes_empty_returns_none() {
300 assert_eq!(MidiMessage::from_bytes(&[]), None);
301 }
302
303 #[test]
304 fn test_from_bytes_data_byte_returns_none() {
305 assert_eq!(MidiMessage::from_bytes(&[0x7F, 0x60, 0x40]), None);
306 }
307
308 #[test]
309 fn test_from_bytes_system_returns_none() {
310 assert_eq!(MidiMessage::from_bytes(&[0xF0, 0x7E, 0x7F]), None);
311 }
312
313 #[test]
316 fn test_to_bytes_pads_short_messages() {
317 let msg = MidiMessage::program_change(0, 5);
318 let bytes = msg.to_bytes();
319 assert_eq!(bytes, [0xC0, 5, 0]);
320 }
321
322 #[test]
325 fn test_classify_clock() {
326 assert_eq!(classify_midi(&[0xF8]), Some(MidiClassification::Clock));
327 }
328
329 #[test]
330 fn test_classify_start() {
331 assert_eq!(classify_midi(&[0xFA]), Some(MidiClassification::Start));
332 }
333
334 #[test]
335 fn test_classify_stop() {
336 assert_eq!(classify_midi(&[0xFC]), Some(MidiClassification::Stop));
337 }
338
339 #[test]
340 fn test_classify_continue() {
341 assert_eq!(classify_midi(&[0xFB]), Some(MidiClassification::Continue));
342 }
343
344 #[test]
345 fn test_classify_active_sensing() {
346 assert_eq!(
347 classify_midi(&[0xFE]),
348 Some(MidiClassification::ActiveSensing)
349 );
350 }
351
352 #[test]
353 fn test_classify_system_reset() {
354 assert_eq!(
355 classify_midi(&[0xFF]),
356 Some(MidiClassification::SystemReset)
357 );
358 }
359
360 #[test]
361 fn test_classify_note_on() {
362 assert_eq!(
363 classify_midi(&[0x90, 60, 100]),
364 Some(MidiClassification::Channel(MidiMessage {
365 status: 0x90,
366 data1: 60,
367 data2: 100,
368 length: 3,
369 }))
370 );
371 }
372
373 #[test]
374 fn test_classify_program_change() {
375 assert_eq!(
376 classify_midi(&[0xC0, 42]),
377 Some(MidiClassification::Channel(MidiMessage {
378 status: 0xC0,
379 data1: 42,
380 data2: 0,
381 length: 2,
382 }))
383 );
384 }
385
386 #[test]
387 fn test_classify_song_position() {
388 assert_eq!(
390 classify_midi(&[0xF2, 0x10, 0x02]),
391 Some(MidiClassification::SongPosition { position: 272 })
392 );
393 }
394
395 #[test]
396 fn test_classify_empty_returns_none() {
397 assert_eq!(classify_midi(&[]), None);
398 }
399
400 #[test]
401 fn test_classify_data_byte_returns_none() {
402 assert_eq!(classify_midi(&[0x60]), None);
403 }
404
405 #[test]
406 fn test_classify_sysex_returns_none() {
407 assert_eq!(classify_midi(&[0xF0, 0x7E, 0xF7]), None);
408 }
409
410 #[test]
413 fn test_classify_mtc_quarter_frame() {
414 assert_eq!(classify_midi(&[0xF1, 0x00]), None);
416 }
417
418 #[test]
419 fn test_classify_song_select() {
420 assert_eq!(classify_midi(&[0xF3, 0x00]), None);
422 }
423
424 #[test]
425 fn test_classify_tune_request() {
426 assert_eq!(classify_midi(&[0xF6]), None);
428 }
429
430 #[test]
433 fn test_from_bytes_note_on_missing_data2() {
434 let msg = MidiMessage::from_bytes(&[0x90, 60]);
437 assert!(msg.is_some(), "should parse partial Note On");
438 let msg = msg.unwrap();
439 assert_eq!(msg.status, 0x90);
440 assert_eq!(msg.data1, 60);
441 assert_eq!(msg.data2, 0);
442 assert_eq!(msg.length, 3);
443 }
444
445 #[test]
446 fn test_from_bytes_single_status_byte() {
447 let msg = MidiMessage::from_bytes(&[0x90]);
450 assert!(msg.is_some(), "should parse status-only Note On");
451 let msg = msg.unwrap();
452 assert_eq!(msg.status, 0x90);
453 assert_eq!(msg.data1, 0);
454 assert_eq!(msg.data2, 0);
455 assert_eq!(msg.length, 3);
456 }
457
458 #[test]
459 fn test_from_bytes_program_change_single_byte() {
460 let msg = MidiMessage::from_bytes(&[0xC0]);
462 assert!(msg.is_some(), "should parse status-only Program Change");
463 let msg = msg.unwrap();
464 assert_eq!(msg.status, 0xC0);
465 assert_eq!(msg.data1, 0);
466 assert_eq!(msg.data2, 0);
467 assert_eq!(msg.length, 2);
468 }
469
470 #[test]
473 fn test_to_bytes_note_on() {
474 let msg = MidiMessage::note_on(0, 60, 100);
475 assert_eq!(msg.to_bytes(), [0x90, 60, 100]);
476 }
477
478 #[test]
479 fn test_to_bytes_note_off() {
480 let msg = MidiMessage::note_off(1, 64, 0);
481 assert_eq!(msg.to_bytes(), [0x81, 64, 0]);
482 }
483
484 #[test]
485 fn test_to_bytes_cc() {
486 let msg = MidiMessage::cc(2, 74, 127);
487 assert_eq!(msg.to_bytes(), [0xB2, 74, 127]);
488 }
489
490 #[test]
491 fn test_to_bytes_program_change() {
492 let msg = MidiMessage::program_change(5, 42);
493 assert_eq!(msg.to_bytes(), [0xC5, 42, 0]);
494 }
495
496 #[test]
497 fn test_to_bytes_pitch_bend() {
498 let msg = MidiMessage::pitch_bend(0, 0, 64);
499 assert_eq!(msg.to_bytes(), [0xE0, 0, 64]);
500 }
501
502 #[test]
505 fn test_classify_song_position_zero() {
506 assert_eq!(
507 classify_midi(&[0xF2, 0x00, 0x00]),
508 Some(MidiClassification::SongPosition { position: 0 })
509 );
510 }
511
512 #[test]
513 fn test_classify_song_position_max() {
514 assert_eq!(
516 classify_midi(&[0xF2, 0x7F, 0x7F]),
517 Some(MidiClassification::SongPosition { position: 16383 })
518 );
519 }
520
521 #[test]
522 fn test_classify_song_position_missing_bytes() {
523 assert_eq!(
525 classify_midi(&[0xF2]),
526 Some(MidiClassification::SongPosition { position: 0 })
527 );
528 }
529
530 #[test]
533 fn test_classify_channel_pressure() {
534 let result = classify_midi(&[0xD0, 100]);
536 assert_eq!(
537 result,
538 Some(MidiClassification::Channel(MidiMessage {
539 status: 0xD0,
540 data1: 100,
541 data2: 0,
542 length: 2,
543 }))
544 );
545 }
546}