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
162pub const MAX_EVENT_PAYLOAD_BYTES: usize = 1024 * 1024;
168
169pub const MAX_EVENT_TEXT_UNITS: usize = 16 * 1024;
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177pub struct PluginEvent {
178 pub bus_index: i32,
180 pub sample_offset: i32,
182 pub ppq_position: f64,
184 pub flags: u16,
186 pub data: PluginEventData,
188}
189
190pub type OutputEvent = PluginEvent;
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198#[non_exhaustive]
199#[allow(missing_docs)]
200pub enum PluginEventData {
201 NoteOn {
203 channel: i16,
204 pitch: i16,
205 tuning: f32,
206 velocity: f32,
207 length: i32,
208 note_id: i32,
209 },
210 NoteOff {
212 channel: i16,
213 pitch: i16,
214 velocity: f32,
215 note_id: i32,
216 tuning: f32,
217 },
218 Data { data_type: u32, bytes: Vec<u8> },
220 PolyPressure {
222 channel: i16,
223 pitch: i16,
224 pressure: f32,
225 note_id: i32,
226 },
227 NoteExpressionValue {
229 type_id: u32,
230 note_id: i32,
231 value: f64,
232 },
233 NoteExpressionText {
235 type_id: u32,
236 note_id: i32,
237 text: Vec<u16>,
238 },
239 NoteExpressionIntValue {
241 type_id: u32,
242 note_id: i32,
243 value: u64,
244 },
245 Chord {
247 root: i16,
248 bass_note: i16,
249 mask: i16,
250 text: Vec<u16>,
251 },
252 Scale {
254 root: i16,
255 mask: i16,
256 text: Vec<u16>,
257 },
258 LegacyMidiCcOut {
260 control_number: u8,
261 channel: i8,
262 value: u8,
263 value2: u8,
264 },
265}
266
267impl PluginEvent {
268 pub fn sysex(bytes: Vec<u8>) -> Self {
270 Self {
271 bus_index: 0,
272 sample_offset: 0,
273 ppq_position: 0.0,
274 flags: 0,
275 data: PluginEventData::Data {
276 data_type: 0,
277 bytes,
278 },
279 }
280 }
281
282 pub fn at(mut self, sample_offset: i32) -> Self {
284 self.sample_offset = sample_offset;
285 self
286 }
287
288 pub fn to_midi(&self) -> Option<MidiEvent> {
293 let byte = |value: f32| (value * 127.0).round().clamp(0.0, 127.0) as u8;
294 match &self.data {
295 PluginEventData::NoteOn {
296 channel,
297 pitch,
298 velocity,
299 ..
300 } => Some(MidiEvent::NoteOn {
301 channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
302 note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
303 velocity: byte(*velocity),
304 }),
305 PluginEventData::NoteOff {
306 channel,
307 pitch,
308 velocity,
309 ..
310 } => Some(MidiEvent::NoteOff {
311 channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
312 note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
313 velocity: byte(*velocity),
314 }),
315 PluginEventData::PolyPressure {
316 channel,
317 pitch,
318 pressure,
319 ..
320 } => Some(MidiEvent::PolyAftertouch {
321 channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
322 note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
323 pressure: byte(*pressure),
324 }),
325 PluginEventData::LegacyMidiCcOut {
326 control_number,
327 channel,
328 value,
329 value2,
330 } => {
331 let channel = MidiChannel::from_index(u8::try_from(*channel).ok()?)?;
332 match u32::from(*control_number) {
333 129 => Some(MidiEvent::PitchBend {
334 channel,
335 value: (u16::from(*value2 & 0x7f) << 7) | u16::from(*value & 0x7f),
336 }),
337 128 => Some(MidiEvent::ChannelAftertouch {
338 channel,
339 pressure: *value & 0x7f,
340 }),
341 130 => Some(MidiEvent::ProgramChange {
342 channel,
343 program: *value & 0x7f,
344 }),
345 cc if cc < 128 => Some(MidiEvent::ControlChange {
346 channel,
347 controller: cc as u8,
348 value: *value & 0x7f,
349 }),
350 _ => None,
351 }
352 }
353 _ => None,
354 }
355 }
356
357 pub(crate) fn payload_bytes(&self) -> usize {
359 match &self.data {
360 PluginEventData::Data { bytes, .. } => bytes.len(),
361 PluginEventData::NoteExpressionText { text, .. }
362 | PluginEventData::Chord { text, .. }
363 | PluginEventData::Scale { text, .. } => text.len().saturating_mul(2),
364 _ => 0,
365 }
366 }
367}
368
369impl From<MidiEvent> for PluginEvent {
370 fn from(event: MidiEvent) -> Self {
371 let data = match event {
372 MidiEvent::NoteOn {
373 channel,
374 note,
375 velocity,
376 } => PluginEventData::NoteOn {
377 channel: i16::from(channel.as_index()),
378 pitch: i16::from(note),
379 tuning: 0.0,
380 velocity: f32::from(velocity) / 127.0,
381 length: 0,
382 note_id: -1,
383 },
384 MidiEvent::NoteOff {
385 channel,
386 note,
387 velocity,
388 } => PluginEventData::NoteOff {
389 channel: i16::from(channel.as_index()),
390 pitch: i16::from(note),
391 velocity: f32::from(velocity) / 127.0,
392 note_id: -1,
393 tuning: 0.0,
394 },
395 MidiEvent::ControlChange {
396 channel,
397 controller,
398 value,
399 } => PluginEventData::LegacyMidiCcOut {
400 control_number: controller,
401 channel: channel.as_index() as i8,
402 value,
403 value2: 0,
404 },
405 MidiEvent::ProgramChange { channel, program } => PluginEventData::LegacyMidiCcOut {
406 control_number: 130,
407 channel: channel.as_index() as i8,
408 value: program,
409 value2: 0,
410 },
411 MidiEvent::PitchBend { channel, value } => PluginEventData::LegacyMidiCcOut {
412 control_number: 129,
413 channel: channel.as_index() as i8,
414 value: (value & 0x7f) as u8,
415 value2: ((value >> 7) & 0x7f) as u8,
416 },
417 MidiEvent::ChannelAftertouch { channel, pressure } => {
418 PluginEventData::LegacyMidiCcOut {
419 control_number: 128,
420 channel: channel.as_index() as i8,
421 value: pressure,
422 value2: 0,
423 }
424 }
425 MidiEvent::PolyAftertouch {
426 channel,
427 note,
428 pressure,
429 } => PluginEventData::PolyPressure {
430 channel: i16::from(channel.as_index()),
431 pitch: i16::from(note),
432 pressure: f32::from(pressure) / 127.0,
433 note_id: -1,
434 },
435 };
436 Self {
437 bus_index: 0,
438 sample_offset: 0,
439 ppq_position: 0.0,
440 flags: 0,
441 data,
442 }
443 }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
450pub struct NoteId(pub(crate) i32);
451
452impl NoteId {
453 pub fn raw(self) -> i32 {
455 self.0
456 }
457
458 pub fn from_raw(raw: i32) -> Self {
464 NoteId(raw)
465 }
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
471#[non_exhaustive]
472pub enum NoteExpressionType {
473 Volume,
475 Pan,
477 Tuning,
479 Vibrato,
481 Expression,
483 Brightness,
485 Custom(u32),
487}
488
489impl NoteExpressionType {
490 pub(crate) fn type_id(self) -> u32 {
492 match self {
493 NoteExpressionType::Volume => 0,
494 NoteExpressionType::Pan => 1,
495 NoteExpressionType::Tuning => 2,
496 NoteExpressionType::Vibrato => 3,
497 NoteExpressionType::Expression => 4,
498 NoteExpressionType::Brightness => 5,
499 NoteExpressionType::Custom(id) => id,
500 }
501 }
502
503 pub(crate) fn from_type_id(id: u32) -> Self {
505 match id {
506 0 => NoteExpressionType::Volume,
507 1 => NoteExpressionType::Pan,
508 2 => NoteExpressionType::Tuning,
509 3 => NoteExpressionType::Vibrato,
510 4 => NoteExpressionType::Expression,
511 5 => NoteExpressionType::Brightness,
512 other => NoteExpressionType::Custom(other),
513 }
514 }
515}
516
517#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520pub struct NoteExpressionInfo {
521 pub kind: NoteExpressionType,
523 pub title: String,
525 pub short_title: String,
527 pub units: String,
529 pub default_value: f64,
531 pub min: f64,
533 pub max: f64,
535 pub step_count: i32,
537 pub is_bipolar: bool,
539 pub is_one_shot: bool,
541 pub is_absolute: bool,
543}
544
545impl MidiEvent {
546 pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
554 let status = *bytes.first()?;
555 if !(0x80..0xF0).contains(&status) {
558 return None;
559 }
560 let channel = MidiChannel::from_index(status & 0x0F)?;
561 let d1 = || bytes.get(1).map(|b| b & 0x7F);
562 let d2 = || bytes.get(2).map(|b| b & 0x7F);
563 match status & 0xF0 {
564 0x90 => {
565 let note = d1()?;
566 let velocity = d2()?;
567 Some(if velocity == 0 {
568 MidiEvent::NoteOff {
569 channel,
570 note,
571 velocity: 0,
572 }
573 } else {
574 MidiEvent::NoteOn {
575 channel,
576 note,
577 velocity,
578 }
579 })
580 }
581 0x80 => Some(MidiEvent::NoteOff {
582 channel,
583 note: d1()?,
584 velocity: d2()?,
585 }),
586 0xB0 => Some(MidiEvent::ControlChange {
587 channel,
588 controller: d1()?,
589 value: d2()?,
590 }),
591 0xA0 => Some(MidiEvent::PolyAftertouch {
592 channel,
593 note: d1()?,
594 pressure: d2()?,
595 }),
596 0xD0 => Some(MidiEvent::ChannelAftertouch {
597 channel,
598 pressure: d1()?,
599 }),
600 0xE0 => {
601 let value = (d2()? as u16) << 7 | d1()? as u16;
602 Some(MidiEvent::PitchBend { channel, value })
603 }
604 0xC0 => Some(MidiEvent::ProgramChange {
605 channel,
606 program: d1()?,
607 }),
608 _ => None,
609 }
610 }
611}
612
613pub mod cc {
615 pub const BANK_SELECT_MSB: u8 = 0;
617 pub const MODULATION: u8 = 1;
619 pub const BREATH: u8 = 2;
621 pub const FOOT: u8 = 4;
623 pub const PORTAMENTO_TIME: u8 = 5;
625 pub const DATA_ENTRY_MSB: u8 = 6;
627 pub const VOLUME: u8 = 7;
629 pub const BALANCE: u8 = 8;
631 pub const PAN: u8 = 10;
633 pub const EXPRESSION: u8 = 11;
635 pub const SUSTAIN: u8 = 64;
637 pub const PORTAMENTO: u8 = 65;
639 pub const SOSTENUTO: u8 = 66;
641 pub const SOFT_PEDAL: u8 = 67;
643 pub const LEGATO: u8 = 68;
645 pub const HOLD_2: u8 = 69;
647 pub const SOUND_CONTROLLER_1: u8 = 70;
649 pub const SOUND_CONTROLLER_2: u8 = 71;
651 pub const SOUND_CONTROLLER_3: u8 = 72;
653 pub const SOUND_CONTROLLER_4: u8 = 73;
655 pub const SOUND_CONTROLLER_5: u8 = 74;
657 pub const SOUND_CONTROLLER_6: u8 = 75;
659 pub const SOUND_CONTROLLER_7: u8 = 76;
661 pub const SOUND_CONTROLLER_8: u8 = 77;
663 pub const SOUND_CONTROLLER_9: u8 = 78;
665 pub const SOUND_CONTROLLER_10: u8 = 79;
667 pub const GENERAL_PURPOSE_1: u8 = 80;
669 pub const GENERAL_PURPOSE_2: u8 = 81;
671 pub const GENERAL_PURPOSE_3: u8 = 82;
673 pub const GENERAL_PURPOSE_4: u8 = 83;
675 pub const PORTAMENTO_CONTROL: u8 = 84;
677 pub const REVERB_DEPTH: u8 = 91;
679 pub const TREMOLO_DEPTH: u8 = 92;
681 pub const CHORUS_DEPTH: u8 = 93;
683 pub const CELESTE_DEPTH: u8 = 94;
685 pub const PHASER_DEPTH: u8 = 95;
687 pub const DATA_INCREMENT: u8 = 96;
689 pub const DATA_DECREMENT: u8 = 97;
691 pub const NRPN_LSB: u8 = 98;
693 pub const NRPN_MSB: u8 = 99;
695 pub const RPN_LSB: u8 = 100;
697 pub const RPN_MSB: u8 = 101;
699 pub const ALL_SOUNDS_OFF: u8 = 120;
701 pub const RESET_ALL_CONTROLLERS: u8 = 121;
703 pub const LOCAL_CONTROL: u8 = 122;
705 pub const ALL_NOTES_OFF: u8 = 123;
707 pub const OMNI_MODE_OFF: u8 = 124;
709 pub const OMNI_MODE_ON: u8 = 125;
711 pub const MONO_MODE_ON: u8 = 126;
713 pub const POLY_MODE_ON: u8 = 127;
715}
716
717pub fn note_to_name(note: u8) -> String {
724 if note > 127 {
725 return format!("Invalid({note})");
726 }
727 let note_names = [
728 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
729 ];
730 let octave = (note as i32 / 12) - 2;
731 let note_in_octave = note % 12;
732 format!("{}{}", note_names[note_in_octave as usize], octave)
733}
734
735pub fn name_to_note(name: &str) -> Option<u8> {
739 let name = name.trim().to_uppercase();
740
741 let mut chars = name.chars();
744 let letter = chars.next()?;
745 if !letter.is_ascii_alphabetic() {
746 return None;
747 }
748
749 let rest = chars.as_str();
753 let (accidental, octave_str) = match rest.chars().next() {
754 Some('#') => (Some('#'), &rest[1..]),
755 Some('B') => (Some('B'), &rest[1..]),
756 _ => (None, rest),
757 };
758
759 let octave: i32 = octave_str.parse().ok()?;
761
762 let semitone = match (letter, accidental) {
764 ('C', None) => 0,
765 ('C', Some('#')) | ('D', Some('B')) => 1,
766 ('D', None) => 2,
767 ('D', Some('#')) | ('E', Some('B')) => 3,
768 ('E', None) => 4,
769 ('F', None) => 5,
770 ('F', Some('#')) | ('G', Some('B')) => 6,
771 ('G', None) => 7,
772 ('G', Some('#')) | ('A', Some('B')) => 8,
773 ('A', None) => 9,
774 ('A', Some('#')) | ('B', Some('B')) => 10,
775 ('B', None) => 11,
776 _ => return None,
777 };
778
779 let midi_note = octave
783 .checked_add(2)
784 .and_then(|o| o.checked_mul(12))
785 .and_then(|base| base.checked_add(semitone))?;
786
787 if (0..=127).contains(&midi_note) {
788 Some(midi_note as u8)
789 } else {
790 None
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
799 fn note_expression_type_ids_round_trip() {
800 for kind in [
801 NoteExpressionType::Volume,
802 NoteExpressionType::Pan,
803 NoteExpressionType::Tuning,
804 NoteExpressionType::Vibrato,
805 NoteExpressionType::Expression,
806 NoteExpressionType::Brightness,
807 NoteExpressionType::Custom(100_001),
808 ] {
809 assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
810 }
811 assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
813 assert_eq!(
814 NoteExpressionType::from_type_id(5),
815 NoteExpressionType::Brightness
816 );
817 }
818
819 #[test]
820 fn from_midi_bytes_maps_channel_voice_messages() {
821 assert_eq!(
823 MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
824 Some(MidiEvent::NoteOn {
825 channel: MidiChannel::Ch1,
826 note: 60,
827 velocity: 100
828 })
829 );
830 assert_eq!(
832 MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
833 Some(MidiEvent::NoteOff {
834 channel: MidiChannel::Ch1,
835 note: 60,
836 velocity: 0
837 })
838 );
839 assert_eq!(
841 MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
842 Some(MidiEvent::NoteOff {
843 channel: MidiChannel::Ch10,
844 note: 64,
845 velocity: 40
846 })
847 );
848 assert_eq!(
850 MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
851 Some(MidiEvent::ControlChange {
852 channel: MidiChannel::Ch1,
853 controller: 1,
854 value: 64
855 })
856 );
857 assert_eq!(
859 MidiEvent::from_midi_bytes(&[0xD0, 90]),
860 Some(MidiEvent::ChannelAftertouch {
861 channel: MidiChannel::Ch1,
862 pressure: 90
863 })
864 );
865 assert_eq!(
866 MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
867 Some(MidiEvent::PolyAftertouch {
868 channel: MidiChannel::Ch1,
869 note: 60,
870 pressure: 70
871 })
872 );
873 }
874
875 #[test]
876 fn from_midi_bytes_pitch_bend_is_14_bit() {
877 assert_eq!(
879 MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
880 Some(MidiEvent::PitchBend {
881 channel: MidiChannel::Ch1,
882 value: 8192
883 })
884 );
885 assert_eq!(
887 MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
888 Some(MidiEvent::PitchBend {
889 channel: MidiChannel::Ch1,
890 value: 16383
891 })
892 );
893 }
894
895 #[test]
896 fn from_midi_bytes_rejects_unsupported_and_junk() {
897 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); }
903
904 #[test]
905 fn from_midi_bytes_maps_program_change() {
906 assert_eq!(
907 MidiEvent::from_midi_bytes(&[0xC0, 5]),
908 Some(MidiEvent::ProgramChange {
909 channel: MidiChannel::Ch1,
910 program: 5
911 })
912 );
913 assert_eq!(
915 MidiEvent::from_midi_bytes(&[0xC9, 0xFF]),
916 Some(MidiEvent::ProgramChange {
917 channel: MidiChannel::Ch10,
918 program: 127
919 })
920 );
921 assert_eq!(MidiEvent::from_midi_bytes(&[0xC0]), None);
923 }
924
925 #[test]
926 fn test_midi_conversions() {
927 assert_eq!(name_to_note("C3"), Some(60));
929 assert_eq!(name_to_note("C2"), Some(48));
930 assert_eq!(name_to_note("A3"), Some(69)); assert_eq!(name_to_note("C-2"), Some(0));
932 assert_eq!(name_to_note("G8"), Some(127));
933
934 assert_eq!(note_to_name(60), "C3");
936 assert_eq!(note_to_name(48), "C2");
937 assert_eq!(note_to_name(69), "A3");
938 assert_eq!(note_to_name(0), "C-2");
939 assert_eq!(note_to_name(127), "G8");
940
941 assert_eq!(name_to_note("C#3"), Some(61));
943 assert_eq!(name_to_note("Db3"), Some(61));
944 assert_eq!(name_to_note("F#3"), Some(66));
945 }
946
947 #[test]
951 fn name_to_note_rejects_junk_without_panicking() {
952 for junk in [
953 "éB3", "ÉB3", "日本語", "", "3", "H3", "C", "C#", "Cb3", "C##3", "CB3", "C99", "C-99", "#3", "C2147483647",
970 "C-2147483648",
971 "Bb2147483647",
972 "C#2147483647",
973 ] {
974 assert_eq!(name_to_note(junk), None, "expected None for {junk:?}");
975 }
976 }
977
978 #[test]
981 fn name_to_note_distinguishes_b_natural_from_flats() {
982 assert_eq!(name_to_note("B3"), Some(71));
983 assert_eq!(name_to_note("Bb3"), Some(70));
984 assert_eq!(name_to_note("bb3"), Some(70)); assert_eq!(name_to_note("Eb3"), Some(63));
986 assert_eq!(name_to_note("Ab3"), Some(68));
987 for n in 0..=127u8 {
989 assert_eq!(name_to_note(¬e_to_name(n)), Some(n), "round-trip {n}");
990 }
991 }
992
993 #[test]
997 fn note_to_name_marks_out_of_domain_notes_instead_of_fabricating_one() {
998 for n in 128..=255u8 {
999 let name = note_to_name(n);
1000 assert_eq!(name, format!("Invalid({n})"));
1001 assert_eq!(
1002 name_to_note(&name),
1003 None,
1004 "{name} must not parse back as a note"
1005 );
1006 }
1007 for n in 0..=127u8 {
1009 let name = note_to_name(n);
1010 assert!(!name.starts_with("Invalid"), "note {n} rendered as {name}");
1011 assert_eq!(name_to_note(&name), Some(n), "round-trip {n}");
1012 }
1013 }
1014
1015 #[test]
1016 fn test_midi_channel() {
1017 assert_eq!(MidiChannel::Ch1.as_index(), 0);
1018 assert_eq!(MidiChannel::Ch16.as_index(), 15);
1019 assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
1020 assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
1021 assert_eq!(MidiChannel::from_index(16), None);
1022 }
1023}