1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum MidiChannel {
9 Ch1,
11 Ch2,
13 Ch3,
15 Ch4,
17 Ch5,
19 Ch6,
21 Ch7,
23 Ch8,
25 Ch9,
27 Ch10,
29 Ch11,
31 Ch12,
33 Ch13,
35 Ch14,
37 Ch15,
39 Ch16,
41}
42
43impl MidiChannel {
44 pub fn as_index(&self) -> u8 {
46 match self {
47 MidiChannel::Ch1 => 0,
48 MidiChannel::Ch2 => 1,
49 MidiChannel::Ch3 => 2,
50 MidiChannel::Ch4 => 3,
51 MidiChannel::Ch5 => 4,
52 MidiChannel::Ch6 => 5,
53 MidiChannel::Ch7 => 6,
54 MidiChannel::Ch8 => 7,
55 MidiChannel::Ch9 => 8,
56 MidiChannel::Ch10 => 9,
57 MidiChannel::Ch11 => 10,
58 MidiChannel::Ch12 => 11,
59 MidiChannel::Ch13 => 12,
60 MidiChannel::Ch14 => 13,
61 MidiChannel::Ch15 => 14,
62 MidiChannel::Ch16 => 15,
63 }
64 }
65
66 pub fn from_index(index: u8) -> Option<Self> {
68 match index {
69 0 => Some(MidiChannel::Ch1),
70 1 => Some(MidiChannel::Ch2),
71 2 => Some(MidiChannel::Ch3),
72 3 => Some(MidiChannel::Ch4),
73 4 => Some(MidiChannel::Ch5),
74 5 => Some(MidiChannel::Ch6),
75 6 => Some(MidiChannel::Ch7),
76 7 => Some(MidiChannel::Ch8),
77 8 => Some(MidiChannel::Ch9),
78 9 => Some(MidiChannel::Ch10),
79 10 => Some(MidiChannel::Ch11),
80 11 => Some(MidiChannel::Ch12),
81 12 => Some(MidiChannel::Ch13),
82 13 => Some(MidiChannel::Ch14),
83 14 => Some(MidiChannel::Ch15),
84 15 => Some(MidiChannel::Ch16),
85 _ => None,
86 }
87 }
88}
89
90impl fmt::Display for MidiChannel {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "Ch{}", self.as_index() + 1)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
101#[non_exhaustive]
102pub enum MidiEvent {
103 NoteOn {
105 channel: MidiChannel,
107 note: u8,
109 velocity: u8,
111 },
112 NoteOff {
114 channel: MidiChannel,
116 note: u8,
118 velocity: u8,
120 },
121 ControlChange {
123 channel: MidiChannel,
125 controller: u8,
127 value: u8,
129 },
130 ProgramChange {
132 channel: MidiChannel,
134 program: u8,
136 },
137 PitchBend {
139 channel: MidiChannel,
141 value: u16,
143 },
144 ChannelAftertouch {
146 channel: MidiChannel,
148 pressure: u8,
150 },
151 PolyAftertouch {
153 channel: MidiChannel,
155 note: u8,
157 pressure: u8,
159 },
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct NoteId(pub(crate) i32);
167
168impl NoteId {
169 pub fn raw(self) -> i32 {
171 self.0
172 }
173
174 pub fn from_raw(raw: i32) -> Self {
180 NoteId(raw)
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187#[non_exhaustive]
188pub enum NoteExpressionType {
189 Volume,
191 Pan,
193 Tuning,
195 Vibrato,
197 Expression,
199 Brightness,
201 Custom(u32),
203}
204
205impl NoteExpressionType {
206 pub(crate) fn type_id(self) -> u32 {
208 match self {
209 NoteExpressionType::Volume => 0,
210 NoteExpressionType::Pan => 1,
211 NoteExpressionType::Tuning => 2,
212 NoteExpressionType::Vibrato => 3,
213 NoteExpressionType::Expression => 4,
214 NoteExpressionType::Brightness => 5,
215 NoteExpressionType::Custom(id) => id,
216 }
217 }
218
219 pub(crate) fn from_type_id(id: u32) -> Self {
221 match id {
222 0 => NoteExpressionType::Volume,
223 1 => NoteExpressionType::Pan,
224 2 => NoteExpressionType::Tuning,
225 3 => NoteExpressionType::Vibrato,
226 4 => NoteExpressionType::Expression,
227 5 => NoteExpressionType::Brightness,
228 other => NoteExpressionType::Custom(other),
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct NoteExpressionInfo {
237 pub kind: NoteExpressionType,
239 pub title: String,
241 pub short_title: String,
243 pub units: String,
245 pub default_value: f64,
247 pub min: f64,
249 pub max: f64,
251 pub step_count: i32,
253 pub is_bipolar: bool,
255 pub is_one_shot: bool,
257 pub is_absolute: bool,
259}
260
261impl MidiEvent {
262 pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
270 let status = *bytes.first()?;
271 if !(0x80..0xF0).contains(&status) {
274 return None;
275 }
276 let channel = MidiChannel::from_index(status & 0x0F)?;
277 let d1 = || bytes.get(1).map(|b| b & 0x7F);
278 let d2 = || bytes.get(2).map(|b| b & 0x7F);
279 match status & 0xF0 {
280 0x90 => {
281 let note = d1()?;
282 let velocity = d2()?;
283 Some(if velocity == 0 {
284 MidiEvent::NoteOff {
285 channel,
286 note,
287 velocity: 0,
288 }
289 } else {
290 MidiEvent::NoteOn {
291 channel,
292 note,
293 velocity,
294 }
295 })
296 }
297 0x80 => Some(MidiEvent::NoteOff {
298 channel,
299 note: d1()?,
300 velocity: d2()?,
301 }),
302 0xB0 => Some(MidiEvent::ControlChange {
303 channel,
304 controller: d1()?,
305 value: d2()?,
306 }),
307 0xA0 => Some(MidiEvent::PolyAftertouch {
308 channel,
309 note: d1()?,
310 pressure: d2()?,
311 }),
312 0xD0 => Some(MidiEvent::ChannelAftertouch {
313 channel,
314 pressure: d1()?,
315 }),
316 0xE0 => {
317 let value = (d2()? as u16) << 7 | d1()? as u16;
318 Some(MidiEvent::PitchBend { channel, value })
319 }
320 0xC0 => Some(MidiEvent::ProgramChange {
321 channel,
322 program: d1()?,
323 }),
324 _ => None,
325 }
326 }
327}
328
329pub mod cc {
331 pub const BANK_SELECT_MSB: u8 = 0;
333 pub const MODULATION: u8 = 1;
335 pub const BREATH: u8 = 2;
337 pub const FOOT: u8 = 4;
339 pub const PORTAMENTO_TIME: u8 = 5;
341 pub const DATA_ENTRY_MSB: u8 = 6;
343 pub const VOLUME: u8 = 7;
345 pub const BALANCE: u8 = 8;
347 pub const PAN: u8 = 10;
349 pub const EXPRESSION: u8 = 11;
351 pub const SUSTAIN: u8 = 64;
353 pub const PORTAMENTO: u8 = 65;
355 pub const SOSTENUTO: u8 = 66;
357 pub const SOFT_PEDAL: u8 = 67;
359 pub const LEGATO: u8 = 68;
361 pub const HOLD_2: u8 = 69;
363 pub const SOUND_CONTROLLER_1: u8 = 70;
365 pub const SOUND_CONTROLLER_2: u8 = 71;
367 pub const SOUND_CONTROLLER_3: u8 = 72;
369 pub const SOUND_CONTROLLER_4: u8 = 73;
371 pub const SOUND_CONTROLLER_5: u8 = 74;
373 pub const SOUND_CONTROLLER_6: u8 = 75;
375 pub const SOUND_CONTROLLER_7: u8 = 76;
377 pub const SOUND_CONTROLLER_8: u8 = 77;
379 pub const SOUND_CONTROLLER_9: u8 = 78;
381 pub const SOUND_CONTROLLER_10: u8 = 79;
383 pub const GENERAL_PURPOSE_1: u8 = 80;
385 pub const GENERAL_PURPOSE_2: u8 = 81;
387 pub const GENERAL_PURPOSE_3: u8 = 82;
389 pub const GENERAL_PURPOSE_4: u8 = 83;
391 pub const PORTAMENTO_CONTROL: u8 = 84;
393 pub const REVERB_DEPTH: u8 = 91;
395 pub const TREMOLO_DEPTH: u8 = 92;
397 pub const CHORUS_DEPTH: u8 = 93;
399 pub const CELESTE_DEPTH: u8 = 94;
401 pub const PHASER_DEPTH: u8 = 95;
403 pub const DATA_INCREMENT: u8 = 96;
405 pub const DATA_DECREMENT: u8 = 97;
407 pub const NRPN_LSB: u8 = 98;
409 pub const NRPN_MSB: u8 = 99;
411 pub const RPN_LSB: u8 = 100;
413 pub const RPN_MSB: u8 = 101;
415 pub const ALL_SOUNDS_OFF: u8 = 120;
417 pub const RESET_ALL_CONTROLLERS: u8 = 121;
419 pub const LOCAL_CONTROL: u8 = 122;
421 pub const ALL_NOTES_OFF: u8 = 123;
423 pub const OMNI_MODE_OFF: u8 = 124;
425 pub const OMNI_MODE_ON: u8 = 125;
427 pub const MONO_MODE_ON: u8 = 126;
429 pub const POLY_MODE_ON: u8 = 127;
431}
432
433pub fn note_to_name(note: u8) -> String {
436 let note_names = [
437 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
438 ];
439 let octave = (note as i32 / 12) - 2;
440 let note_in_octave = note % 12;
441 format!("{}{}", note_names[note_in_octave as usize], octave)
442}
443
444pub fn name_to_note(name: &str) -> Option<u8> {
448 let name = name.trim().to_uppercase();
449
450 let (note_part, octave_str) = if name.contains('#') {
452 let parts: Vec<&str> = name.split('#').collect();
453 if parts.len() != 2 {
454 return None;
455 }
456 (format!("{}#", parts[0]), parts[1])
457 } else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
458 (format!("{}B", &name[0..1]), &name[2..])
460 } else {
461 let mut chars = name.chars();
463 let note = chars.next()?.to_string();
464 let octave = chars.as_str();
465 (note, octave)
466 };
467
468 let octave: i32 = octave_str.parse().ok()?;
470
471 let semitone = match note_part.as_str() {
473 "C" => 0,
474 "C#" | "DB" => 1,
475 "D" => 2,
476 "D#" | "EB" => 3,
477 "E" => 4,
478 "F" => 5,
479 "F#" | "GB" => 6,
480 "G" => 7,
481 "G#" | "AB" => 8,
482 "A" => 9,
483 "A#" | "BB" => 10,
484 "B" => 11,
485 _ => return None,
486 };
487
488 let midi_note = (octave + 2) * 12 + semitone;
491
492 if (0..=127).contains(&midi_note) {
493 Some(midi_note as u8)
494 } else {
495 None
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn note_expression_type_ids_round_trip() {
505 for kind in [
506 NoteExpressionType::Volume,
507 NoteExpressionType::Pan,
508 NoteExpressionType::Tuning,
509 NoteExpressionType::Vibrato,
510 NoteExpressionType::Expression,
511 NoteExpressionType::Brightness,
512 NoteExpressionType::Custom(100_001),
513 ] {
514 assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
515 }
516 assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
518 assert_eq!(
519 NoteExpressionType::from_type_id(5),
520 NoteExpressionType::Brightness
521 );
522 }
523
524 #[test]
525 fn from_midi_bytes_maps_channel_voice_messages() {
526 assert_eq!(
528 MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
529 Some(MidiEvent::NoteOn {
530 channel: MidiChannel::Ch1,
531 note: 60,
532 velocity: 100
533 })
534 );
535 assert_eq!(
537 MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
538 Some(MidiEvent::NoteOff {
539 channel: MidiChannel::Ch1,
540 note: 60,
541 velocity: 0
542 })
543 );
544 assert_eq!(
546 MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
547 Some(MidiEvent::NoteOff {
548 channel: MidiChannel::Ch10,
549 note: 64,
550 velocity: 40
551 })
552 );
553 assert_eq!(
555 MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
556 Some(MidiEvent::ControlChange {
557 channel: MidiChannel::Ch1,
558 controller: 1,
559 value: 64
560 })
561 );
562 assert_eq!(
564 MidiEvent::from_midi_bytes(&[0xD0, 90]),
565 Some(MidiEvent::ChannelAftertouch {
566 channel: MidiChannel::Ch1,
567 pressure: 90
568 })
569 );
570 assert_eq!(
571 MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
572 Some(MidiEvent::PolyAftertouch {
573 channel: MidiChannel::Ch1,
574 note: 60,
575 pressure: 70
576 })
577 );
578 }
579
580 #[test]
581 fn from_midi_bytes_pitch_bend_is_14_bit() {
582 assert_eq!(
584 MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
585 Some(MidiEvent::PitchBend {
586 channel: MidiChannel::Ch1,
587 value: 8192
588 })
589 );
590 assert_eq!(
592 MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
593 Some(MidiEvent::PitchBend {
594 channel: MidiChannel::Ch1,
595 value: 16383
596 })
597 );
598 }
599
600 #[test]
601 fn from_midi_bytes_rejects_unsupported_and_junk() {
602 assert_eq!(MidiEvent::from_midi_bytes(&[]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); }
608
609 #[test]
610 fn from_midi_bytes_maps_program_change() {
611 assert_eq!(
612 MidiEvent::from_midi_bytes(&[0xC0, 5]),
613 Some(MidiEvent::ProgramChange {
614 channel: MidiChannel::Ch1,
615 program: 5
616 })
617 );
618 assert_eq!(
620 MidiEvent::from_midi_bytes(&[0xC9, 0xFF]),
621 Some(MidiEvent::ProgramChange {
622 channel: MidiChannel::Ch10,
623 program: 127
624 })
625 );
626 assert_eq!(MidiEvent::from_midi_bytes(&[0xC0]), None);
628 }
629
630 #[test]
631 fn test_midi_conversions() {
632 assert_eq!(name_to_note("C3"), Some(60));
634 assert_eq!(name_to_note("C2"), Some(48));
635 assert_eq!(name_to_note("A3"), Some(69)); assert_eq!(name_to_note("C-2"), Some(0));
637 assert_eq!(name_to_note("G8"), Some(127));
638
639 assert_eq!(note_to_name(60), "C3");
641 assert_eq!(note_to_name(48), "C2");
642 assert_eq!(note_to_name(69), "A3");
643 assert_eq!(note_to_name(0), "C-2");
644 assert_eq!(note_to_name(127), "G8");
645
646 assert_eq!(name_to_note("C#3"), Some(61));
648 assert_eq!(name_to_note("Db3"), Some(61));
649 assert_eq!(name_to_note("F#3"), Some(66));
650 }
651
652 #[test]
653 fn test_midi_channel() {
654 assert_eq!(MidiChannel::Ch1.as_index(), 0);
655 assert_eq!(MidiChannel::Ch16.as_index(), 15);
656 assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
657 assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
658 assert_eq!(MidiChannel::from_index(16), None);
659 }
660}