//! The three parts of BLE MIDI framing that are known to break in the field. //! //! A thirteen-bit millisecond counter turns over every eight seconds, running status may be sent //! by a device but must not be assumed to survive a packet boundary, and a dump larger than the //! MTU is spread over packets that carry no timestamps of their own. Each is a place where a //! plausible-looking parser produces plausible-looking nonsense, so each gets a test that would //! fail rather than merely producing different notes. //! //! Byte layouts follow the BLE-MIDI 1.0 specification (MMA/AMEI, 2015): a header byte //! `10hhhhhh` carrying the high six timestamp bits, then a timestamp byte `1lllllll` carrying the //! low seven before each message, so a timestamp in milliseconds is `high * 128 + low`. #![allow( clippy::expect_used, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used )] use midi_harbor_blemidi::codec::{ DecodeError, Decoder, EncodeError, Encoder, Event, MAX_PACKET, MIN_PACKET, TIMESTAMP_PERIOD, }; use midi_harbor_core::midi::{Channel, MidiMessage}; use midi_harbor_core::stream::SysExEnd; use proptest::prelude::*; /// What a decode produced, copied so system-exclusive runs outlive the packet they came from. #[derive(Clone, Debug, PartialEq, Eq)] enum Read { Message(u64, MidiMessage), SysEx(u64, Vec, SysExEnd), } /// Reads one packet, returning everything it carried and any complaint about how it ended. fn decode(decoder: &mut Decoder, packet: &[u8]) -> (Vec, Result<(), DecodeError>) { let mut out = Vec::new(); let result = decoder.decode(packet, &mut |event| match event { Event::Message { at, message } => out.push(Read::Message(at, message)), Event::SysEx { at, bytes, end } => out.push(Read::SysEx(at, bytes.to_vec(), end)), }); (out, result) } /// Encodes a run of timestamped messages into whole packets. fn encode(capacity: usize, messages: &[(u64, MidiMessage)]) -> Vec> { let mut encoder = Encoder::new(capacity); let mut packets = Vec::new(); for (at, message) in messages { encoder.push(*at, message, &mut |packet| packets.push(packet.to_vec())); } encoder.flush(&mut |packet| packets.push(packet.to_vec())); packets } /// Reads a whole run of packets through one decoder, as a link would. fn decode_all(packets: &[Vec]) -> Vec { let mut decoder = Decoder::new(); let mut out = Vec::new(); for packet in packets { let (read, result) = decode(&mut decoder, packet); assert_eq!( result, Ok(()), "packet {packet:02x?} came from the encoder, so it must decode" ); out.extend(read); } out } fn channel(index: u8) -> Channel { Channel::new(index % 16).expect("index is masked into range") } fn note_on(note: u8) -> MidiMessage { MidiMessage::NoteOn { channel: channel(0), note, velocity: 100, } } prop_compose! { /// Any message the encoder can carry, which is every channel message plus real time. fn any_message()( kind in 0_u8..8, channel_index in 0_u8..16, first in 0_u8..128, second in 0_u8..128, realtime in 0xF8_u8..=0xFF, ) -> MidiMessage { let channel = channel(channel_index); match kind { 0 => MidiMessage::NoteOff { channel, note: first, velocity: second }, 1 => MidiMessage::NoteOn { channel, note: first, velocity: second }, 2 => MidiMessage::PolyAftertouch { channel, note: first, pressure: second }, 3 => MidiMessage::ControlChange { channel, controller: first, value: second }, 4 => MidiMessage::ProgramChange { channel, program: first }, 5 => MidiMessage::ChannelAftertouch { channel, pressure: first }, 6 => MidiMessage::PitchBend { channel, value: (u16::from(second) << 7) | u16::from(first), }, _ => MidiMessage::System { status: realtime }, } } } proptest! { /// Whatever goes in comes out, in order, however the packets happen to divide, and every /// packet stands alone. /// /// Packets are separate attribute writes and any one can be lost, so the encoder never uses /// running status: byte 2 of every packet, after the header and the first timestamp, must be /// a status byte. A receiver that saw only that packet can then still read it. #[test] fn every_message_survives_the_round_trip( messages in prop::collection::vec(any_message(), 1..40), capacity in MIN_PACKET..=64_usize, ) { // Messages are spaced a millisecond apart so the run crosses packet boundaries on // timestamp changes as well as on capacity. let timed: Vec<_> = messages .iter() .enumerate() .map(|(step, message)| (step as u64, *message)) .collect(); let packets = encode(capacity, &timed); for packet in &packets { prop_assert!( packet.len() <= capacity, "packet {:02x?} is longer than the {} bytes the link can write", packet, capacity ); prop_assert!( packet[2] & 0x80 == 0x80, "packet {:02x?} opens on a data byte, relying on status from an earlier packet", packet ); } let read = decode_all(&packets); let recovered: Vec<_> = read .iter() .map(|entry| match entry { Read::Message(at, message) => (*at, *message), Read::SysEx(..) => panic!("no dump was sent, so none can be read"), }) .collect(); prop_assert_eq!(recovered, timed, "the decoder must return exactly what was encoded"); } /// Timing survives the thirteen-bit counter returning to zero, however far into the cycle a /// run starts. /// /// The counter holds `2^13 = 8192` milliseconds, so it turns over every 8.192 seconds. Four /// notes a second apart span three seconds, and whether the counter turns over part-way /// through depends entirely on where the run started. #[test] fn timestamps_stay_ordered_across_the_turnover(start in 0_u64..TIMESTAMP_PERIOD) { let timed: Vec<_> = (0..4) .map(|step| (start + step * 1000, note_on(60))) .collect(); let read = decode_all(&encode(MAX_PACKET, &timed)); let times: Vec = read .iter() .map(|entry| match entry { Read::Message(at, _) => *at, Read::SysEx(at, ..) => *at, }) .collect(); // The decoder counts from its own zero rather than the sender's, so what must hold is the // spacing, not the absolute value. prop_assert_eq!(times.len(), timed.len(), "every note sent must be read back"); for pair in times.windows(2) { prop_assert_eq!( pair[1] - pair[0], 1000, "a one-second gap was lost across the turnover: {:?}", times ); } } /// A dump of any size arrives byte-identical however many packets it took. /// /// Continuation packets carry a header and then data with no timestamp bytes, so a decoder /// that expected timestamps in them would eat every other byte of the dump. #[test] fn a_dump_split_across_packets_is_rejoined_byte_for_byte( body in prop::collection::vec(0_u8..128, 0..600), capacity in MIN_PACKET..=64_usize, ) { let mut payload = vec![0xF0]; payload.extend_from_slice(&body); payload.push(0xF7); let mut encoder = Encoder::new(capacity); let mut packets = Vec::new(); encoder .push_sysex(1234, &payload, &mut |packet| packets.push(packet.to_vec())) .expect("the payload is framed by F0 and F7, so the encoder must accept it"); encoder.flush(&mut |packet| packets.push(packet.to_vec())); prop_assert!( packets.iter().all(|packet| packet.len() <= capacity), "a dump packet is longer than the link can write" ); let mut rejoined = Vec::new(); let mut ended = None; for entry in decode_all(&packets) { match entry { Read::SysEx(_, bytes, end) => { rejoined.extend_from_slice(&bytes); if end != SysExEnd::Open { ended = Some(end); } } Read::Message(_, message) => panic!("a dump alone produced {message:?}"), } } prop_assert_eq!(ended, Some(SysExEnd::Complete), "the dump must be read as complete"); prop_assert_eq!(rejoined, payload, "the dump must be rejoined byte for byte"); } } /// Packets as devices write them decode as the BLE-MIDI 1.0 specification frames them, and the /// malformed ones are refused rather than guessed at. /// /// Running status is accepted inside a packet, because devices send it, but not across a packet /// boundary, because a lost packet between the two would attach the data to the wrong status. A /// real-time byte may interrupt a dump with its own timestamp byte before it, which is the shape a /// naive parser mistakes for the terminator. A dump interrupted by a channel message was /// abandoned by its sender and must say so, or a receiver acts on the fragment. Timestamps in the /// wants are `high * 128 + low` from the header and timestamp bytes: `0x82, 0xC0` is /// `2 * 128 + 64 = 320`, and a timestamp byte of `0xF0` is `0x70 = 112`. #[test] fn device_packets_decode_as_the_specification_frames_them() { struct Case { name: &'static str, packets: &'static [&'static [u8]], want: Vec, want_last: Result<(), DecodeError>, } let cases = [ Case { name: "running status inside one packet", packets: &[&[0x82, 0xC0, 0x90, 60, 100, 62, 100]], want: vec![ Read::Message(320, note_on(60)), Read::Message(320, note_on(62)), ], want_last: Ok(()), }, Case { name: "running status carried across a packet boundary", packets: &[&[0x80, 0x80, 0x90, 60, 100], &[0x80, 0x80, 62, 100]], want: vec![Read::Message(0, note_on(60))], want_last: Err(DecodeError::Orphaned(62)), }, Case { name: "a clock inside a dump", packets: &[&[ 0x80, 0x80, 0xF0, 0x43, 0x10, 0x81, 0xF8, 0x44, 0x45, 0x81, 0xF7, ]], want: vec![ Read::SysEx(0, vec![0xF0, 0x43, 0x10], SysExEnd::Open), Read::Message(1, MidiMessage::System { status: 0xF8 }), Read::SysEx(1, vec![0x44, 0x45], SysExEnd::Open), Read::SysEx(1, vec![0xF7], SysExEnd::Complete), ], want_last: Ok(()), }, Case { name: "a dump abandoned for a note", packets: &[&[0x80, 0x80, 0xF0, 0x43, 0x10, 0x81, 0x90, 60, 100]], want: vec![ Read::SysEx(0, vec![0xF0, 0x43, 0x10], SysExEnd::Abandoned), Read::Message(1, note_on(60)), ], want_last: Ok(()), }, Case { name: "a continuation whose timestamp byte reads as 0xF0", packets: &[&[0x80, 0x80, 0xF0, 0x43], &[0x80, 0xF0, 0xF7]], want: vec![ Read::SysEx(0, vec![0xF0, 0x43], SysExEnd::Open), Read::SysEx(112, vec![0xF7], SysExEnd::Complete), ], want_last: Ok(()), }, Case { name: "an empty write", packets: &[&[]], want: vec![], want_last: Err(DecodeError::Empty), }, Case { name: "a write opening on a data byte", packets: &[&[0x00, 0x90, 60, 100]], want: vec![], want_last: Err(DecodeError::NotAHeader(0x00)), }, Case { name: "a timestamp with no message after it", packets: &[&[0x80, 0x80]], want: vec![], want_last: Err(DecodeError::Truncated), }, ]; for case in cases { let mut decoder = Decoder::new(); let mut read = Vec::new(); let mut last = Ok(()); for packet in case.packets { let (events, result) = decode(&mut decoder, packet); read.extend(events); last = result; } assert_eq!( read, case.want, "{}: the decoder must read exactly these events", case.name ); assert_eq!( last, case.want_last, "{}: the last packet must end with this result", case.name ); } } /// The encoder sends a dump only when it is a whole message, framed by `F0` and `F7`. /// /// Half a dump on the wire is worse than none: the receiver waits for a terminator that is never /// coming and swallows everything after it. The smallest framed dump, `F0 F7`, is the accepted /// boundary, and goes out as header, timestamp, `F0`, timestamp, `F7`, with every timestamp zero. #[test] fn only_a_framed_dump_is_sent() { let cases = [ ( "the smallest framed dump", vec![0xF0, 0xF7], Ok(()), vec![vec![0x80, 0x80, 0xF0, 0x80, 0xF7]], ), ( "a dump with its terminator missing", vec![0xF0, 0x43, 0x10], Err(EncodeError::NotFramed), vec![], ), ( "a dump with its opening byte missing", vec![0x43, 0x10, 0xF7], Err(EncodeError::NotFramed), vec![], ), ( "an opening byte alone", vec![0xF0], Err(EncodeError::NotFramed), vec![], ), ]; for (name, payload, want, want_packets) in cases { let mut encoder = Encoder::new(MAX_PACKET); let mut packets = Vec::new(); let result = encoder.push_sysex(0, &payload, &mut |packet| packets.push(packet.to_vec())); encoder.flush(&mut |packet| packets.push(packet.to_vec())); assert_eq!(result, want, "{name}: the framing decides acceptance"); assert_eq!( packets, want_packets, "{name}: only an accepted dump may reach the wire" ); } }