Skip to main content

use_midi/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8    pub use crate::{
9        MidiChannel, MidiControllerNumber, MidiDeviceKind, MidiError, MidiEventKind,
10        MidiMessageKind, MidiNoteNumber, MidiPortName, MidiProfileKind, MidiProgramNumber,
11        MidiPropertyKind, MidiUmpMessageKind, MidiVelocity, MidiVersion,
12    };
13}
14#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct MidiPortName(String);
16
17impl MidiPortName {
18    pub fn new(value: impl AsRef<str>) -> Result<Self, MidiError> {
19        non_empty_text(value).map(Self)
20    }
21
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25
26    pub fn value(&self) -> &str {
27        self.as_str()
28    }
29
30    pub fn into_string(self) -> String {
31        self.0
32    }
33}
34
35impl AsRef<str> for MidiPortName {
36    fn as_ref(&self) -> &str {
37        self.as_str()
38    }
39}
40
41impl fmt::Display for MidiPortName {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47impl FromStr for MidiPortName {
48    type Err = MidiError;
49
50    fn from_str(value: &str) -> Result<Self, Self::Err> {
51        Self::new(value)
52    }
53}
54
55impl TryFrom<&str> for MidiPortName {
56    type Error = MidiError;
57
58    fn try_from(value: &str) -> Result<Self, Self::Error> {
59        Self::new(value)
60    }
61}
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct MidiChannel(u8);
64
65impl MidiChannel {
66    pub fn new(value: u8) -> Result<Self, MidiError> {
67        if !(1..=16).contains(&value) {
68            return Err(MidiError::OutOfRange);
69        }
70
71        Ok(Self(value))
72    }
73
74    pub const fn value(self) -> u8 {
75        self.0
76    }
77}
78
79impl fmt::Display for MidiChannel {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        self.0.fmt(formatter)
82    }
83}
84
85impl FromStr for MidiChannel {
86    type Err = MidiError;
87
88    fn from_str(value: &str) -> Result<Self, Self::Err> {
89        let parsed = value
90            .trim()
91            .parse::<u8>()
92            .map_err(|_| MidiError::InvalidFormat)?;
93        Self::new(parsed)
94    }
95}
96
97impl TryFrom<u8> for MidiChannel {
98    type Error = MidiError;
99
100    fn try_from(value: u8) -> Result<Self, Self::Error> {
101        Self::new(value)
102    }
103}
104#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub struct MidiNoteNumber(u8);
106
107impl MidiNoteNumber {
108    pub fn new(value: u8) -> Result<Self, MidiError> {
109        if !(0..=127).contains(&value) {
110            return Err(MidiError::OutOfRange);
111        }
112
113        Ok(Self(value))
114    }
115
116    pub const fn value(self) -> u8 {
117        self.0
118    }
119}
120
121impl fmt::Display for MidiNoteNumber {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        self.0.fmt(formatter)
124    }
125}
126
127impl FromStr for MidiNoteNumber {
128    type Err = MidiError;
129
130    fn from_str(value: &str) -> Result<Self, Self::Err> {
131        let parsed = value
132            .trim()
133            .parse::<u8>()
134            .map_err(|_| MidiError::InvalidFormat)?;
135        Self::new(parsed)
136    }
137}
138
139impl TryFrom<u8> for MidiNoteNumber {
140    type Error = MidiError;
141
142    fn try_from(value: u8) -> Result<Self, Self::Error> {
143        Self::new(value)
144    }
145}
146#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
147pub struct MidiVelocity(u8);
148
149impl MidiVelocity {
150    pub fn new(value: u8) -> Result<Self, MidiError> {
151        if !(0..=127).contains(&value) {
152            return Err(MidiError::OutOfRange);
153        }
154
155        Ok(Self(value))
156    }
157
158    pub const fn value(self) -> u8 {
159        self.0
160    }
161}
162
163impl fmt::Display for MidiVelocity {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        self.0.fmt(formatter)
166    }
167}
168
169impl FromStr for MidiVelocity {
170    type Err = MidiError;
171
172    fn from_str(value: &str) -> Result<Self, Self::Err> {
173        let parsed = value
174            .trim()
175            .parse::<u8>()
176            .map_err(|_| MidiError::InvalidFormat)?;
177        Self::new(parsed)
178    }
179}
180
181impl TryFrom<u8> for MidiVelocity {
182    type Error = MidiError;
183
184    fn try_from(value: u8) -> Result<Self, Self::Error> {
185        Self::new(value)
186    }
187}
188#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
189pub struct MidiControllerNumber(u8);
190
191impl MidiControllerNumber {
192    pub fn new(value: u8) -> Result<Self, MidiError> {
193        if !(0..=127).contains(&value) {
194            return Err(MidiError::OutOfRange);
195        }
196
197        Ok(Self(value))
198    }
199
200    pub const fn value(self) -> u8 {
201        self.0
202    }
203}
204
205impl fmt::Display for MidiControllerNumber {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        self.0.fmt(formatter)
208    }
209}
210
211impl FromStr for MidiControllerNumber {
212    type Err = MidiError;
213
214    fn from_str(value: &str) -> Result<Self, Self::Err> {
215        let parsed = value
216            .trim()
217            .parse::<u8>()
218            .map_err(|_| MidiError::InvalidFormat)?;
219        Self::new(parsed)
220    }
221}
222
223impl TryFrom<u8> for MidiControllerNumber {
224    type Error = MidiError;
225
226    fn try_from(value: u8) -> Result<Self, Self::Error> {
227        Self::new(value)
228    }
229}
230#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
231pub struct MidiProgramNumber(u8);
232
233impl MidiProgramNumber {
234    pub fn new(value: u8) -> Result<Self, MidiError> {
235        if !(0..=127).contains(&value) {
236            return Err(MidiError::OutOfRange);
237        }
238
239        Ok(Self(value))
240    }
241
242    pub const fn value(self) -> u8 {
243        self.0
244    }
245}
246
247impl fmt::Display for MidiProgramNumber {
248    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249        self.0.fmt(formatter)
250    }
251}
252
253impl FromStr for MidiProgramNumber {
254    type Err = MidiError;
255
256    fn from_str(value: &str) -> Result<Self, Self::Err> {
257        let parsed = value
258            .trim()
259            .parse::<u8>()
260            .map_err(|_| MidiError::InvalidFormat)?;
261        Self::new(parsed)
262    }
263}
264
265impl TryFrom<u8> for MidiProgramNumber {
266    type Error = MidiError;
267
268    fn try_from(value: u8) -> Result<Self, Self::Error> {
269        Self::new(value)
270    }
271}
272#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
273pub enum MidiVersion {
274    Midi1,
275    Midi2,
276}
277
278impl MidiVersion {
279    pub const ALL: &'static [Self] = &[Self::Midi1, Self::Midi2];
280
281    pub const fn as_str(self) -> &'static str {
282        match self {
283            Self::Midi1 => "midi-1",
284            Self::Midi2 => "midi-2",
285        }
286    }
287}
288
289impl fmt::Display for MidiVersion {
290    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291        formatter.write_str(self.as_str())
292    }
293}
294
295impl FromStr for MidiVersion {
296    type Err = MidiError;
297
298    fn from_str(value: &str) -> Result<Self, Self::Err> {
299        match normalized_label(value)?.as_str() {
300            "midi-1" => Ok(Self::Midi1),
301            "midi-2" => Ok(Self::Midi2),
302            _ => Err(MidiError::UnknownLabel),
303        }
304    }
305}
306#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
307pub enum MidiMessageKind {
308    NoteOff,
309    NoteOn,
310    PolyphonicKeyPressure,
311    ControlChange,
312    ProgramChange,
313    ChannelPressure,
314    PitchBend,
315    SystemExclusive,
316    SystemCommon,
317    SystemRealTime,
318    UniversalMidiPacket,
319    Unknown,
320}
321
322impl MidiMessageKind {
323    pub const ALL: &'static [Self] = &[
324        Self::NoteOff,
325        Self::NoteOn,
326        Self::PolyphonicKeyPressure,
327        Self::ControlChange,
328        Self::ProgramChange,
329        Self::ChannelPressure,
330        Self::PitchBend,
331        Self::SystemExclusive,
332        Self::SystemCommon,
333        Self::SystemRealTime,
334        Self::UniversalMidiPacket,
335        Self::Unknown,
336    ];
337
338    pub const fn as_str(self) -> &'static str {
339        match self {
340            Self::NoteOff => "note-off",
341            Self::NoteOn => "note-on",
342            Self::PolyphonicKeyPressure => "polyphonic-key-pressure",
343            Self::ControlChange => "control-change",
344            Self::ProgramChange => "program-change",
345            Self::ChannelPressure => "channel-pressure",
346            Self::PitchBend => "pitch-bend",
347            Self::SystemExclusive => "system-exclusive",
348            Self::SystemCommon => "system-common",
349            Self::SystemRealTime => "system-real-time",
350            Self::UniversalMidiPacket => "universal-midi-packet",
351            Self::Unknown => "unknown",
352        }
353    }
354}
355
356impl fmt::Display for MidiMessageKind {
357    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
358        formatter.write_str(self.as_str())
359    }
360}
361
362impl FromStr for MidiMessageKind {
363    type Err = MidiError;
364
365    fn from_str(value: &str) -> Result<Self, Self::Err> {
366        match normalized_label(value)?.as_str() {
367            "note-off" => Ok(Self::NoteOff),
368            "note-on" => Ok(Self::NoteOn),
369            "polyphonic-key-pressure" => Ok(Self::PolyphonicKeyPressure),
370            "control-change" => Ok(Self::ControlChange),
371            "program-change" => Ok(Self::ProgramChange),
372            "channel-pressure" => Ok(Self::ChannelPressure),
373            "pitch-bend" => Ok(Self::PitchBend),
374            "system-exclusive" => Ok(Self::SystemExclusive),
375            "system-common" => Ok(Self::SystemCommon),
376            "system-real-time" => Ok(Self::SystemRealTime),
377            "universal-midi-packet" => Ok(Self::UniversalMidiPacket),
378            "unknown" => Ok(Self::Unknown),
379            _ => Err(MidiError::UnknownLabel),
380        }
381    }
382}
383#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
384pub enum MidiEventKind {
385    ChannelVoice,
386    System,
387    Meta,
388    Sysex,
389    Ump,
390    Unknown,
391}
392
393impl MidiEventKind {
394    pub const ALL: &'static [Self] = &[
395        Self::ChannelVoice,
396        Self::System,
397        Self::Meta,
398        Self::Sysex,
399        Self::Ump,
400        Self::Unknown,
401    ];
402
403    pub const fn as_str(self) -> &'static str {
404        match self {
405            Self::ChannelVoice => "channel-voice",
406            Self::System => "system",
407            Self::Meta => "meta",
408            Self::Sysex => "sysex",
409            Self::Ump => "ump",
410            Self::Unknown => "unknown",
411        }
412    }
413}
414
415impl fmt::Display for MidiEventKind {
416    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
417        formatter.write_str(self.as_str())
418    }
419}
420
421impl FromStr for MidiEventKind {
422    type Err = MidiError;
423
424    fn from_str(value: &str) -> Result<Self, Self::Err> {
425        match normalized_label(value)?.as_str() {
426            "channel-voice" => Ok(Self::ChannelVoice),
427            "system" => Ok(Self::System),
428            "meta" => Ok(Self::Meta),
429            "sysex" => Ok(Self::Sysex),
430            "ump" => Ok(Self::Ump),
431            "unknown" => Ok(Self::Unknown),
432            _ => Err(MidiError::UnknownLabel),
433        }
434    }
435}
436#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
437pub enum MidiDeviceKind {
438    Keyboard,
439    Controller,
440    Synthesizer,
441    DrumMachine,
442    Sequencer,
443    Daw,
444    Interface,
445    Unknown,
446}
447
448impl MidiDeviceKind {
449    pub const ALL: &'static [Self] = &[
450        Self::Keyboard,
451        Self::Controller,
452        Self::Synthesizer,
453        Self::DrumMachine,
454        Self::Sequencer,
455        Self::Daw,
456        Self::Interface,
457        Self::Unknown,
458    ];
459
460    pub const fn as_str(self) -> &'static str {
461        match self {
462            Self::Keyboard => "keyboard",
463            Self::Controller => "controller",
464            Self::Synthesizer => "synthesizer",
465            Self::DrumMachine => "drum-machine",
466            Self::Sequencer => "sequencer",
467            Self::Daw => "daw",
468            Self::Interface => "interface",
469            Self::Unknown => "unknown",
470        }
471    }
472}
473
474impl fmt::Display for MidiDeviceKind {
475    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
476        formatter.write_str(self.as_str())
477    }
478}
479
480impl FromStr for MidiDeviceKind {
481    type Err = MidiError;
482
483    fn from_str(value: &str) -> Result<Self, Self::Err> {
484        match normalized_label(value)?.as_str() {
485            "keyboard" => Ok(Self::Keyboard),
486            "controller" => Ok(Self::Controller),
487            "synthesizer" => Ok(Self::Synthesizer),
488            "drum-machine" => Ok(Self::DrumMachine),
489            "sequencer" => Ok(Self::Sequencer),
490            "daw" => Ok(Self::Daw),
491            "interface" => Ok(Self::Interface),
492            "unknown" => Ok(Self::Unknown),
493            _ => Err(MidiError::UnknownLabel),
494        }
495    }
496}
497#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
498pub enum MidiProfileKind {
499    DefaultControlChange,
500    Mpe,
501    DrawbarOrgan,
502    OrchestralArticulation,
503    Custom,
504}
505
506impl MidiProfileKind {
507    pub const ALL: &'static [Self] = &[
508        Self::DefaultControlChange,
509        Self::Mpe,
510        Self::DrawbarOrgan,
511        Self::OrchestralArticulation,
512        Self::Custom,
513    ];
514
515    pub const fn as_str(self) -> &'static str {
516        match self {
517            Self::DefaultControlChange => "default-control-change",
518            Self::Mpe => "mpe",
519            Self::DrawbarOrgan => "drawbar-organ",
520            Self::OrchestralArticulation => "orchestral-articulation",
521            Self::Custom => "custom",
522        }
523    }
524}
525
526impl fmt::Display for MidiProfileKind {
527    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
528        formatter.write_str(self.as_str())
529    }
530}
531
532impl FromStr for MidiProfileKind {
533    type Err = MidiError;
534
535    fn from_str(value: &str) -> Result<Self, Self::Err> {
536        match normalized_label(value)?.as_str() {
537            "default-control-change" => Ok(Self::DefaultControlChange),
538            "mpe" => Ok(Self::Mpe),
539            "drawbar-organ" => Ok(Self::DrawbarOrgan),
540            "orchestral-articulation" => Ok(Self::OrchestralArticulation),
541            "custom" => Ok(Self::Custom),
542            _ => Err(MidiError::UnknownLabel),
543        }
544    }
545}
546#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
547pub enum MidiPropertyKind {
548    PerNoteExpression,
549    PropertyExchange,
550    ProfileConfiguration,
551    EndpointInfo,
552    Custom,
553}
554
555impl MidiPropertyKind {
556    pub const ALL: &'static [Self] = &[
557        Self::PerNoteExpression,
558        Self::PropertyExchange,
559        Self::ProfileConfiguration,
560        Self::EndpointInfo,
561        Self::Custom,
562    ];
563
564    pub const fn as_str(self) -> &'static str {
565        match self {
566            Self::PerNoteExpression => "per-note-expression",
567            Self::PropertyExchange => "property-exchange",
568            Self::ProfileConfiguration => "profile-configuration",
569            Self::EndpointInfo => "endpoint-info",
570            Self::Custom => "custom",
571        }
572    }
573}
574
575impl fmt::Display for MidiPropertyKind {
576    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
577        formatter.write_str(self.as_str())
578    }
579}
580
581impl FromStr for MidiPropertyKind {
582    type Err = MidiError;
583
584    fn from_str(value: &str) -> Result<Self, Self::Err> {
585        match normalized_label(value)?.as_str() {
586            "per-note-expression" => Ok(Self::PerNoteExpression),
587            "property-exchange" => Ok(Self::PropertyExchange),
588            "profile-configuration" => Ok(Self::ProfileConfiguration),
589            "endpoint-info" => Ok(Self::EndpointInfo),
590            "custom" => Ok(Self::Custom),
591            _ => Err(MidiError::UnknownLabel),
592        }
593    }
594}
595#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
596pub enum MidiUmpMessageKind {
597    Utility,
598    System,
599    Midi1ChannelVoice,
600    Data64,
601    Midi2ChannelVoice,
602    Data128,
603    FlexData,
604    Unknown,
605}
606
607impl MidiUmpMessageKind {
608    pub const ALL: &'static [Self] = &[
609        Self::Utility,
610        Self::System,
611        Self::Midi1ChannelVoice,
612        Self::Data64,
613        Self::Midi2ChannelVoice,
614        Self::Data128,
615        Self::FlexData,
616        Self::Unknown,
617    ];
618
619    pub const fn as_str(self) -> &'static str {
620        match self {
621            Self::Utility => "utility",
622            Self::System => "system",
623            Self::Midi1ChannelVoice => "midi-1-channel-voice",
624            Self::Data64 => "data-64",
625            Self::Midi2ChannelVoice => "midi-2-channel-voice",
626            Self::Data128 => "data-128",
627            Self::FlexData => "flex-data",
628            Self::Unknown => "unknown",
629        }
630    }
631}
632
633impl fmt::Display for MidiUmpMessageKind {
634    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
635        formatter.write_str(self.as_str())
636    }
637}
638
639impl FromStr for MidiUmpMessageKind {
640    type Err = MidiError;
641
642    fn from_str(value: &str) -> Result<Self, Self::Err> {
643        match normalized_label(value)?.as_str() {
644            "utility" => Ok(Self::Utility),
645            "system" => Ok(Self::System),
646            "midi-1-channel-voice" => Ok(Self::Midi1ChannelVoice),
647            "data-64" => Ok(Self::Data64),
648            "midi-2-channel-voice" => Ok(Self::Midi2ChannelVoice),
649            "data-128" => Ok(Self::Data128),
650            "flex-data" => Ok(Self::FlexData),
651            "unknown" => Ok(Self::Unknown),
652            _ => Err(MidiError::UnknownLabel),
653        }
654    }
655}
656
657#[derive(Clone, Copy, Debug, Eq, PartialEq)]
658pub enum MidiError {
659    Empty,
660    InvalidFormat,
661    OutOfRange,
662    NonFinite,
663    NonPositive,
664    UnknownLabel,
665}
666
667impl fmt::Display for MidiError {
668    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
669        match self {
670            Self::Empty => formatter.write_str("MIDI metadata text cannot be empty"),
671            Self::InvalidFormat => formatter.write_str("MIDI metadata has an invalid format"),
672            Self::OutOfRange => formatter.write_str("MIDI metadata value is out of range"),
673            Self::NonFinite => formatter.write_str("MIDI metadata value must be finite"),
674            Self::NonPositive => formatter.write_str("MIDI metadata value must be positive"),
675            Self::UnknownLabel => formatter.write_str("unknown MIDI metadata label"),
676        }
677    }
678}
679
680impl Error for MidiError {}
681
682#[allow(dead_code)]
683fn non_empty_text(value: impl AsRef<str>) -> Result<String, MidiError> {
684    let trimmed = value.as_ref().trim();
685    if trimmed.is_empty() {
686        Err(MidiError::Empty)
687    } else {
688        Ok(trimmed.to_string())
689    }
690}
691
692fn normalized_label(value: &str) -> Result<String, MidiError> {
693    let trimmed = value.trim();
694    if trimmed.is_empty() {
695        Err(MidiError::Empty)
696    } else {
697        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
698    }
699}
700#[cfg(test)]
701#[allow(
702    unused_imports,
703    clippy::unnecessary_wraps,
704    clippy::assertions_on_constants
705)]
706mod tests {
707    use super::{
708        MidiChannel, MidiControllerNumber, MidiDeviceKind, MidiError, MidiEventKind,
709        MidiMessageKind, MidiNoteNumber, MidiPortName, MidiProfileKind, MidiProgramNumber,
710        MidiPropertyKind, MidiUmpMessageKind, MidiVelocity, MidiVersion,
711    };
712    use core::{fmt, str::FromStr};
713
714    fn assert_enum_family<T>(variants: &[T]) -> Result<(), MidiError>
715    where
716        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = MidiError>,
717    {
718        for variant in variants {
719            let label = variant.to_string();
720            assert_eq!(label.parse::<T>()?, *variant);
721            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
722            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
723        }
724        Ok(())
725    }
726
727    #[test]
728    fn validates_text_newtypes() -> Result<(), MidiError> {
729        let value = MidiPortName::new(" example-value ")?;
730        assert_eq!(value.as_str(), "example-value");
731        assert_eq!(value.value(), "example-value");
732        assert_eq!(value.to_string(), "example-value");
733        assert_eq!(
734            <MidiPortName as TryFrom<&str>>::try_from("example-value")?,
735            value
736        );
737        Ok(())
738    }
739
740    #[test]
741    fn validates_numeric_newtypes() -> Result<(), MidiError> {
742        let value = MidiChannel::new(1)?;
743        assert_eq!(value.value(), 1);
744        assert_eq!("1".parse::<MidiChannel>()?, value);
745        assert_eq!(MidiChannel::new(17), Err(MidiError::OutOfRange));
746        let value = MidiNoteNumber::new(0)?;
747        assert_eq!(value.value(), 0);
748        assert_eq!("0".parse::<MidiNoteNumber>()?, value);
749        assert_eq!(MidiNoteNumber::new(128), Err(MidiError::OutOfRange));
750        let value = MidiVelocity::new(0)?;
751        assert_eq!(value.value(), 0);
752        assert_eq!("0".parse::<MidiVelocity>()?, value);
753        assert_eq!(MidiVelocity::new(128), Err(MidiError::OutOfRange));
754        let value = MidiControllerNumber::new(0)?;
755        assert_eq!(value.value(), 0);
756        assert_eq!("0".parse::<MidiControllerNumber>()?, value);
757        assert_eq!(MidiControllerNumber::new(128), Err(MidiError::OutOfRange));
758        let value = MidiProgramNumber::new(0)?;
759        assert_eq!(value.value(), 0);
760        assert_eq!("0".parse::<MidiProgramNumber>()?, value);
761        assert_eq!(MidiProgramNumber::new(128), Err(MidiError::OutOfRange));
762        Ok(())
763    }
764
765    #[test]
766    fn displays_and_parses_enums() -> Result<(), MidiError> {
767        assert_enum_family(MidiVersion::ALL)?;
768        assert_enum_family(MidiMessageKind::ALL)?;
769        assert_enum_family(MidiEventKind::ALL)?;
770        assert_enum_family(MidiDeviceKind::ALL)?;
771        assert_enum_family(MidiProfileKind::ALL)?;
772        assert_enum_family(MidiPropertyKind::ALL)?;
773        assert_enum_family(MidiUmpMessageKind::ALL)?;
774        Ok(())
775    }
776}