//! MIDI 1.0 message semantics. //! //! Shared by routing, note silencing, the recovery journal and the message monitor, because all //! four need to know what a run of bytes means rather than just how long it is. /// Highest value a data byte may carry. Anything above has the status bit set. pub const MAX_DATA: u8 = 0x7F; /// Channels in MIDI 1.0. pub const CHANNELS: u8 = 16; /// Controller number for all-sound-off. pub const CC_ALL_SOUND_OFF: u8 = 120; /// Controller number for reset-all-controllers. pub const CC_RESET_ALL_CONTROLLERS: u8 = 121; /// Controller number for all-notes-off. pub const CC_ALL_NOTES_OFF: u8 = 123; /// Controller number for the sustain pedal. pub const CC_SUSTAIN: u8 = 64; /// A MIDI channel, zero-based internally and one-based when shown to people. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Channel(u8); impl Channel { /// Creates a channel from a zero-based number, returning `None` when out of range. pub fn new(zero_based: u8) -> Option { (zero_based < CHANNELS).then_some(Self(zero_based)) } /// Creates a channel from the low nibble of a status byte, which is always in range. pub fn from_status(status: u8) -> Self { Self(status & 0x0F) } /// Returns the zero-based number, as it appears on the wire. pub fn index(&self) -> u8 { self.0 } /// Returns the one-based number, as people refer to it. pub fn number(&self) -> u8 { self.0.saturating_add(1) } /// Returns every channel, for operations that sweep all of them. pub fn all() -> impl Iterator { (0..CHANNELS).map(Self) } } /// A parsed MIDI 1.0 message. /// /// System-exclusive payloads are deliberately absent: they are unbounded and must not be copied /// around on the data path, so they travel as handles into a pre-allocated pool instead. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MidiMessage { /// A key was released. NoteOff { /// Which channel. channel: Channel, /// Which key. note: u8, /// How fast it was released. velocity: u8, }, /// A key was pressed. A velocity of zero means the same as note off. NoteOn { /// Which channel. channel: Channel, /// Which key. note: u8, /// How hard it was pressed. velocity: u8, }, /// Pressure applied to one held key. PolyAftertouch { /// Which channel. channel: Channel, /// Which key. note: u8, /// How much pressure. pressure: u8, }, /// A controller moved. ControlChange { /// Which channel. channel: Channel, /// Which controller. controller: u8, /// Its new value. value: u8, }, /// The sound selection changed. ProgramChange { /// Which channel. channel: Channel, /// Which program. program: u8, }, /// Pressure applied to the whole channel. ChannelAftertouch { /// Which channel. channel: Channel, /// How much pressure. pressure: u8, }, /// The pitch wheel moved, centred at 8192. PitchBend { /// Which channel. channel: Channel, /// The fourteen-bit position. value: u16, }, /// A system common message that carries data: a time code quarter frame, a song position /// or a song select. SystemCommon { /// The status byte, `0xF1` to `0xF3`. status: u8, /// The data bytes, of which a quarter frame and a song select use only the first. data: [u8; 2], }, /// A system message of one byte, which is every real-time message and a tune request. System { /// The status byte. status: u8, }, } impl MidiMessage { /// Returns the channel this message applies to, or `None` for system messages. pub fn channel(&self) -> Option { match self { Self::NoteOff { channel, .. } | Self::NoteOn { channel, .. } | Self::PolyAftertouch { channel, .. } | Self::ControlChange { channel, .. } | Self::ProgramChange { channel, .. } | Self::ChannelAftertouch { channel, .. } | Self::PitchBend { channel, .. } => Some(*channel), Self::SystemCommon { .. } | Self::System { .. } => None, } } /// Reports whether this message starts a note sounding. /// /// A note on with zero velocity is a note off by convention, and treating it as a start is /// the classic way to leave a note hanging forever. pub fn starts_note(&self) -> bool { matches!(self, Self::NoteOn { velocity, .. } if *velocity > 0) } /// Reports whether this message stops a note sounding. pub fn stops_note(&self) -> Option<(Channel, u8)> { match self { Self::NoteOff { channel, note, .. } => Some((*channel, *note)), Self::NoteOn { channel, note, velocity, } if *velocity == 0 => Some((*channel, *note)), _ => None, } } /// Returns the status byte for this message. pub fn status(&self) -> u8 { match self { Self::NoteOff { channel, .. } => 0x80 | channel.index(), Self::NoteOn { channel, .. } => 0x90 | channel.index(), Self::PolyAftertouch { channel, .. } => 0xA0 | channel.index(), Self::ControlChange { channel, .. } => 0xB0 | channel.index(), Self::ProgramChange { channel, .. } => 0xC0 | channel.index(), Self::ChannelAftertouch { channel, .. } => 0xD0 | channel.index(), Self::PitchBend { channel, .. } => 0xE0 | channel.index(), Self::SystemCommon { status, .. } | Self::System { status } => *status, } } /// Returns how many bytes this message occupies, including its status byte. pub fn len(&self) -> usize { match self { Self::ProgramChange { .. } | Self::ChannelAftertouch { .. } => 2, Self::SystemCommon { status, .. } | Self::System { status } => { system_message_len(*status) } _ => 3, } } /// Reports whether this message carries no bytes, which cannot happen. pub fn is_empty(&self) -> bool { false } /// Writes this message into `out`, returning how many bytes were written. /// /// Writes nothing and returns zero when `out` is too small, so a caller on the data path /// never has to handle a panic or a reallocation. pub fn encode(&self, out: &mut [u8]) -> usize { let needed = self.len(); if out.len() < needed { return 0; } let bytes: [u8; 3] = match self { Self::NoteOff { note, velocity, .. } => [self.status(), *note, *velocity], Self::NoteOn { note, velocity, .. } => [self.status(), *note, *velocity], Self::PolyAftertouch { note, pressure, .. } => [self.status(), *note, *pressure], Self::ControlChange { controller, value, .. } => [self.status(), *controller, *value], Self::ProgramChange { program, .. } => [self.status(), *program, 0], Self::ChannelAftertouch { pressure, .. } => [self.status(), *pressure, 0], Self::PitchBend { value, .. } => { // Fourteen bits split across two seven-bit bytes, least significant first. [ self.status(), (*value & 0x7F) as u8, ((*value >> 7) & 0x7F) as u8, ] } Self::SystemCommon { status, data: [first, second], } => [*status, *first, *second], Self::System { status } => [*status, 0, 0], }; for (slot, byte) in out.iter_mut().zip(bytes.iter()).take(needed) { *slot = *byte; } needed } /// Parses one message from `bytes`, returning it and how many bytes it consumed. /// /// `running_status` supplies the status byte when the message omits it, which is legal inside /// an RTP-MIDI list and common from hardware. pub fn parse(bytes: &[u8], running_status: Option) -> Option<(Self, usize)> { let first = bytes.first().copied()?; // Resolve the status byte, which may be carried over from the previous message. let (status, payload_offset) = if first >= 0x80 { (first, 1) } else { (running_status?, 0) }; // A byte with the high bit set is never data. Finding one here means a status arrived // before this message was complete, and forwarding it as data would put a status byte in // the middle of the next device's stream. let len = if status >= 0xF0 { system_message_len(status) } else { channel_message_len(status) }; let data = bytes.get(payload_offset..)?; let needed = data.get(..len.saturating_sub(1))?; if needed.iter().any(|byte| *byte >= 0x80) { return None; } // System messages carry no channel. if status >= 0xF0 { let message = if len > 1 { Self::SystemCommon { status, data: [ needed.first().copied().unwrap_or(0), needed.get(1).copied().unwrap_or(0), ], } } else { Self::System { status } }; return Some((message, payload_offset + len.saturating_sub(1))); } let channel = Channel::from_status(status); let first_data = data.first().copied()?; let message = match status & 0xF0 { 0x80 => { let velocity = data.get(1).copied()?; Self::NoteOff { channel, note: first_data, velocity, } } 0x90 => { let velocity = data.get(1).copied()?; Self::NoteOn { channel, note: first_data, velocity, } } 0xA0 => { let pressure = data.get(1).copied()?; Self::PolyAftertouch { channel, note: first_data, pressure, } } 0xB0 => { let value = data.get(1).copied()?; Self::ControlChange { channel, controller: first_data, value, } } 0xC0 => Self::ProgramChange { channel, program: first_data, }, 0xD0 => Self::ChannelAftertouch { channel, pressure: first_data, }, 0xE0 => { let msb = data.get(1).copied()?; Self::PitchBend { channel, value: u16::from(first_data) | (u16::from(msb) << 7), } } _ => return None, }; let consumed = payload_offset + message.len().saturating_sub(1); Some((message, consumed)) } } /// Returns how many bytes a channel message occupies, including its status byte. fn channel_message_len(status: u8) -> usize { match status & 0xF0 { 0xC0 | 0xD0 => 2, _ => 3, } } /// Returns how many bytes a system message occupies. /// /// System-exclusive is unbounded, so it reports its status byte alone and the caller scans for /// the terminator itself. fn system_message_len(status: u8) -> usize { match status { // Time code quarter frame and song select carry one data byte. 0xF1 | 0xF3 => 2, // Song position pointer carries two. 0xF2 => 3, _ => 1, } } /// Returns a note number's key and octave, as Apple's MIDI tools write it: middle C, note 60, is C3. /// /// Every place a note is shown uses this, so the monitor and the window's note picker name a note /// alike. pub fn note_name(note: u8) -> String { /// Key names, indexed by semitone within an octave. const NAMES: [&str; 12] = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", ]; let key = NAMES.get(usize::from(note % 12)).copied().unwrap_or("?"); // Twelve semitones per octave, and note 0 sits two octaves below octave 0. #[allow(clippy::integer_division)] let octave = i16::from(note / 12) - 2; format!("{key}{octave}") } /// Builds the messages that silence a channel completely. /// /// Sent on every link teardown and recovery. All-notes-off alone is not enough: a held sustain /// pedal keeps notes sounding through it. This sends the pedal release first, then the two broad /// resets. A synth that ignores all-notes-off needs a note off for each note, which only a caller /// that knows the notes can send; `Sounding::silence` does. pub fn silence_channel(channel: Channel) -> [MidiMessage; 3] { [ MidiMessage::ControlChange { channel, controller: CC_SUSTAIN, value: 0, }, MidiMessage::ControlChange { channel, controller: CC_ALL_NOTES_OFF, value: 0, }, MidiMessage::ControlChange { channel, controller: CC_ALL_SOUND_OFF, value: 0, }, ] } #[cfg(test)] mod tests { use super::*; /// Locks Apple's octave numbering, which the monitor and the note picker both show. /// /// Audio MIDI Setup and Logic write middle C, note 60, as C3; other tools write it C4, so the /// convention is a choice that has to stay put. Note 0 is C-2 and note 127 is G8, the ends of /// MIDI's range: 127 is ten octaves and seven semitones above 0. #[test] fn a_note_is_named_with_middle_c_as_c3() { for (note, want) in [(0, "C-2"), (60, "C3"), (61, "C#3"), (69, "A3"), (127, "G8")] { assert_eq!(note_name(note), want, "note {note} must read {want}"); } } fn channel(index: u8) -> Channel { Channel::new(index).expect("channel in range") } /// Every kind of message encodes to the bytes the MIDI 1.0 specification gives it and parses /// back from them. /// /// The status byte is the kind in the high nibble and the zero-based channel in the low one. /// Program change and channel pressure carry one data byte, quarter frame and song select one, /// song position two, and real-time messages none. Pitch bend is fourteen bits sent least /// significant seven first, so the centre, 8192 = 0x40 << 7 | 0x00, is `E1 00 40`. #[test] fn messages_encode_to_the_bytes_the_specification_gives_and_parse_back() { let cases = [ ( "note off", MidiMessage::NoteOff { channel: channel(0), note: 60, velocity: 64, }, &[0x80, 60, 64][..], ), ( "note on, channel 16", MidiMessage::NoteOn { channel: channel(15), note: 127, velocity: 100, }, &[0x9F, 127, 100][..], ), ( "poly pressure", MidiMessage::PolyAftertouch { channel: channel(3), note: 40, pressure: 90, }, &[0xA3, 40, 90][..], ), ( "control change", MidiMessage::ControlChange { channel: channel(7), controller: 74, value: 12, }, &[0xB7, 74, 12][..], ), ( "program change", MidiMessage::ProgramChange { channel: channel(2), program: 5, }, &[0xC2, 5][..], ), ( "channel pressure", MidiMessage::ChannelAftertouch { channel: channel(9), pressure: 77, }, &[0xD9, 77][..], ), ( "pitch bend centred", MidiMessage::PitchBend { channel: channel(1), value: 8192, }, &[0xE1, 0x00, 0x40][..], ), ( "time code quarter frame", MidiMessage::SystemCommon { status: 0xF1, data: [0x35, 0], }, &[0xF1, 0x35][..], ), ( "song position", MidiMessage::SystemCommon { status: 0xF2, data: [0x10, 0x20], }, &[0xF2, 0x10, 0x20][..], ), ( "song select", MidiMessage::SystemCommon { status: 0xF3, data: [0x07, 0], }, &[0xF3, 0x07][..], ), ( "timing clock", MidiMessage::System { status: 0xF8 }, &[0xF8][..], ), ]; for (case, message, bytes) in cases { let mut buffer = [0u8; 3]; let written = message.encode(&mut buffer); assert_eq!( &buffer[..written], bytes, "{case}: the encoding must be the specification's bytes" ); assert_eq!( MidiMessage::parse(bytes, None), Some((message, bytes.len())), "{case}: the specification's bytes must parse back to the message, consumed whole" ); } } proptest::proptest! { /// Whatever parses from a run of bytes encodes back to exactly the bytes it consumed. /// /// A message the daemon would read one way and send another is a peer misreading us. #[test] fn what_is_parsed_encodes_back_to_the_bytes_it_came_from( bytes in proptest::collection::vec(proptest::num::u8::ANY, 1..4), ) { proptest::prop_assume!(bytes.first().is_some_and(|status| *status >= 0x80)); if let Some((message, consumed)) = MidiMessage::parse(&bytes, None) { let mut buffer = [0u8; 3]; let written = message.encode(&mut buffer); proptest::prop_assert_eq!(buffer.get(..written), bytes.get(..consumed)); } } } /// Running status supplies a missing status byte, and bytes that do not make a whole message /// are refused rather than completed by guessing. /// /// Running status is legal inside an RTP-MIDI list and common from hardware, so data bytes /// with a status carried over are a message. With nothing carried over, or with the message /// cut short, nothing establishes what the bytes mean. #[test] fn running_status_completes_a_message_and_nothing_else_does() { let note_on = MidiMessage::NoteOn { channel: channel(0), note: 62, velocity: 100, }; let cases = [ ( "data bytes under running status", &[62, 100][..], Some(0x90), Some((note_on, 2)), ), ( "data bytes with no running status", &[62, 100][..], None, None, ), ( "a note on missing its velocity", &[0x90, 60][..], None, None, ), ("a status byte alone", &[0x90][..], None, None), ("nothing at all", &[][..], Some(0x90), None), ]; for (case, bytes, running, want) in cases { assert_eq!( MidiMessage::parse(bytes, running), want, "{case}: only a whole message may be parsed" ); } } /// A byte with the high bit set is never read as data, wherever it falls in a message. /// /// MIDI 1.0 reserves the high bit for status bytes. One arriving before a message is complete /// abandons it; forwarding it as data would put a status byte in the middle of the next /// device's stream, which that device reads as the start of something else. #[test] fn a_status_byte_is_never_read_as_data() { for status in (0x80..=0xE0).step_by(0x10).chain([0xF1, 0xF2, 0xF3]) { let len = if status >= 0xF0 { system_message_len(status) } else { channel_message_len(status) }; for intruder in 0x80..=0xFF { for position in 1..len { let mut bytes = [status, 0x00, 0x00]; bytes[position] = intruder; assert_eq!( MidiMessage::parse(&bytes, None), None, "{bytes:02X?}: a status byte inside a message must refuse it" ); } } } } /// A note on with zero velocity stops a note rather than starting one. /// /// The MIDI 1.0 specification defines it as a note off, and senders use it to keep running /// status across a run of notes. Treating it as a start is the classic way to leave a note /// hanging forever. #[test] fn a_note_on_with_zero_velocity_stops_a_note() { let cases = [ ("velocity zero", 0, false, Some((channel(0), 60))), ("velocity one", 1, true, None), ]; for (case, velocity, starts, stops) in cases { let message = MidiMessage::NoteOn { channel: channel(0), note: 60, velocity, }; assert_eq!( (message.starts_note(), message.stops_note()), (starts, stops), "{case}: only a note on with velocity starts a note" ); } } }