Skip to main content

open_stomp/
blemidi.rs

1//! BLE-MIDI packet framing (Apple / MMA "MIDI over Bluetooth LE" spec), used for the pedal's
2//! Wireless mode. Pure code: the transport itself is in the engine (feature `ble`).
3//!
4//! A packet is a header byte (`1 0 t12…t7`) followed by MIDI messages, each status byte preceded
5//! by a timestamp byte (`1 t6…t0`). SysEx may span packets: a continuation packet carries its
6//! data bytes straight after the header, and the closing `F7` gets its own timestamp byte.
7//! Running status is allowed, with or without a fresh timestamp.
8//!
9//! The pedal speaks exactly the USB-MIDI protocol over BLE (SysEx, CCs, PCs), only framed like
10//! this.
11
12/// The pedal's GATT service and characteristic (the standard BLE-MIDI ones).
13pub const SERVICE: &str = "03b80e5a-ede8-4b33-a751-6ce34ec4c700";
14pub const CHARACTERISTIC: &str = "7772e5db-3868-4112-a1a9-f2669d106bf3";
15
16/// Packet payload size that every BLE link supports (ATT MTU 23 minus the 3-byte header).
17pub const MIN_PACKET: usize = 20;
18
19fn header(ts: u16) -> u8 {
20    0x80 | ((ts >> 7) & 0x3F) as u8
21}
22
23fn stamp(ts: u16) -> u8 {
24    0x80 | (ts & 0x7F) as u8
25}
26
27/// Frame whole MIDI messages into BLE-MIDI packets of at most `max` bytes (`max` ≥ 5), packing
28/// several messages per packet. `ts_ms` is the 13-bit millisecond timestamp (wraps).
29pub fn encode(msgs: &[Vec<u8>], ts_ms: u16, max: usize) -> Vec<Vec<u8>> {
30    let max = max.max(5);
31    let (h, t) = (header(ts_ms), stamp(ts_ms));
32    let mut out = vec![];
33    let mut cur = vec![h];
34    let flush = |cur: &mut Vec<u8>, out: &mut Vec<Vec<u8>>| {
35        if cur.len() > 1 {
36            out.push(std::mem::replace(cur, vec![h]));
37        }
38    };
39    for m in msgs {
40        if m.is_empty() {
41            continue;
42        }
43        if m[0] == 0xF0 {
44            // timestamp + F0 + data, split anywhere in the data; timestamp + F7 stay together
45            if cur.len() + 3 > max {
46                flush(&mut cur, &mut out);
47            }
48            cur.extend([t, 0xF0]);
49            let end = m.last() == Some(&0xF7);
50            let data = &m[1..m.len() - end as usize];
51            for &b in data {
52                if cur.len() + 1 > max {
53                    out.push(std::mem::replace(&mut cur, vec![h]));
54                }
55                cur.push(b);
56            }
57            if end {
58                if cur.len() + 2 > max {
59                    out.push(std::mem::replace(&mut cur, vec![h]));
60                }
61                cur.extend([t, 0xF7]);
62            }
63        } else {
64            if cur.len() + 1 + m.len() > max {
65                flush(&mut cur, &mut out);
66            }
67            cur.push(t);
68            cur.extend_from_slice(m);
69        }
70    }
71    flush(&mut cur, &mut out);
72    out
73}
74
75/// Number of data bytes after a (non-SysEx) status byte.
76fn data_len(status: u8) -> usize {
77    match status {
78        0xF1 | 0xF3 => 1,
79        0xF2 => 2,
80        0xF0..=0xFF => 0,
81        s if s & 0xF0 == 0xC0 || s & 0xF0 == 0xD0 => 1,
82        _ => 2,
83    }
84}
85
86/// Turns BLE-MIDI packets back into whole MIDI messages, keeping SysEx and running status across
87/// packets.
88#[derive(Default)]
89pub struct Decoder {
90    sysex: Option<Vec<u8>>,
91    running: Option<u8>,
92    msg: Vec<u8>,
93}
94
95impl Decoder {
96    pub fn push(&mut self, packet: &[u8]) -> Vec<Vec<u8>> {
97        let mut out = vec![];
98        if packet.first().is_none_or(|h| h & 0x80 == 0) {
99            return out;
100        }
101        let mut i = 1;
102        while i < packet.len() {
103            let b = packet[i];
104            if b & 0x80 == 0 {
105                self.data(b, &mut out);
106                i += 1;
107                continue;
108            }
109            // timestamp byte; the next byte is a status or (running status) data
110            i += 1;
111            let Some(&s) = packet.get(i) else { break };
112            if s & 0x80 == 0 {
113                continue;
114            }
115            i += 1;
116            match s {
117                0xF7 => {
118                    if let Some(mut x) = self.sysex.take() {
119                        x.push(0xF7);
120                        out.push(x);
121                    }
122                }
123                0xF0 => {
124                    self.sysex = Some(vec![0xF0]);
125                    self.running = None;
126                }
127                0xF8..=0xFF => out.push(vec![s]),
128                _ => {
129                    self.sysex = None;
130                    self.running = (s < 0xF0).then_some(s);
131                    self.msg = vec![s];
132                    if data_len(s) == 0 {
133                        out.push(std::mem::take(&mut self.msg));
134                    }
135                }
136            }
137        }
138        out
139    }
140
141    fn data(&mut self, b: u8, out: &mut Vec<Vec<u8>>) {
142        if let Some(x) = &mut self.sysex {
143            x.push(b);
144            return;
145        }
146        if self.msg.is_empty() {
147            match self.running {
148                Some(r) => self.msg.push(r),
149                None => return,
150            }
151        }
152        self.msg.push(b);
153        if self.msg.len() == 1 + data_len(self.msg[0]) {
154            out.push(std::mem::take(&mut self.msg));
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    fn decode_all(packets: &[Vec<u8>]) -> Vec<Vec<u8>> {
164        let mut d = Decoder::default();
165        packets.iter().flat_map(|p| d.push(p)).collect()
166    }
167
168    #[test]
169    fn pedal_replies() {
170        // captured from the pedal over BLE (version reply, a CC)
171        let p = [
172            0x99, 0xE4, 0xF0, 0x00, 0x21, 0x7A, 0x10, 0x56, 0x31, 0x2E, 0x31, 0x2E, 0x31, 0x00, 0x4A, 0x4D, 0x45, 0xE4,
173            0xF7,
174        ];
175        assert_eq!(
176            decode_all(&[p.to_vec(), vec![0xA0, 0x81, 0xB0, 0x3B, 0x02]]),
177            vec![
178                vec![0xF0, 0x00, 0x21, 0x7A, 0x10, 0x56, 0x31, 0x2E, 0x31, 0x2E, 0x31, 0x00, 0x4A, 0x4D, 0x45, 0xF7],
179                vec![0xB0, 0x3B, 0x02],
180            ]
181        );
182    }
183
184    #[test]
185    fn host_request_matches_probe() {
186        // what the CoreBluetooth probe sent and the pedal answered
187        let ts = (0x19 << 7) | 0x50;
188        assert_eq!(
189            encode(&[vec![0xF0, 0x00, 0x21, 0x7A, 0x00, 0xF7]], ts, 182),
190            vec![vec![0x99, 0xD0, 0xF0, 0x00, 0x21, 0x7A, 0x00, 0xD0, 0xF7]]
191        );
192    }
193
194    #[test]
195    fn running_status() {
196        // timestamp + data (new time, same status), and bare data (same time)
197        let p = vec![0x80, 0x81, 0xB0, 0x12, 0x40, 0x82, 0x13, 0x41, 0x14, 0x42];
198        assert_eq!(decode_all(&[p]), vec![vec![0xB0, 0x12, 0x40], vec![0xB0, 0x13, 0x41], vec![0xB0, 0x14, 0x42]]);
199    }
200
201    #[test]
202    fn realtime_inside_sysex() {
203        let p = vec![0x80, 0x81, 0xF0, 0x01, 0x02, 0x82, 0xF8, 0x03, 0x83, 0xF7];
204        assert_eq!(decode_all(&[p]), vec![vec![0xF8], vec![0xF0, 0x01, 0x02, 0x03, 0xF7]]);
205    }
206
207    #[test]
208    fn round_trip_split() {
209        let big: Vec<u8> = [0xF0].into_iter().chain((0..300).map(|i| (i % 128) as u8)).chain([0xF7]).collect();
210        let msgs = vec![vec![0xB1, 0x05, 0x03], big, vec![0xC1, 0x04], vec![0xF0, 0x7E, 0xF7]];
211        for max in [5, 6, 7, 20, 179] {
212            let packets = encode(&msgs, 0x1ABC, max);
213            assert!(packets.iter().all(|p| p.len() <= max && p.len() > 1), "max {max}");
214            assert_eq!(decode_all(&packets), msgs, "max {max}");
215        }
216        assert_eq!(encode(&msgs[..1], 0, 20).len(), 1);
217        // small messages share a packet
218        assert_eq!(
219            encode(&[vec![0xB1, 1, 2], vec![0xB1, 3, 4]], 0, 20),
220            vec![vec![0x80, 0x80, 0xB1, 1, 2, 0x80, 0xB1, 3, 4]]
221        );
222    }
223}