Skip to main content

open_stomp/
catalog.rs

1//! Module / model / parameter catalogue (docs/02).
2
3/// A block in the signal chain. The discriminant is the page value of CC `0x3B` and, for the
4/// eight effect modules, also the head CC number and the head-byte offset in a preset record.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
6#[repr(u8)]
7pub enum Module {
8    OdFx = 0,
9    Amp = 1,
10    Cab = 2,
11    Mod = 3,
12    Delay = 4,
13    Reverb = 5,
14    Gate = 6,
15    Eq = 7,
16    Loop = 8,
17    Drum = 9,
18}
19
20impl Module {
21    pub const ALL: [Module; 10] = [
22        Module::OdFx,
23        Module::Amp,
24        Module::Cab,
25        Module::Mod,
26        Module::Delay,
27        Module::Reverb,
28        Module::Gate,
29        Module::Eq,
30        Module::Loop,
31        Module::Drum,
32    ];
33
34    /// The eight modules that have a head CC, models and parameters.
35    pub const EFFECTS: [Module; 8] = [
36        Module::OdFx,
37        Module::Amp,
38        Module::Cab,
39        Module::Mod,
40        Module::Delay,
41        Module::Reverb,
42        Module::Gate,
43        Module::Eq,
44    ];
45
46    pub fn from_index(i: u8) -> Option<Module> {
47        Module::ALL.get(i as usize).copied()
48    }
49
50    pub fn index(self) -> usize {
51        self as usize
52    }
53
54    pub fn is_effect(self) -> bool {
55        (self as u8) < 8
56    }
57
58    pub fn name(self) -> &'static str {
59        match self {
60            Module::OdFx => "OD/FX",
61            Module::Amp => "AMP",
62            Module::Cab => "CAB",
63            Module::Mod => "MOD",
64            Module::Delay => "DELAY",
65            Module::Reverb => "REVERB",
66            Module::Gate => "GATE",
67            Module::Eq => "EQ",
68            Module::Loop => "LOOP",
69            Module::Drum => "DRUM",
70        }
71    }
72
73    pub fn head_cc(self) -> Option<u8> {
74        self.is_effect().then_some(self as u8)
75    }
76
77    /// First parameter CC and number of parameter CCs.
78    pub fn param_ccs(self) -> Option<(u8, u8)> {
79        Some(match self {
80            Module::Gate => (0x08, 2),
81            Module::OdFx => (0x0C, 4),
82            Module::Amp => (0x12, 6),
83            Module::Cab => (0x1A, 5),
84            Module::Eq => (0x20, 12),
85            Module::Mod => (0x2C, 4),
86            Module::Delay => (0x30, 4),
87            Module::Reverb => (0x36, 4),
88            Module::Loop | Module::Drum => return None,
89        })
90    }
91
92    /// Which module (and param slot) a parameter CC belongs to.
93    pub fn from_param_cc(cc: u8) -> Option<(Module, usize)> {
94        Module::EFFECTS.into_iter().find_map(|m| {
95            let (first, n) = m.param_ccs()?;
96            (first..first + n)
97                .contains(&cc)
98                .then(|| (m, (cc - first) as usize))
99        })
100    }
101
102    pub fn models(self) -> &'static [&'static str] {
103        match self {
104            Module::OdFx => OD_MODELS,
105            Module::Amp => AMP_MODELS,
106            Module::Cab => CAB_MODELS,
107            Module::Mod => MOD_MODELS,
108            Module::Delay => DELAY_MODELS,
109            Module::Reverb => REVERB_MODELS,
110            Module::Gate => &["Noise Gate"],
111            Module::Eq => &["6-band EQ", "10-band EQ"],
112            Module::Loop | Module::Drum => &[],
113        }
114    }
115
116    pub fn model_name(self, model: u8) -> String {
117        match self.models().get(model as usize) {
118            Some(n) => (*n).to_owned(),
119            None => format!("Model {}", model + 1),
120        }
121    }
122
123    /// Parameters of a model in app display order.
124    pub fn params(self, model: u8) -> Vec<ParamDef> {
125        let m = model as usize;
126        match self {
127            Module::OdFx => {
128                let mut v = knobs(OD_PARAMS.get(m).copied().unwrap_or(&[]));
129                if m == 13 {
130                    v[0] = ParamDef::enumerated(0, "GAIN BOOST", &["OFF", "ON"]); // Boost: a switch
131                }
132                v
133            }
134            Module::Amp => knobs(&["GAIN", "BASS", "MIDDLE", "TREBLE", "PRESENCE", "MASTER"]),
135            Module::Cab => {
136                let mut v = vec![
137                    ParamDef::enumerated(0, "MIC", MICS),
138                    ParamDef::enumerated(1, "POSITION", POSITIONS),
139                    ParamDef::knob(2, "LOW CUT"),
140                    ParamDef::knob(3, "HIGH CUT"),
141                    ParamDef::knob(4, "VOLUME"),
142                ];
143                if model >= 30 {
144                    v.drain(..2); // user IRs hide MIC / POSITION
145                }
146                v
147            }
148            Module::Mod => MOD_PARAMS.get(m).map(|p| p.to_vec()).unwrap_or_default(),
149            Module::Delay => knobs(if m == 2 {
150                &["TIME", "FEEDBACK", "MOD", "LEVEL"]
151            } else {
152                &["TIME", "FEEDBACK", "LEVEL"]
153            }),
154            Module::Reverb => knobs(REVERB_PARAMS.get(m).copied().unwrap_or(&[])),
155            Module::Gate => knobs(&["THRESHOLD", "DECAY"]),
156            Module::Eq => {
157                let names: &[&'static str] = if model == 1 { EQ10 } else { EQ6 };
158                names
159                    .iter()
160                    .enumerate()
161                    .map(|(i, n)| ParamDef {
162                        slot: i as u8,
163                        name: n,
164                        kind: if model == 1 && (i == 0 || i == 11) {
165                            ParamKind::Knob
166                        } else {
167                            ParamKind::Bipolar
168                        },
169                    })
170                    .collect()
171            }
172            Module::Loop | Module::Drum => vec![],
173        }
174    }
175
176    /// The parameter definition for a given param slot of a model, if the model uses it.
177    pub fn param(self, model: u8, slot: usize) -> Option<ParamDef> {
178        self.params(model).into_iter().find(|p| p.slot as usize == slot)
179    }
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum ParamKind {
184    /// 0–100 knob.
185    Knob,
186    /// 0–100 with 50 = centre (EQ bands).
187    Bipolar,
188    /// Index into a list of choices.
189    Enum(&'static [&'static str]),
190}
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub struct ParamDef {
194    /// Offset from the module's first param CC (= offset in the preset record).
195    pub slot: u8,
196    pub name: &'static str,
197    pub kind: ParamKind,
198}
199
200impl ParamDef {
201    const fn knob(slot: u8, name: &'static str) -> Self {
202        ParamDef { slot, name, kind: ParamKind::Knob }
203    }
204
205    const fn enumerated(slot: u8, name: &'static str, opts: &'static [&'static str]) -> Self {
206        ParamDef { slot, name, kind: ParamKind::Enum(opts) }
207    }
208
209    /// Human-readable value.
210    pub fn format(&self, v: u8) -> String {
211        match self.kind {
212            ParamKind::Knob => v.to_string(),
213            ParamKind::Bipolar => format!("{:+}", v as i16 - 50),
214            ParamKind::Enum(opts) => opts
215                .get(v as usize)
216                .map(|s| (*s).to_owned())
217                .unwrap_or_else(|| v.to_string()),
218        }
219    }
220}
221
222fn knobs(names: &[&'static str]) -> Vec<ParamDef> {
223    names
224        .iter()
225        .enumerate()
226        .map(|(i, n)| ParamDef::knob(i as u8, n))
227        .collect()
228}
229
230pub const OD_MODELS: &[&str] = &[
231    "Red Comp", "RS Comp", "Yellow Dist", "Boost RC", "Boost AC", "DS1", "TS9", "BD2", "Klone", "Ratt2",
232    "Red Drive", "Cruncher", "Muff", "Boost", "Face Fuzz", "ST Drive",
233];
234
235const OD_PARAMS: &[&[&str]] = &[
236    &["OUTPUT", "SENSITIVITY"],
237    &["SUSTAIN", "LEVEL"],
238    &["OUTPUT", "DISTORTION"],
239    &["GAIN", "VOLUME", "BASS", "TREBLE"],
240    &["GAIN", "VOLUME", "BASS", "TREBLE"],
241    &["GAIN", "TONE", "VOLUME"],
242    &["DRIVE", "TONE", "LEVEL"],
243    &["LEVEL", "TONE", "GAIN"],
244    &["VOLUME", "DRIVE", "TONE"],
245    &["DIST", "FILTER", "VOLUME"],
246    &["DRIVE", "TONE", "LEVEL"],
247    &["VOLUME", "TONE", "GAIN"],
248    &["VOLUME", "TONE", "SUSTAIN"],
249    &["GAIN BOOST", "VOLUME"],
250    &["GAIN", "TONE", "OUTPUT"],
251    &["VOLUME", "GAIN", "FILTER"],
252];
253
254pub const AMP_MODELS: &[&str] = &[
255    "Black Twin", "Tweed 5F1", "Bassguy", "Prince Rev", "Tweed 5F8", "Deluxe Rev", "Bdotone CL", "Voxy 30HW",
256    "Mar J800", "Mar J45", "Mar Plexi100", "Jazz CL", "Mes Rector", "Mes MKIV Lead", "Mes MKV Red",
257    "Mes LStar CL", "Cornfed M50", "ODS-100", "Spawn Qrod", "Soloist 100", "Fryman HB", "Fryman DS", "PVH 61",
258    "Eddie 51", "2Rock J35", "Ampug SV Bass", "Ampug Port Bass", "FR Bass", "Mes 400 Bass", "Agula DP751",
259    "USER AMP 01", "USER AMP 02", "USER AMP 03", "USER AMP 04", "USER AMP 05", "USER AMP 06", "USER AMP 07",
260    "USER AMP 08", "USER AMP 09", "USER AMP 10",
261];
262
263pub const CAB_MODELS: &[&str] = &[
264    "Twin 212", "Tweed 110", "Bassguy 410", "Prince 112", "Tweed 210", "Deluxe 112", "Bdotone 212", "Voxy 212",
265    "Mar G12M 412", "Mar Silver 212", "Mar V30 412", "Jazz 212", "Mes OS 412", "Mes Trad 412", "Mes V30 212",
266    "Mes EV 112", "Cornfed 212", "ODS 212", "Spawn 412", "SLO 412", "Fryman 412", "Fryman 212", "PVH 412",
267    "Eddie 412", "2Rock 212", "Ampug 810", "Ampug 410", "FR 210", "Mes 410", "Agula 810",
268    "USER IR 01", "USER IR 02", "USER IR 03", "USER IR 04", "USER IR 05", "USER IR 06", "USER IR 07",
269    "USER IR 08", "USER IR 09", "USER IR 10",
270];
271
272pub const MICS: &[&str] = &["E906", "M160", "MD421", "R121", "SM57", "SM58", "SM7B", "U47"];
273pub const POSITIONS: &[&str] = &["EDGE", "MIDDLE", "CENTER"];
274
275pub const MOD_MODELS: &[&str] = &[
276    "Chorus-1", "Chorus-2", "Stereo Chorus", "Vibrato", "Detune", "Flanger", "Phaser 90", "Phaser 100", "Chorus+",
277    "Uni Vibe", "Tremolo", "Chorus-3", "Harmony",
278];
279
280const KEYS: &[&str] = &["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
281const HARM: &[&str] = &["-8TH", "-6TH", "-5TH", "-4TH", "-3TH", "3TH", "4TH", "5TH", "6TH", "8TH"];
282
283const MOD_PARAMS: &[&[ParamDef]] = &[
284    &[ParamDef::knob(0, "INTENSITY"), ParamDef::knob(1, "DEPTH"), ParamDef::knob(2, "RATE")],
285    &[ParamDef::knob(0, "RATE"), ParamDef::knob(1, "DEPTH")],
286    &[ParamDef::knob(0, "INTENSITY"), ParamDef::knob(1, "WIDTH"), ParamDef::knob(2, "RATE")],
287    &[ParamDef::knob(0, "RATE"), ParamDef::knob(1, "DEPTH")],
288    &[ParamDef::knob(0, "L"), ParamDef::knob(1, "MIX"), ParamDef::knob(2, "R")],
289    &[
290        ParamDef::knob(0, "LEVEL"),
291        ParamDef::knob(1, "RATE"),
292        ParamDef::knob(2, "WIDTH"),
293        ParamDef::knob(3, "FEEDBACK"),
294    ],
295    &[ParamDef::knob(0, "SPEED")],
296    &[ParamDef::knob(0, "INTENSITY"), ParamDef::knob(1, "SPEED")],
297    &[
298        ParamDef::knob(0, "SPEED"),
299        ParamDef::knob(1, "WIDTH"),
300        ParamDef::enumerated(2, "MODE", &["CHORUS", "FLANGER"]),
301        ParamDef::knob(3, "INTENSITY"),
302    ],
303    &[
304        ParamDef::knob(0, "SPEED"),
305        ParamDef::knob(1, "VOLUME"),
306        ParamDef::knob(2, "INTENSITY"),
307        ParamDef::enumerated(3, "MODE", &["CHORUS", "VIBE"]),
308    ],
309    &[ParamDef::knob(0, "RATE"), ParamDef::knob(1, "DEPTH")],
310    &[ParamDef::knob(0, "RATE"), ParamDef::knob(1, "DEPTH"), ParamDef::knob(2, "TONE")],
311    &[
312        ParamDef::enumerated(0, "KEY", KEYS),
313        ParamDef::enumerated(1, "MODE", &["MINOR", "MAJOR"]),
314        ParamDef::enumerated(2, "HARM", HARM),
315        ParamDef::knob(3, "MIX"),
316    ],
317];
318
319pub const DELAY_MODELS: &[&str] = &[
320    "Analog Delay", "Digital Delay", "Mod Delay", "Tape Delay", "Reverse Delay", "Pan Delay", "Physical Delay",
321];
322
323pub const REVERB_MODELS: &[&str] = &["Room", "Hall", "Plate", "Spring", "Shimmer", "Damp"];
324
325const REVERB_PARAMS: &[&[&str]] = &[
326    &["DECAY", "TONE", "LEVEL"],
327    &["DECAY", "TONE", "PRE DELAY", "LEVEL"],
328    &["DECAY", "LEVEL"],
329    &["DECAY", "LEVEL"],
330    &["LEVEL", "DECAY", "SHIMMER"],
331    &["LEVEL", "DEPTH"],
332];
333
334const EQ6: &[&str] = &["100Hz", "220Hz", "500Hz", "1.2kHz", "2.5kHz", "6.4kHz"];
335const EQ10: &[&str] = &[
336    "GAIN", "31Hz", "62.5Hz", "125Hz", "250Hz", "500Hz", "1kHz", "2kHz", "4kHz", "8kHz", "16kHz", "VOLUME",
337];
338
339pub const NOTES: &[&str] = &["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
340
341pub const DRUM_STYLES: &[&str] = &[
342    "Pop", "Rock", "Metal", "Disco", "Funk", "Blues", "Hip Hop", "Jazz", "Latin", "Metro",
343];
344const METRO: &[&str] = &["4/4", "2/4", "3/4", "5/4", "6/8", "7/8", "9/8", "10/8", "11/8", "12/8"];
345
346/// Drum pattern name from the flat value (`style*10 + pattern`).
347pub fn drum_pattern_name(v: u8) -> String {
348    let (style, pat) = ((v / 10) as usize, (v % 10) as usize);
349    match DRUM_STYLES.get(style) {
350        Some(&"Metro") => format!("Metro {}", METRO[pat]),
351        Some(s) => format!("{s} {:02}", pat + 1),
352        None => format!("Pattern {v}"),
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn param_cc_lookup() {
362        assert_eq!(Module::from_param_cc(0x12), Some((Module::Amp, 0)));
363        assert_eq!(Module::from_param_cc(0x09), Some((Module::Gate, 1)));
364        assert_eq!(Module::from_param_cc(0x2B), Some((Module::Eq, 11)));
365        assert_eq!(Module::from_param_cc(0x3A), None);
366    }
367
368    #[test]
369    fn model_counts() {
370        assert_eq!(OD_MODELS.len(), 16);
371        assert_eq!(AMP_MODELS.len(), 40);
372        assert_eq!(CAB_MODELS.len(), 40);
373        assert_eq!(MOD_MODELS.len(), 13);
374        assert_eq!(MOD_PARAMS.len(), 13);
375        assert_eq!(OD_PARAMS.len(), 16);
376    }
377
378    #[test]
379    fn drum_names() {
380        assert_eq!(drum_pattern_name(13), "Rock 04");
381        assert_eq!(drum_pattern_name(92), "Metro 3/4");
382    }
383}