Skip to main content

open_stomp/
midi.rs

1//! MIDI message parsing, SysEx framing and stream reassembly (docs/02, docs/03).
2
3pub const MFR: [u8; 3] = [0x00, 0x21, 0x7A];
4pub const UID: u8 = 0x70;
5
6/// Pedal reports on channel 1, the host commands on channel 2.
7pub const PEDAL_CH: u8 = 0;
8pub const HOST_CH: u8 = 1;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum Cmd {
12    IrSlot = 0x09,
13    Preset = 0x0B,
14    EditBuffer = 0x0C,
15    IrData = 0x10,
16    PresetName = 0x12,
17    System = 0x14,
18    CurState = 0x15,
19    Loop = 0x18,
20    Drum = 0x19,
21    UserAmp = 0x58,
22    Tuner = 0x6F,
23    Notify = 0x7E,
24}
25
26impl Cmd {
27    pub fn from_u8(b: u8) -> Option<Cmd> {
28        use Cmd::*;
29        Some(match b {
30            0x09 => IrSlot,
31            0x0B => Preset,
32            0x0C => EditBuffer,
33            0x10 => IrData,
34            0x12 => PresetName,
35            0x14 => System,
36            0x15 => CurState,
37            0x18 => Loop,
38            0x19 => Drum,
39            0x58 => UserAmp,
40            0x6F => Tuner,
41            0x7E => Notify,
42            _ => return None,
43        })
44    }
45
46    pub fn label(b: u8) -> &'static str {
47        match b {
48            0x09 => "user IR slot",
49            0x0B => "preset",
50            0x0C => "edit buffer",
51            0x0D => "module link",
52            0x10 => "IR data",
53            0x12 => "preset name",
54            0x14 => "system settings",
55            0x15 => "current state",
56            0x16 => "delete IR",
57            0x18 => "looper",
58            0x19 => "drum",
59            0x1B => "bluetooth",
60            0x58 => "user amp (NAM)",
61            0x6F => "tuner",
62            0x7E => "notify",
63            _ => "unknown",
64        }
65    }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub enum Dir {
70    Read = 0,
71    Write = 1,
72    Reply = 2,
73    Ack = 3,
74}
75
76impl Dir {
77    pub fn label(b: u8) -> &'static str {
78        match b {
79            0 => "read",
80            1 => "write",
81            2 => "reply",
82            3 => "ack",
83            _ => "?",
84        }
85    }
86}
87
88/// Build a NEP frame `F0 00 21 7A 70 <cmd> <dir> <payload> F7`.
89pub fn frame(cmd: Cmd, dir: Dir, payload: &[u8]) -> Vec<u8> {
90    let mut v = Vec::with_capacity(payload.len() + 8);
91    v.push(0xF0);
92    v.extend_from_slice(&MFR);
93    v.push(UID);
94    v.push(cmd as u8);
95    v.push(dir as u8);
96    debug_assert!(payload.iter().all(|b| *b < 0x80));
97    v.extend_from_slice(payload);
98    v.push(0xF7);
99    v
100}
101
102/// Short identification request (`0x00` version, `0x60` product name).
103pub fn short_request(kind: u8) -> Vec<u8> {
104    vec![0xF0, MFR[0], MFR[1], MFR[2], kind, 0xF7]
105}
106
107/// Read requests used by the official app's handshake.
108pub mod req {
109    use super::*;
110    use crate::pack::pack;
111
112    pub fn version() -> Vec<u8> {
113        short_request(0x00)
114    }
115    pub fn product() -> Vec<u8> {
116        short_request(0x60)
117    }
118    pub fn settings() -> Vec<u8> {
119        frame(Cmd::System, Dir::Read, &[])
120    }
121    pub fn cur_state() -> Vec<u8> {
122        frame(Cmd::CurState, Dir::Read, &[])
123    }
124    pub fn drum() -> Vec<u8> {
125        frame(Cmd::Drum, Dir::Read, &[])
126    }
127    pub fn looper() -> Vec<u8> {
128        frame(Cmd::Loop, Dir::Read, &[])
129    }
130    pub fn tuner() -> Vec<u8> {
131        frame(Cmd::Tuner, Dir::Read, &[])
132    }
133    /// Padded with 14 zero bytes like the official app's request: the pedal takes request
134    /// parameters from those bytes, and a bare request makes it read whatever the previous message
135    /// left in its buffer (seen over BLE: empty or truncated replies).
136    pub fn edit_buffer() -> Vec<u8> {
137        frame(Cmd::EditBuffer, Dir::Read, &[0; 14])
138    }
139    pub fn ir_slot(slot: u8) -> Vec<u8> {
140        frame(Cmd::IrSlot, Dir::Read, &[slot])
141    }
142    pub fn preset(index: u8) -> Vec<u8> {
143        let mut p = vec![0u8; 14];
144        p[0] = index & 0x7F;
145        frame(Cmd::Preset, Dir::Read, &p)
146    }
147    pub fn user_amp(slot: u8) -> Vec<u8> {
148        let mut hdr = [0u8; 12];
149        hdr[1] = slot;
150        hdr[2] = 4; // sub-command: read slot info
151        frame(Cmd::UserAmp, Dir::Read, &pack(&hdr))
152    }
153}
154
155/// Host → pedal commands (docs/02 live control, docs/03 writes). Live changes are channel-2
156/// CCs/PCs; the pedal does not echo them, so apply them to the local model too.
157pub mod host {
158    use super::*;
159    use crate::catalog::Module;
160    use crate::records::{Preset, Settings, name_bytes};
161
162    pub fn cc(cc: u8, val: u8) -> Vec<u8> {
163        vec![0xB0 | HOST_CH, cc & 0x7F, val & 0x7F]
164    }
165
166    /// Select preset `index` (0-based, P01 = 0).
167    pub fn program(index: u8) -> Vec<u8> {
168        vec![0xC0 | HOST_CH, index & 0x7F]
169    }
170
171    /// Model + on/off of an effect module.
172    pub fn head(m: Module, model: u8, on: bool) -> Option<Vec<u8>> {
173        Some(cc(m.head_cc()?, (if on { 0 } else { 0x40 }) | (model & 0x3F)))
174    }
175
176    /// A module parameter by slot (app display order).
177    pub fn param(m: Module, slot: usize, val: u8) -> Option<Vec<u8>> {
178        let (first, n) = m.param_ccs()?;
179        (slot < n as usize).then(|| cc(first + slot as u8, val))
180    }
181
182    pub fn volume(v: u8) -> Vec<u8> {
183        cc(0x3A, v.min(100))
184    }
185
186    /// Make the pedal display jump to a module's page.
187    pub fn page(m: Module) -> Vec<u8> {
188        cc(0x3B, m as u8)
189    }
190
191    pub const LOOP_REC_PLAY_DUB: u8 = 0x01;
192    pub const LOOP_STOP: u8 = 0x02;
193    pub const LOOP_CLEAR: u8 = 0x04;
194    pub const LOOP_UNDO_REDO: u8 = 0x08;
195
196    pub fn looper_button(b: u8) -> Vec<u8> {
197        cc(0x44, b)
198    }
199
200    /// Set the edit buffer's name. The official app sends the slot being edited first
201    /// (the active preset, even when saving to another slot).
202    pub fn preset_name(source_slot: u8, name: &str) -> Vec<u8> {
203        let mut p = Vec::with_capacity(17);
204        p.push(source_slot & 0x7F);
205        p.extend(name_bytes(name).iter().map(|b| b & 0x7F));
206        frame(Cmd::PresetName, Dir::Write, &p)
207    }
208
209    /// Store a whole preset record in slot `preset.index`.
210    pub fn preset_write(preset: &Preset) -> Vec<u8> {
211        frame(Cmd::Preset, Dir::Write, &preset.to_packed())
212    }
213
214    /// The official app's save sequence: name, record, then select the saved slot.
215    pub fn save_preset(source_slot: u8, preset: &Preset) -> Vec<Vec<u8>> {
216        vec![preset_name(source_slot, &preset.name), preset_write(preset), program(preset.index)]
217    }
218
219    pub fn settings_write(s: &Settings) -> Vec<u8> {
220        frame(Cmd::System, Dir::Write, &s.to_packed())
221    }
222}
223
224/// One parsed MIDI message.
225#[derive(Clone, Debug, PartialEq, Eq)]
226pub enum Msg {
227    Cc { ch: u8, cc: u8, val: u8 },
228    Pc { ch: u8, prog: u8 },
229    /// `F0 00 21 7A 70 <cmd> <dir> <payload> F7`
230    Nep { cmd: u8, dir: u8, payload: Vec<u8> },
231    /// `F0 00 21 7A 70 7E 02 <what> 02 <payload> F7`
232    Notify { what: u8, payload: Vec<u8> },
233    /// `F0 00 21 7A <kind> <payload> F7` (`00`/`10` version, `60` product)
234    Short { kind: u8, payload: Vec<u8> },
235    /// Anything else (other manufacturers, realtime, note on/off...).
236    Other(Vec<u8>),
237}
238
239impl Msg {
240    pub fn parse(b: &[u8]) -> Msg {
241        let Some(&s) = b.first() else {
242            return Msg::Other(vec![]);
243        };
244        match s & 0xF0 {
245            0xB0 if b.len() >= 3 => return Msg::Cc { ch: s & 0x0F, cc: b[1], val: b[2] },
246            0xC0 if b.len() >= 2 => return Msg::Pc { ch: s & 0x0F, prog: b[1] },
247            _ => {}
248        }
249        if s != 0xF0 || b.len() < 6 || b[1..4] != MFR {
250            return Msg::Other(b.to_vec());
251        }
252        let end = if b.last() == Some(&0xF7) { b.len() - 1 } else { b.len() };
253        let body = &b[4..end];
254        if body[0] != UID || body.len() < 3 {
255            return Msg::Short { kind: body[0], payload: body[1..].to_vec() };
256        }
257        let (cmd, dir, payload) = (body[1], body[2], &body[3..]);
258        if cmd == Cmd::Notify as u8 && payload.len() >= 2 {
259            return Msg::Notify { what: payload[0], payload: payload[2..].to_vec() };
260        }
261        Msg::Nep { cmd, dir, payload: payload.to_vec() }
262    }
263}
264
265/// Reassembles a raw byte stream (possibly split SysEx) into whole messages.
266#[derive(Default)]
267pub struct Assembler {
268    buf: Vec<u8>,
269}
270
271impl Assembler {
272    pub fn push(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
273        self.buf.extend_from_slice(data);
274        let mut out = vec![];
275        loop {
276            // drop stray data bytes in front of a status byte
277            let start = self.buf.iter().position(|b| *b >= 0x80).unwrap_or(self.buf.len());
278            self.buf.drain(..start);
279            let Some(&s) = self.buf.first() else { break };
280            let len = if s == 0xF0 {
281                match self.buf.iter().position(|b| *b == 0xF7) {
282                    Some(e) => e + 1,
283                    None => break,
284                }
285            } else if s >= 0xF8 {
286                1
287            } else {
288                let need = match s & 0xF0 {
289                    0xC0 | 0xD0 => 2,
290                    0xF0 => 1,
291                    _ => 3,
292                };
293                if self.buf.len() < need {
294                    break;
295                }
296                // a status byte inside the data means a truncated message
297                match self.buf[1..need].iter().position(|b| *b >= 0x80) {
298                    Some(p) => p + 1,
299                    None => need,
300                }
301            };
302            out.push(self.buf.drain(..len).collect());
303        }
304        out
305    }
306}
307
308pub fn hex(b: &[u8]) -> String {
309    let mut s = String::with_capacity(b.len() * 3);
310    for (i, x) in b.iter().enumerate() {
311        if i > 0 {
312            s.push(' ');
313        }
314        s.push_str(&format!("{x:02X}"));
315    }
316    s
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn parse_basic() {
325        assert_eq!(Msg::parse(&[0xB0, 0x12, 0x40]), Msg::Cc { ch: 0, cc: 0x12, val: 0x40 });
326        assert_eq!(Msg::parse(&[0xC1, 4]), Msg::Pc { ch: 1, prog: 4 });
327        assert_eq!(
328            Msg::parse(&[0xF0, 0, 0x21, 0x7A, 0x70, 0x7E, 0x02, 0x19, 0x02, 1, 2, 3, 0, 0, 0x78, 0xF7]),
329            Msg::Notify { what: 0x19, payload: vec![1, 2, 3, 0, 0, 0x78] }
330        );
331        assert_eq!(
332            Msg::parse(&req::settings()),
333            Msg::Nep { cmd: 0x14, dir: 0, payload: vec![] }
334        );
335    }
336
337    #[test]
338    fn assembler_splits_and_joins() {
339        let mut a = Assembler::default();
340        assert!(a.push(&[0xF0, 0x00, 0x21]).is_empty());
341        let m = a.push(&[0x7A, 0x00, 0xF7, 0xB0, 0x01, 0x08, 0xC0]);
342        assert_eq!(m, vec![vec![0xF0, 0, 0x21, 0x7A, 0, 0xF7], vec![0xB0, 1, 8]]);
343        assert_eq!(a.push(&[0x05]), vec![vec![0xC0, 5]]);
344    }
345
346    #[test]
347    fn host_commands() {
348        use crate::catalog::Module;
349        assert_eq!(host::head(Module::Amp, 8, false), Some(vec![0xB1, 0x01, 0x48]));
350        assert_eq!(host::param(Module::OdFx, 1, 66), Some(vec![0xB1, 0x0D, 66]));
351        assert_eq!(host::param(Module::Gate, 2, 1), None);
352        assert_eq!(host::head(Module::Loop, 0, true), None);
353        assert_eq!(host::program(84), vec![0xC1, 0x54]);
354        assert_eq!(host::page(Module::Gate), vec![0xB1, 0x3B, 0x06]);
355        // captured rename (source slot P69 = 0x44, docs/03)
356        assert_eq!(
357            hex(&host::preset_name(0x44, "4Ooz!")),
358            "F0 00 21 7A 70 12 01 44 34 4F 6F 7A 21 00 00 00 00 00 00 00 00 00 00 00 F7"
359        );
360    }
361
362    #[test]
363    fn user_amp_request_matches_capture() {
364        assert_eq!(
365            hex(&req::user_amp(1)),
366            "F0 00 21 7A 70 58 00 00 02 10 00 00 00 00 00 00 00 00 00 00 00 F7"
367        );
368    }
369}