midi-harbor/crates/blemidi/src/codec.rs
2026-09-28 13:52:47 -05:00

563 lines
21 KiB
Rust

//! The Bluetooth LE MIDI packet format.
//!
//! BLE carries MIDI in short attribute writes rather than a byte stream, so the framing is its
//! own. Every packet opens with a header byte, each message inside carries a thirteen-bit
//! millisecond timestamp, and a system-exclusive dump is spread across as many packets as it
//! takes. None of that survives being treated as plain MIDI bytes: a timestamp byte and a status
//! byte are both `1xxxxxxx`, so a parser that does not track position turns timing information
//! into notes.
//!
//! The encoder is deliberately stricter than the decoder. It never uses running status, because
//! the byte it saves is worth less than being understood by every receiver, while the decoder
//! accepts running status because devices in the field send it.
//!
//! Nothing here allocates, locks, or can panic, because both directions run on the data path.
use midi_harbor_core::midi::MidiMessage;
use midi_harbor_core::stream::SysExEnd;
use uuid::Uuid;
/// The GATT service every BLE MIDI device carries.
pub const SERVICE_UUID: Uuid = Uuid::from_u128(0x03B8_0E5A_EDE8_4B33_A751_6CE3_4EC4_C700);
/// The characteristic MIDI travels over, in both directions.
pub const CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x7772_E5DB_3868_4112_A1A9_F266_9D10_6BF3);
/// How many milliseconds the thirteen-bit timestamp counts before returning to zero.
pub const TIMESTAMP_PERIOD: u64 = 8192;
/// The largest packet this encoder will build.
///
/// An attribute write cannot exceed the negotiated MTU, and no BLE link negotiates beyond this.
pub const MAX_PACKET: usize = 512;
/// The smallest packet worth building: a header, a timestamp and a three-byte message.
pub const MIN_PACKET: usize = 5;
/// Why a packet could not be read.
///
/// Peripheral input is hostile input, so every one of these is a refusal rather than a guess.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum DecodeError {
/// The write carried no bytes at all.
#[error("packet carried no header byte")]
Empty,
/// The first byte was a data byte, so the packet is not BLE MIDI framing.
#[error("packet opened with {0:#04x} rather than a header byte")]
NotAHeader(u8),
/// The packet stopped part-way through a message.
#[error("packet ended part-way through a message")]
Truncated,
/// A data byte arrived with no status for it to inherit.
#[error("data byte {0:#04x} arrived with no status to inherit")]
Orphaned(u8),
}
/// Why a system-exclusive message could not be packed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum EncodeError {
/// The payload was not a whole system-exclusive message.
#[error("payload is not framed by {:#04x} and {:#04x}", SYSEX_START, SYSEX_END)]
NotFramed,
}
/// The byte that opens a system-exclusive message.
const SYSEX_START: u8 = 0xF0;
/// The byte that closes a system-exclusive message.
const SYSEX_END: u8 = 0xF7;
/// The lowest status byte that is a real-time message.
const FIRST_REALTIME: u8 = 0xF8;
/// One thing read out of a packet, with the time the sender stamped on it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Event<'a> {
/// A complete channel, system common or real-time message.
Message {
/// Milliseconds since this decoder started, with the counter's turnover already undone.
at: u64,
/// The message.
message: MidiMessage,
},
/// Part or all of a system-exclusive message.
SysEx {
/// Milliseconds since this decoder started, with the counter's turnover already undone.
at: u64,
/// The bytes, including `0xF0` on the first run and `0xF7` on the last.
bytes: &'a [u8],
/// Whether more is to come.
end: SysExEnd,
},
}
/// Reads packets from one peripheral, carrying the state that spans them.
///
/// One decoder belongs to one link. The timestamp counter's turnover and an unfinished dump both
/// continue across packets, so a decoder shared between two devices would splice one device's
/// dump onto the other's and read both clocks as one.
#[derive(Debug, Default)]
pub struct Decoder {
/// The last thirteen-bit timestamp seen, which is how the counter's turnover is noticed.
last_ticks: Option<u16>,
/// Milliseconds already accounted for by earlier turnovers.
epoch: u64,
/// Whether a dump begun in an earlier packet is still open.
in_sysex: bool,
}
impl Decoder {
/// Creates a decoder with no packet history.
pub fn new() -> Self {
Self::default()
}
/// Reads one packet, handing each message to `emit` in the order it appeared.
///
/// Emits through a closure rather than returning a collection because this runs on the data
/// path, where a returned `Vec` would mean allocating per notification. Whatever was read
/// before a malformed byte has already been emitted when the error is returned, because a
/// packet that goes wrong half-way through still delivered the notes in its first half.
pub fn decode<'a>(
&mut self,
packet: &'a [u8],
emit: &mut impl FnMut(Event<'a>),
) -> Result<(), DecodeError> {
// Read the header, which carries the high six bits every timestamp in this packet shares.
let Some(header) = packet.first().copied() else {
return Err(DecodeError::Empty);
};
if header < 0x80 {
return Err(DecodeError::NotAHeader(header));
}
let mut clock = PacketClock {
high: header & 0x3F,
low: None,
};
let mut running: Option<u8> = None;
let mut index = 1;
while index < packet.len() {
// A dump carries on until something ends it, whether it began in this packet, in an
// earlier one, or before the real-time message that just interrupted it. Its payload
// bytes carry no timestamps, so they are read here rather than below.
if self.in_sysex {
let resume = self.sysex_run(packet, index, false, self.now(), &mut clock, emit);
if resume > index {
index = resume;
continue;
}
if self.in_sysex {
// The packet ended on a timestamp byte with nothing behind it.
return Err(DecodeError::Truncated);
}
// The dump was abandoned, and the byte it stopped on times what follows.
}
let Some(byte) = packet.get(index).copied() else {
return Ok(());
};
// A byte with the high bit set here is a timestamp, not a status: the status byte it
// introduces is the one after it.
let at = if byte >= 0x80 {
index = index.saturating_add(1);
let ticks = clock.tick(byte);
self.advance(ticks)
} else {
self.now()
};
let Some(status) = packet.get(index).copied() else {
return Err(DecodeError::Truncated);
};
// Begin a dump, which may well outlast this packet. The loop above reads it, because
// what follows the opening byte is the same continuation it already handles.
if status == SYSEX_START {
// System common clears running status, and this is the loudest case of it.
running = None;
self.in_sysex = true;
let resume = self.sysex_run(packet, index, true, at, &mut clock, emit);
if resume <= index {
return Ok(());
}
index = resume;
continue;
}
// Real-time bytes belong to no other message, so they never touch running status.
if status >= FIRST_REALTIME {
emit(Event::Message {
at,
message: MidiMessage::System { status },
});
index = index.saturating_add(1);
continue;
}
// A terminator with nothing open frames nothing, so passing it on would only confuse
// a receiver.
if status == SYSEX_END {
index = index.saturating_add(1);
continue;
}
let Some(rest) = packet.get(index..) else {
return Ok(());
};
let Some((message, consumed)) = MidiMessage::parse(rest, running) else {
return Err(if status < 0x80 && running.is_none() {
DecodeError::Orphaned(status)
} else {
DecodeError::Truncated
});
};
if status >= 0x80 {
running = (message.status() < 0xF0).then_some(message.status());
}
emit(Event::Message { at, message });
if consumed == 0 {
return Ok(());
}
index = index.saturating_add(consumed);
}
Ok(())
}
/// Emits one run of system-exclusive bytes, returning where parsing resumes.
///
/// `start` points either at the `0xF0` that opens a dump, which `opens` says, or at the first
/// byte of one already in progress. The caller says which because the byte cannot: a
/// continuation may begin with a timestamp, and a timestamp of 112 is `0xF0`. Unlike a plain
/// MIDI stream, the terminator does not follow the payload
/// directly — a timestamp byte sits between them — so it is emitted as a run of its own to
/// keep the framing bytes travelling with the message.
fn sysex_run<'a>(
&mut self,
packet: &'a [u8],
start: usize,
opens: bool,
at: u64,
clock: &mut PacketClock,
emit: &mut impl FnMut(Event<'a>),
) -> usize {
// Find where the payload stops, which is the first byte carrying a high bit.
let mut cursor = if opens {
start.saturating_add(1)
} else {
start
};
while packet.get(cursor).is_some_and(|byte| *byte < 0x80) {
cursor = cursor.saturating_add(1);
}
// Nothing follows, which for a dump of any size is the ordinary case rather than a fault.
if cursor >= packet.len() {
emit_run(packet, start, cursor, at, SysExEnd::Open, emit);
return cursor;
}
// The byte is a timestamp; the one after it says what interrupted the payload.
let terminator = cursor.saturating_add(1);
match packet.get(terminator).copied() {
Some(SYSEX_END) => {
emit_run(packet, start, cursor, at, SysExEnd::Open, emit);
let ends_at = self.stamp(packet, cursor, clock, at);
emit_run(
packet,
terminator,
terminator.saturating_add(1),
ends_at,
SysExEnd::Complete,
emit,
);
self.in_sysex = false;
terminator.saturating_add(1)
}
// A real-time message may interrupt a dump without ending it.
Some(status) if status >= FIRST_REALTIME => {
emit_run(packet, start, cursor, at, SysExEnd::Open, emit);
let sent_at = self.stamp(packet, cursor, clock, at);
emit(Event::Message {
at: sent_at,
message: MidiMessage::System { status },
});
terminator.saturating_add(1)
}
// Anything else means the sender moved on and this dump will never be completed.
Some(_) => {
emit_run(packet, start, cursor, at, SysExEnd::Abandoned, emit);
self.in_sysex = false;
cursor
}
// The packet ended on a dangling timestamp byte. Resuming on it lets the caller
// report the truncation rather than inventing a message to blame it on.
None => {
emit_run(packet, start, cursor, at, SysExEnd::Open, emit);
cursor
}
}
}
/// Advances the clock over the timestamp byte at `index`, keeping `fallback` if it is not one.
fn stamp(
&mut self,
packet: &[u8],
index: usize,
clock: &mut PacketClock,
fallback: u64,
) -> u64 {
match packet.get(index).copied() {
Some(byte) if byte >= 0x80 => {
let ticks = clock.tick(byte);
self.advance(ticks)
}
_ => fallback,
}
}
/// Turns a thirteen-bit packet timestamp into a millisecond count that only moves forward.
///
/// The counter returns to zero every eight seconds, so a timestamp lower than the one before
/// it is a turnover rather than time running backwards.
fn advance(&mut self, ticks: u16) -> u64 {
if self.last_ticks.is_some_and(|last| ticks < last) {
self.epoch = self.epoch.saturating_add(TIMESTAMP_PERIOD);
}
self.last_ticks = Some(ticks);
self.epoch.saturating_add(u64::from(ticks))
}
/// The time the last timestamp named, for bytes that carry none of their own.
fn now(&self) -> u64 {
self.epoch
.saturating_add(u64::from(self.last_ticks.unwrap_or(0)))
}
}
/// The timestamp bits one packet carries: six opened by its header, seven per message.
///
/// The low seven bits count to 128 milliseconds, which a single packet can outlast, so the high
/// bits carry rather than staying fixed for the whole packet.
struct PacketClock {
/// The high six bits, as opened by the header and carried since.
high: u8,
/// The low seven bits most recently read, which is how a carry is noticed.
low: Option<u8>,
}
impl PacketClock {
/// Reads one timestamp byte, returning the thirteen-bit value it completes.
fn tick(&mut self, byte: u8) -> u16 {
let next = byte & 0x7F;
if self.low.is_some_and(|previous| next < previous) {
self.high = self.high.wrapping_add(1) & 0x3F;
}
self.low = Some(next);
(u16::from(self.high) << 7) | u16::from(next)
}
}
/// Emits one slice of system-exclusive bytes, skipping runs that say nothing.
///
/// An empty run still has to be reported when it ends a message, because that is how a consumer
/// learns to stop waiting or to discard what it has.
fn emit_run<'a>(
packet: &'a [u8],
start: usize,
end: usize,
at: u64,
kind: SysExEnd,
emit: &mut impl FnMut(Event<'a>),
) {
let bytes = packet.get(start..end).unwrap_or_default();
if bytes.is_empty() && kind == SysExEnd::Open {
return;
}
emit(Event::SysEx {
at,
bytes,
end: kind,
});
}
/// Packs MIDI into packets no larger than the link will carry.
///
/// One encoder belongs to one link, because the packet being built and the timestamp bits in its
/// header are per-link state.
#[derive(Debug)]
pub struct Encoder {
/// The packet being built, header byte included.
buffer: [u8; MAX_PACKET],
/// How much of the buffer is in use.
used: usize,
/// The largest packet this link will carry.
capacity: usize,
/// The seven timestamp bits most recently written into the open packet.
low: Option<u8>,
}
impl Encoder {
/// Creates an encoder for a link that will carry `capacity` bytes per write.
///
/// The value is clamped rather than rejected: a link that negotiates an unusable MTU should
/// send small packets, not refuse to send.
pub fn new(capacity: usize) -> Self {
Self {
buffer: [0; MAX_PACKET],
used: 0,
capacity: capacity.clamp(MIN_PACKET, MAX_PACKET),
low: None,
}
}
/// Adds one message, emitting a packet whenever one fills.
///
/// `at` is a millisecond count of the caller's choosing; only its position within the
/// thirteen-bit cycle travels over the link.
pub fn push(&mut self, at: u64, message: &MidiMessage, emit: &mut impl FnMut(&[u8])) {
let mut encoded = [0_u8; 3];
let len = message.encode(&mut encoded);
let Some(bytes) = encoded.get(..len) else {
return;
};
if bytes.is_empty() {
return;
}
// Start a packet whose header can carry this timestamp, flushing one that cannot.
self.open_for(at, bytes.len().saturating_add(1), emit);
self.write_timestamp(at);
for byte in bytes {
self.write(*byte);
}
}
/// Adds one whole system-exclusive message, spreading it over as many packets as it needs.
///
/// The payload is the complete message, both framing bytes included, because a dump that is
/// only half a message has no legitimate place on the wire.
pub fn push_sysex(
&mut self,
at: u64,
payload: &[u8],
emit: &mut impl FnMut(&[u8]),
) -> Result<(), EncodeError> {
// Validate the framing.
if payload.first().copied() != Some(SYSEX_START)
|| payload.last().copied() != Some(SYSEX_END)
|| payload.len() < 2
{
return Err(EncodeError::NotFramed);
}
let Some(body) = payload.get(1..payload.len().saturating_sub(1)) else {
return Err(EncodeError::NotFramed);
};
// Open the dump, which needs room for a timestamp and the start byte.
self.open_for(at, 2, emit);
self.write_timestamp(at);
self.write(SYSEX_START);
// Payload bytes carry no timestamps, so a continuation packet is a header and then data.
for byte in body {
if self.used >= self.capacity {
self.flush(emit);
self.write_header(at);
}
self.write(*byte);
}
// Close it, which needs room for a timestamp and the terminator together.
if self.used.saturating_add(2) > self.capacity {
self.flush(emit);
self.write_header(at);
}
self.write_timestamp(at);
self.write(SYSEX_END);
Ok(())
}
/// Emits the packet being built, if there is one.
///
/// A packet is never held back waiting for company: latency is the whole point of this
/// transport, so the caller flushes as soon as it has nothing more to add.
pub fn flush(&mut self, emit: &mut impl FnMut(&[u8])) {
if self.used == 0 {
return;
}
if let Some(packet) = self.buffer.get(..self.used) {
emit(packet);
}
self.used = 0;
self.low = None;
}
/// Makes room for `needed` bytes at timestamp `at`, flushing the open packet if it cannot.
///
/// A packet's header fixes the high six bits of every timestamp in it, so a message that has
/// moved past that window needs a packet of its own even when the old one has space.
fn open_for(&mut self, at: u64, needed: usize, emit: &mut impl FnMut(&[u8])) {
let ticks = ticks_of(at);
let high = high_bits(ticks);
let low = low_bits(ticks);
let header_fits = self
.buffer
.first()
.is_some_and(|byte| *byte == (0x80 | high));
// A timestamp lower than the last one in this packet would read as the counter turning
// over, which would put the message eight seconds into the future.
let ordered = self.low.is_none_or(|previous| low >= previous);
if self.used > 0 && (!header_fits || !ordered) {
self.flush(emit);
}
if self.used > 0 && self.used.saturating_add(needed) > self.capacity {
self.flush(emit);
}
if self.used == 0 {
self.write_header(at);
}
}
/// Writes the header byte that opens a packet.
fn write_header(&mut self, at: u64) {
self.write(0x80 | high_bits(ticks_of(at)));
self.low = None;
}
/// Writes the timestamp byte that precedes a message.
fn write_timestamp(&mut self, at: u64) {
let low = low_bits(ticks_of(at));
self.write(0x80 | low);
self.low = Some(low);
}
/// Appends one byte, dropping it rather than overrunning the buffer.
///
/// Callers reserve space before writing, so a drop here means a bug above rather than a full
/// link, and losing the byte is still better than a panic on the data path.
fn write(&mut self, byte: u8) {
if let Some(slot) = self.buffer.get_mut(self.used) {
*slot = byte;
self.used = self.used.saturating_add(1);
}
}
}
/// The position of a millisecond count within the thirteen-bit timestamp cycle.
fn ticks_of(at: u64) -> u16 {
(at % TIMESTAMP_PERIOD) as u16
}
/// The six timestamp bits carried in a packet's header byte.
fn high_bits(ticks: u16) -> u8 {
((ticks >> 7) & 0x3F) as u8
}
/// The seven timestamp bits carried in a message's timestamp byte.
fn low_bits(ticks: u16) -> u8 {
(ticks & 0x7F) as u8
}