Skip to main content

open_stomp/
records.rs

1//! Data records carried in SysEx (docs/04).
2
3use crate::catalog::Module;
4use crate::pack::{pack, unpack};
5
6pub const PRESET_LEN: usize = 110;
7pub const PRESET_COUNT: usize = 99;
8
9/// One effect module's state.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub struct ModuleState {
12    pub on: bool,
13    pub model: u8,
14    pub params: [u8; 12],
15}
16
17impl ModuleState {
18    pub fn set_head(&mut self, head: u8) {
19        self.on = head & 0x40 == 0;
20        self.model = head & 0x3F;
21    }
22
23    pub fn head(&self) -> u8 {
24        (if self.on { 0 } else { 0x40 }) | (self.model & 0x3F)
25    }
26}
27
28/// A preset (SysEx `0x0B` / `0x0C`), kept with its raw record so unknown bytes survive a round trip.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Preset {
31    pub index: u8,
32    /// Indexed by `Module as usize` for the eight effect modules.
33    pub modules: [ModuleState; 8],
34    pub volume: u8,
35    /// Chain order of the seven non-gate modules (module indices).
36    pub chain: [u8; 7],
37    pub name: String,
38    pub raw: [u8; PRESET_LEN],
39}
40
41/// (count offset, first param offset) per effect module, indexed by module.
42const PARAM_LAYOUT: [(usize, usize); 8] = [
43    (25, 26), // OD/FX
44    (32, 33), // AMP
45    (41, 42), // CAB
46    (61, 62), // MOD
47    (66, 67), // DELAY
48    (73, 74), // REVERB
49    (20, 21), // GATE
50    (48, 49), // EQ
51];
52
53impl Default for Preset {
54    fn default() -> Self {
55        Preset {
56            index: 0,
57            modules: Default::default(),
58            volume: 80,
59            chain: DEFAULT_CHAIN,
60            name: String::new(),
61            raw: [0; PRESET_LEN],
62        }
63    }
64}
65
66pub const DEFAULT_CHAIN: [u8; 7] = [0, 1, 2, 7, 3, 4, 5];
67
68impl Preset {
69    /// From the packed SysEx body (after `<dir>`).
70    pub fn from_packed(midi: &[u8]) -> Option<Preset> {
71        Preset::from_raw(&unpack(midi))
72    }
73
74    pub fn from_raw(b: &[u8]) -> Option<Preset> {
75        let raw: [u8; PRESET_LEN] = b.get(..PRESET_LEN)?.try_into().ok()?;
76        let mut p = Preset { index: raw[0], raw, ..Default::default() };
77        for m in Module::EFFECTS {
78            let i = m.index();
79            let st = &mut p.modules[i];
80            st.set_head(raw[12 + i]);
81            let (_, first) = PARAM_LAYOUT[i];
82            let n = m.param_ccs().map(|x| x.1 as usize).unwrap_or(0);
83            st.params[..n].copy_from_slice(&raw[first..first + n]);
84        }
85        p.volume = raw[78];
86        p.chain.copy_from_slice(&raw[83..90]);
87        if p.chain.iter().any(|c| *c > 7) {
88            p.chain = DEFAULT_CHAIN;
89        }
90        p.name = ascii(&raw[94..110]);
91        Some(p)
92    }
93
94    /// The 110-byte record: the original raw bytes with every decoded field written back.
95    /// Param-count bytes are only recomputed for modules whose model changed.
96    pub fn to_raw(&self) -> [u8; PRESET_LEN] {
97        let mut raw = self.raw;
98        raw[0] = self.index;
99        for m in Module::EFFECTS {
100            let i = m.index();
101            let st = &self.modules[i];
102            if raw[12 + i] & 0x3F != st.model {
103                raw[PARAM_LAYOUT[i].0] = param_count(m, st.model);
104            }
105            raw[12 + i] = st.head();
106            let (_, first) = PARAM_LAYOUT[i];
107            let n = m.param_ccs().map(|x| x.1 as usize).unwrap_or(0);
108            raw[first..first + n].copy_from_slice(&st.params[..n]);
109        }
110        raw[78] = self.volume;
111        raw[83..90].copy_from_slice(&self.chain);
112        raw[94..110].copy_from_slice(&name_bytes(&self.name));
113        raw
114    }
115
116    /// Packed SysEx body for a `0x0B` write.
117    pub fn to_packed(&self) -> Vec<u8> {
118        pack(&self.to_raw())
119    }
120
121    pub fn module(&self, m: Module) -> Option<&ModuleState> {
122        self.modules.get(m.index())
123    }
124
125    pub fn module_mut(&mut self, m: Module) -> Option<&mut ModuleState> {
126        self.modules.get_mut(m.index())
127    }
128
129    /// Whether the decoded sound (modules, volume, chain) is the same, ignoring unused params.
130    pub fn same_sound(&self, other: &Preset) -> bool {
131        self.volume == other.volume
132            && self.chain == other.chain
133            && Module::EFFECTS.into_iter().all(|m| {
134                let (a, b) = (&self.modules[m.index()], &other.modules[m.index()]);
135                a.on == b.on
136                    && a.model == b.model
137                    && m.params(a.model)
138                        .iter()
139                        .all(|p| a.params[p.slot as usize] == b.params[p.slot as usize])
140            })
141    }
142}
143
144pub const SETTINGS_LEN: usize = 17;
145
146/// Value of a module's param-count byte for a model (the number of params the model uses).
147/// CAB always carries all five: user IRs only hide MIC/POSITION in the UI.
148pub fn param_count(m: Module, model: u8) -> u8 {
149    match m {
150        Module::Cab => 5,
151        _ => m.params(model).len() as u8,
152    }
153}
154
155/// A preset name as the 16-byte zero-padded ASCII field (non-ASCII becomes `?`).
156pub fn name_bytes(name: &str) -> [u8; 16] {
157    let mut b = [0u8; 16];
158    for (d, c) in b.iter_mut().zip(name.chars()) {
159        *d = if (' '..='~').contains(&c) { c as u8 } else { b'?' };
160    }
161    b
162}
163
164/// Global system settings (SysEx `0x14`), 17 bytes.
165#[derive(Clone, Debug, Default, PartialEq, Eq)]
166pub struct Settings {
167    /// 0–20 → −10…+10 dB.
168    pub input_trim: u8,
169    pub eq_bass: u8,
170    pub eq_mid: u8,
171    pub eq_treble: u8,
172    pub wireless: bool,
173    pub loopback: bool,
174    pub raw: Vec<u8>,
175}
176
177impl Settings {
178    pub fn from_packed(midi: &[u8]) -> Option<Settings> {
179        let raw = unpack(midi);
180        if raw.len() < 11 {
181            return None;
182        }
183        Some(Settings {
184            input_trim: raw[1],
185            eq_bass: raw[4],
186            eq_mid: raw[5],
187            eq_treble: raw[6],
188            wireless: raw[9] != 0,
189            loopback: raw[10] != 0,
190            raw,
191        })
192    }
193
194    /// The record with the decoded fields written back over the original bytes.
195    pub fn to_raw(&self) -> Vec<u8> {
196        let mut raw = self.raw.clone();
197        if raw.len() < SETTINGS_LEN {
198            raw.resize(SETTINGS_LEN, 0);
199        }
200        raw[1] = self.input_trim.min(20);
201        raw[4] = self.eq_bass.min(100);
202        raw[5] = self.eq_mid.min(100);
203        raw[6] = self.eq_treble.min(100);
204        raw[9] = self.wireless as u8;
205        raw[10] = self.loopback as u8;
206        raw
207    }
208
209    pub fn to_packed(&self) -> Vec<u8> {
210        pack(&self.to_raw())
211    }
212
213    pub fn input_trim_db(&self) -> i16 {
214        self.input_trim as i16 - 10
215    }
216}
217
218/// User IR slot reply payload (`<slot> <packed: 4 B field, name>`).
219pub fn parse_ir_slot(payload: &[u8]) -> Option<(u8, String)> {
220    let (&slot, rest) = payload.split_first()?;
221    let raw = unpack(rest);
222    Some((slot, ascii(raw.get(4..)?)))
223}
224
225/// User AMP (NAM) slot info reply payload: slot and name. See [`crate::upload::UserAmpInfo`] for
226/// the whole record.
227pub fn parse_user_amp(payload: &[u8]) -> Option<(u8, String)> {
228    let i = crate::upload::UserAmpInfo::parse(payload)?;
229    (crate::upload::Chunk::parse(payload)?.sub == crate::upload::amp_sub::READ_INFO).then_some((i.slot, i.name))
230}
231
232/// Plain 7-bit 14-bit value, high 7 bits first.
233pub fn u14(hi: u8, lo: u8) -> u16 {
234    ((hi as u16 & 0x7F) << 7) | (lo as u16 & 0x7F)
235}
236
237/// Zero-terminated, zero-padded ASCII.
238pub fn ascii(b: &[u8]) -> String {
239    b.iter()
240        .take_while(|c| **c != 0)
241        .map(|&c| if (0x20..0x7F).contains(&c) { c as char } else { '?' })
242        .collect::<String>()
243        .trim_end()
244        .to_owned()
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::pack::pack;
251
252    #[test]
253    fn preset_decode() {
254        let mut raw = [0u8; PRESET_LEN];
255        raw[0] = 4;
256        raw[13] = 0x48; // AMP off, model 8
257        raw[33] = 64; // AMP gain
258        raw[78] = 80;
259        raw[83..90].copy_from_slice(&DEFAULT_CHAIN);
260        raw[94..99].copy_from_slice(b"Hello");
261        let p = Preset::from_packed(&pack(&raw)).unwrap();
262        assert_eq!(p.index, 4);
263        assert!(!p.modules[1].on);
264        assert_eq!(p.modules[1].model, 8);
265        assert_eq!(p.modules[1].params[0], 64);
266        assert_eq!(p.name, "Hello");
267    }
268
269    #[test]
270    fn settings_factory() {
271        let raw = [0x00, 0x0A, 0x1E, 0x0A, 0x32, 0x32, 0x32, 0x32, 0x32, 1, 1, 0x3C, 0xB4, 0, 0, 0, 0];
272        let s = Settings::from_packed(&pack(&raw)).unwrap();
273        assert_eq!(s.input_trim_db(), 0);
274        assert_eq!(s.eq_mid, 50);
275        assert!(s.wireless && s.loopback);
276        assert_eq!(s.to_raw(), raw);
277        let mut s2 = s.clone();
278        s2.input_trim = 13;
279        s2.loopback = false;
280        let r = Settings::from_packed(&s2.to_packed()).unwrap();
281        assert_eq!((r.input_trim, r.loopback, r.raw[12]), (13, false, 0xB4));
282    }
283
284    #[test]
285    fn preset_encode_patches_fields() {
286        let mut raw = [0u8; PRESET_LEN];
287        raw[8] = 0x62;
288        raw[12] = 0x05; // OD/FX DS1 (3 params)
289        raw[25] = 3;
290        raw[83..90].copy_from_slice(&DEFAULT_CHAIN);
291        let mut p = Preset::from_raw(&raw).unwrap();
292        assert_eq!(p.to_raw(), raw);
293        p.index = 9;
294        p.modules[0].model = 3; // Boost RC (4 params)
295        p.modules[0].on = false;
296        p.modules[0].params[3] = 77;
297        p.name = "Lead ünicode name too long".into();
298        let r = p.to_raw();
299        assert_eq!((r[0], r[8], r[12], r[25], r[29]), (9, 0x62, 0x43, 4, 77));
300        assert_eq!(&r[94..110], b"Lead ?nicode nam");
301        assert_eq!(Preset::from_packed(&p.to_packed()).unwrap().to_raw(), r);
302    }
303}