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