Skip to main content

sim_lib_midi_core/
wire.rs

1//! Canonical MIDI channel-message wire bytes.
2//!
3//! The status-nibble + data-byte layout of a [`ChannelMessage`] is fixed by the
4//! MIDI spec; the SMF writer and the wasm frame model each hand-rolled the same
5//! `match`. This is the one home for it (OVERLAP6.14).
6
7use crate::{Channel, ChannelMessage, MidiError, U7, U14};
8
9/// Encode a channel message to its `(status, data)` wire bytes.
10///
11/// Pitch-bend's two data bytes are masked to 7 bits each, per the MIDI spec.
12pub fn encode_channel(message: &ChannelMessage) -> (u8, Vec<u8>) {
13    match *message {
14        ChannelMessage::NoteOff { ch, key, vel } => (0x80 | ch.0, vec![key.0, vel.0]),
15        ChannelMessage::NoteOn { ch, key, vel } => (0x90 | ch.0, vec![key.0, vel.0]),
16        ChannelMessage::PolyAftertouch { ch, key, pressure } => {
17            (0xa0 | ch.0, vec![key.0, pressure.0])
18        }
19        ChannelMessage::ControlChange { ch, cc, value } => (0xb0 | ch.0, vec![cc.0, value.0]),
20        ChannelMessage::ProgramChange { ch, program } => (0xc0 | ch.0, vec![program.0]),
21        ChannelMessage::ChanAftertouch { ch, pressure } => (0xd0 | ch.0, vec![pressure.0]),
22        ChannelMessage::PitchBend { ch, value } => (
23            0xe0 | ch.0,
24            vec![(value.0 & 0x7f) as u8, ((value.0 >> 7) & 0x7f) as u8],
25        ),
26    }
27}
28
29/// Decode a channel message from its `(status, data)` wire bytes: the inverse
30/// of [`encode_channel`].
31///
32/// The channel is taken from the low nibble of `status` and the message type
33/// from the high nibble. Each consumed data byte must be a valid 7-bit value
34/// ([`MidiError::InvalidU7`]); a status whose high nibble is not a channel-voice
35/// type yields [`MidiError::NotChannelStatus`], and a data slice shorter than
36/// the message requires yields [`MidiError::TruncatedChannel`]. Pitch-bend's
37/// two 7-bit data bytes are recombined into the 14-bit value.
38pub fn decode_channel(status: u8, data: &[u8]) -> Result<ChannelMessage, MidiError> {
39    let ch = Channel::new(status & 0x0f)?;
40    let message = match status & 0xf0 {
41        0x80 => ChannelMessage::NoteOff {
42            ch,
43            key: data_u7(data, 0)?,
44            vel: data_u7(data, 1)?,
45        },
46        0x90 => ChannelMessage::NoteOn {
47            ch,
48            key: data_u7(data, 0)?,
49            vel: data_u7(data, 1)?,
50        },
51        0xa0 => ChannelMessage::PolyAftertouch {
52            ch,
53            key: data_u7(data, 0)?,
54            pressure: data_u7(data, 1)?,
55        },
56        0xb0 => ChannelMessage::ControlChange {
57            ch,
58            cc: data_u7(data, 0)?,
59            value: data_u7(data, 1)?,
60        },
61        0xc0 => ChannelMessage::ProgramChange {
62            ch,
63            program: data_u7(data, 0)?,
64        },
65        0xd0 => ChannelMessage::ChanAftertouch {
66            ch,
67            pressure: data_u7(data, 0)?,
68        },
69        0xe0 => {
70            let value = u16::from(data_u7(data, 0)?.0) | (u16::from(data_u7(data, 1)?.0) << 7);
71            ChannelMessage::PitchBend {
72                ch,
73                value: U14::try_from(value)?,
74            }
75        }
76        _ => return Err(MidiError::NotChannelStatus(status)),
77    };
78    Ok(message)
79}
80
81/// Read the 7-bit data byte at `index`, failing closed on a short slice or an
82/// out-of-range (`> 127`) byte.
83fn data_u7(data: &[u8], index: usize) -> Result<U7, MidiError> {
84    let byte = *data.get(index).ok_or(MidiError::TruncatedChannel)?;
85    U7::try_from(u16::from(byte))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn ch(value: u8) -> Channel {
93        Channel::new(value).expect("channel in range")
94    }
95
96    /// One representative of every [`ChannelMessage`] variant, on assorted
97    /// channels and with boundary data values.
98    fn every_variant() -> Vec<ChannelMessage> {
99        vec![
100            ChannelMessage::NoteOff {
101                ch: ch(0),
102                key: U7(60),
103                vel: U7(0),
104            },
105            ChannelMessage::NoteOn {
106                ch: ch(15),
107                key: U7(127),
108                vel: U7(100),
109            },
110            ChannelMessage::PolyAftertouch {
111                ch: ch(3),
112                key: U7(64),
113                pressure: U7(90),
114            },
115            ChannelMessage::ControlChange {
116                ch: ch(9),
117                cc: U7(7),
118                value: U7(127),
119            },
120            ChannelMessage::ProgramChange {
121                ch: ch(1),
122                program: U7(42),
123            },
124            ChannelMessage::ChanAftertouch {
125                ch: ch(12),
126                pressure: U7(55),
127            },
128            ChannelMessage::PitchBend {
129                ch: ch(0),
130                value: U14(0),
131            },
132            ChannelMessage::PitchBend {
133                ch: ch(7),
134                value: U14(8192),
135            },
136            ChannelMessage::PitchBend {
137                ch: ch(15),
138                value: U14(16_383),
139            },
140        ]
141    }
142
143    #[test]
144    fn encode_then_decode_round_trips_every_variant() {
145        for message in every_variant() {
146            let (status, data) = encode_channel(&message);
147            let decoded = decode_channel(status, &data).expect("decode");
148            assert_eq!(decoded, message, "round trip for {message:?}");
149        }
150    }
151
152    #[test]
153    fn non_channel_status_is_rejected() {
154        assert_eq!(
155            decode_channel(0xf0, &[0x00]),
156            Err(MidiError::NotChannelStatus(0xf0))
157        );
158    }
159
160    #[test]
161    fn truncated_data_is_rejected() {
162        // NoteOn needs two data bytes.
163        assert_eq!(
164            decode_channel(0x90, &[60]),
165            Err(MidiError::TruncatedChannel)
166        );
167        // ProgramChange needs one.
168        assert_eq!(decode_channel(0xc0, &[]), Err(MidiError::TruncatedChannel));
169    }
170
171    #[test]
172    fn data_byte_above_7_bits_is_rejected() {
173        assert_eq!(
174            decode_channel(0x90, &[200, 0]),
175            Err(MidiError::InvalidU7(200))
176        );
177    }
178}