1use crate::catalog::{Module, NOTES, drum_pattern_name};
5use crate::midi::{Cmd, Dir, HOST_CH, Msg, hex};
6use crate::records::{PRESET_COUNT, Preset, Settings, parse_ir_slot, u14};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum Origin {
10 Pedal,
12 Host,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum Category {
18 Preset,
19 Effect(Module),
20 Volume,
21 Navigation,
22 Looper,
23 Drum,
24 Tuner,
25 Settings,
26 Device,
27 Protocol,
29 Unknown,
30}
31
32impl Category {
33 pub fn label(self) -> &'static str {
34 match self {
35 Category::Preset => "PRESET",
36 Category::Effect(m) => m.name(),
37 Category::Volume => "VOLUME",
38 Category::Navigation => "PAGE",
39 Category::Looper => "LOOPER",
40 Category::Drum => "DRUM",
41 Category::Tuner => "TUNER",
42 Category::Settings => "SETTINGS",
43 Category::Device => "DEVICE",
44 Category::Protocol => "SYSEX",
45 Category::Unknown => "???",
46 }
47 }
48}
49
50#[derive(Clone, Debug)]
52pub struct Event {
53 pub origin: Origin,
54 pub category: Category,
55 pub subject: String,
57 pub value: String,
59 pub coalesce: Option<u64>,
61 pub redundant: bool,
64 pub raw: Vec<u8>,
65}
66
67#[derive(Clone, Debug, Default)]
68pub struct Looper {
69 pub state: u8,
71 pub layers: u8,
73 pub rec_level: Option<u8>,
74 pub play_level: Option<u8>,
75 pub sync: Option<bool>,
76 pub mode: Option<u8>,
78}
79
80impl Looper {
81 pub fn set(&mut self, v: u8) {
82 self.state = v & 0x0F;
83 self.layers = v >> 4;
84 }
85
86 pub fn state_name(&self) -> &'static str {
87 looper_state_name(self.state)
88 }
89}
90
91pub fn looper_state_name(s: u8) -> &'static str {
92 match s {
93 0 => "Empty",
94 1 => "Recording",
95 2 => "Playing",
96 3 => "Overdubbing",
97 4 => "Stopped",
98 _ => "Unknown",
99 }
100}
101
102#[derive(Clone, Debug, Default)]
103pub struct Drum {
104 pub playing: bool,
105 pub pattern: Option<u8>,
106 pub level: Option<u8>,
107 pub bpm: Option<u16>,
108}
109
110#[derive(Clone, Debug, Default)]
111pub struct Tuner {
112 pub active: bool,
113 pub detected: bool,
114 pub note: Option<u8>,
115 pub cents: Option<u8>,
117}
118
119impl Tuner {
120 pub fn cents_offset(&self) -> Option<i16> {
121 self.cents.map(|c| c as i16 - 50)
122 }
123}
124
125#[derive(Clone, Debug)]
126pub struct DeviceState {
127 pub firmware: Option<String>,
128 pub model_id: Option<String>,
129 pub product: Option<String>,
130 pub presets: Vec<Option<Preset>>,
131 pub current: Option<u8>,
133 pub live: Preset,
135 pub live_known: bool,
136 pub page: Option<Module>,
137 pub looper: Looper,
138 pub drum: Drum,
139 pub tuner: Tuner,
140 pub settings: Option<Settings>,
141 pub user_amps: [Option<String>; 10],
142 pub user_irs: [Option<String>; 10],
143}
144
145impl Default for DeviceState {
146 fn default() -> Self {
147 DeviceState {
148 firmware: None,
149 model_id: None,
150 product: None,
151 presets: vec![None; PRESET_COUNT],
152 current: None,
153 live: Preset::default(),
154 live_known: false,
155 page: None,
156 looper: Looper::default(),
157 drum: Drum::default(),
158 tuner: Tuner::default(),
159 settings: None,
160 user_amps: Default::default(),
161 user_irs: Default::default(),
162 }
163 }
164}
165
166fn key(parts: &[u64]) -> Option<u64> {
167 Some(parts.iter().fold(0xcbf29ce484222325u64, |h, p| (h ^ p).wrapping_mul(0x100000001b3)))
168}
169
170fn upload_progress(payload: &[u8]) -> String {
172 match crate::upload::Chunk::parse(payload) {
173 Some(c) => format!("{} / {} bytes", c.offset as usize + c.data.len(), c.total),
174 None => format!("{} bytes", payload.len()),
175 }
176}
177
178pub fn preset_label(i: u8) -> String {
179 format!("P{:02}", i as u16 + 1)
180}
181
182impl DeviceState {
183 pub fn stored(&self, i: u8) -> Option<&Preset> {
184 self.presets.get(i as usize)?.as_ref()
185 }
186
187 pub fn preset_name(&self, i: u8) -> Option<&str> {
188 self.stored(i).map(|p| p.name.as_str())
189 }
190
191 pub fn modified(&self) -> bool {
193 match (self.current, self.live_known) {
194 (Some(i), true) => self.stored(i).is_some_and(|p| !p.same_sound(&self.live)),
195 _ => false,
196 }
197 }
198
199 pub fn model_name(&self, m: Module, model: u8) -> String {
201 let user = match m {
202 Module::Amp if model >= 30 => self.user_amps.get(model as usize - 30),
203 Module::Cab if model >= 30 => self.user_irs.get(model as usize - 30),
204 _ => None,
205 };
206 match user.and_then(|n| n.as_deref()) {
207 Some(n) if !n.is_empty() && !n.starts_with("--") && n != m.model_name(model) => {
208 format!("{} · {n}", m.model_name(model))
209 }
210 _ => m.model_name(model),
211 }
212 }
213
214 pub fn apply(&mut self, bytes: &[u8], origin: Origin) -> Event {
216 let mut ev = Event {
217 origin,
218 category: Category::Unknown,
219 subject: String::new(),
220 value: String::new(),
221 coalesce: None,
222 redundant: false,
223 raw: bytes.to_vec(),
224 };
225 match Msg::parse(bytes) {
226 Msg::Cc { ch, cc, val } => self.apply_cc(&mut ev, ch, cc, val),
227 Msg::Pc { prog, .. } => {
228 self.select_preset(prog);
229 ev.category = Category::Preset;
230 ev.subject = "Preset selected".into();
231 ev.value = match self.preset_name(prog) {
232 Some(n) => format!("{} · {n}", preset_label(prog)),
233 None => preset_label(prog),
234 };
235 }
236 Msg::Short { kind, payload } => self.apply_short(&mut ev, kind, &payload),
237 Msg::Notify { what, payload } => self.apply_notify(&mut ev, what, &payload),
238 Msg::Nep { cmd, dir, payload } => self.apply_nep(&mut ev, cmd, dir, &payload),
239 Msg::Other(b) => {
240 ev.subject = "Unrecognised message".into();
241 ev.value = hex(&b);
242 }
243 }
244 ev
245 }
246
247 pub fn select_preset(&mut self, prog: u8) {
248 self.current = Some(prog);
249 if let Some(p) = self.stored(prog) {
250 self.live = p.clone();
251 self.live_known = true;
252 }
253 }
254
255 fn apply_cc(&mut self, ev: &mut Event, ch: u8, cc: u8, val: u8) {
256 let o = ev.origin as u64;
257 match cc {
258 0x00..=0x07 => {
259 let m = Module::from_index(cc).unwrap();
260 let st = &mut self.live.modules[m.index()];
261 let (was_on, was_model) = (st.on, st.model);
262 ev.redundant = st.head() == val && self.live_known;
263 st.set_head(val);
264 let (on, model) = (st.on, st.model);
265 ev.category = Category::Effect(m);
266 let name = self.model_name(m, model);
267 if model != was_model {
268 ev.subject = "Model".into();
269 ev.value = format!("#{:02} {name}", model + 1);
270 ev.coalesce = key(&[o, 1, cc as u64]);
271 } else {
272 ev.subject = name;
273 ev.value = if on { "ON" } else { "OFF" }.into();
274 }
275 if on != was_on && model != was_model {
276 ev.value.push_str(if on { " · ON" } else { " · OFF" });
277 }
278 }
279 0x08..=0x39 if Module::from_param_cc(cc).is_some() => {
280 let (m, slot) = Module::from_param_cc(cc).unwrap();
281 let st = &mut self.live.modules[m.index()];
282 let model = st.model;
283 ev.redundant = (st.params[slot] == val && self.live_known) || m.param(model, slot).is_none();
284 st.params[slot] = val;
285 ev.category = Category::Effect(m);
286 let (pname, pval) = match m.param(model, slot) {
287 Some(p) => (p.name.to_owned(), p.format(val)),
288 None => (format!("P{}", slot + 1), val.to_string()),
289 };
290 ev.subject = format!("{} · {pname}", m.model_name(model));
291 ev.value = pval;
292 ev.coalesce = key(&[o, 2, cc as u64, model as u64]);
293 }
294 0x3A => {
295 ev.redundant = self.live.volume == val && self.live_known;
296 self.live.volume = val;
297 ev.category = Category::Volume;
298 ev.subject = "Preset volume".into();
299 ev.value = val.to_string();
300 ev.coalesce = key(&[o, 3]);
301 }
302 0x3B => {
303 self.page = Module::from_index(val);
304 ev.category = Category::Navigation;
305 ev.subject = "Pedal page".into();
306 ev.value = self.page.map(|m| m.name().to_owned()).unwrap_or_else(|| val.to_string());
307 }
308 0x3F => {
309 self.drum.playing = val != 0;
310 ev.category = Category::Drum;
311 ev.subject = "Drum".into();
312 ev.value = if val != 0 { "PLAYING" } else { "STOPPED" }.into();
313 }
314 0x40 => {
315 self.drum.pattern = Some(val);
316 ev.category = Category::Drum;
317 ev.subject = "Pattern".into();
318 ev.value = drum_pattern_name(val);
319 ev.coalesce = key(&[o, 4]);
320 }
321 0x41 => {
322 self.drum.level = Some(val);
323 ev.category = Category::Drum;
324 ev.subject = "Drum level".into();
325 ev.value = val.to_string();
326 ev.coalesce = key(&[o, 5]);
327 }
328 0x42 | 0x43 => {
329 let (slot, name) = if cc == 0x42 {
330 (&mut self.looper.rec_level, "Record level")
331 } else {
332 (&mut self.looper.play_level, "Playback level")
333 };
334 *slot = Some(val);
335 ev.category = Category::Looper;
336 ev.subject = name.into();
337 ev.value = val.to_string();
338 ev.coalesce = key(&[o, 6, cc as u64]);
339 }
340 0x44 if ch == HOST_CH => {
341 ev.category = Category::Looper;
342 ev.subject = "Looper button".into();
343 ev.value = match val {
344 1 => "REC / PLAY / DUB",
345 2 => "STOP",
346 4 => "CLEAR",
347 8 => "UNDO / REDO",
348 _ => "?",
349 }
350 .into();
351 }
352 0x44 => {
353 self.looper.set(val);
354 ev.category = Category::Looper;
355 ev.subject = "Looper".into();
356 ev.value = self.looper.state_name().to_uppercase();
357 match self.looper.layers {
358 1 => ev.value.push_str(" · overdub layer (undo available)"),
359 2 => ev.value.push_str(" · base layer"),
360 _ => {}
361 }
362 }
363 0x45 | 0x46 | 0x48 => {
364 match cc {
365 0x45 => self.tuner.detected = val != 0,
366 0x46 => self.tuner.note = Some(val % 12),
367 _ => self.tuner.cents = Some(val),
368 }
369 ev.category = Category::Tuner;
370 ev.subject = "Pitch".into();
371 ev.value = match (self.tuner.detected, self.tuner.note, self.tuner.cents_offset()) {
372 (false, _, _) => "—".into(),
373 (true, Some(n), Some(c)) => format!("{} {c:+} ¢", NOTES[n as usize]),
374 (true, Some(n), None) => NOTES[n as usize].into(),
375 _ => "detecting…".into(),
376 };
377 ev.coalesce = key(&[o, 7]);
378 }
379 0x49 => {
380 self.looper.sync = Some(val != 0);
381 ev.category = Category::Looper;
382 ev.subject = "Loop sync".into();
383 ev.value = if val != 0 { "ON" } else { "OFF" }.into();
384 }
385 0x4A => {
386 self.looper.mode = Some(val);
387 ev.category = Category::Looper;
388 ev.subject = "Loop mode".into();
389 ev.value = if val == 0 { "NR" } else { "AR" }.into();
390 }
391 _ => {
392 ev.subject = format!("CC 0x{cc:02X} (ch {})", ch + 1);
393 ev.value = val.to_string();
394 ev.coalesce = key(&[o, 8, cc as u64]);
395 }
396 }
397 }
398
399 fn apply_short(&mut self, ev: &mut Event, kind: u8, payload: &[u8]) {
400 ev.category = Category::Device;
401 let strings: Vec<String> = payload
402 .split(|b| *b == 0)
403 .filter(|s| !s.is_empty() && s.iter().all(|c| (0x20..0x7F).contains(c)))
404 .map(|s| String::from_utf8_lossy(s).into_owned())
405 .collect();
406 match kind {
407 0x00 => {
408 ev.category = Category::Protocol;
409 ev.subject = "Version request".into();
410 }
411 0x10 if strings.is_empty() => {
412 ev.subject = "Identity ping".into();
413 }
414 0x10 => {
415 self.firmware = strings.first().cloned();
416 self.model_id = strings.get(1).cloned();
417 ev.subject = "Firmware".into();
418 ev.value = strings.join(" · ");
419 }
420 0x60 if payload.is_empty() => {
421 ev.category = Category::Protocol;
422 ev.subject = "Product name request".into();
423 }
424 0x60 => {
425 self.product = strings.first().cloned();
426 ev.subject = "Product".into();
427 ev.value = strings.first().cloned().unwrap_or_default();
428 }
429 _ => {
430 ev.category = Category::Unknown;
431 ev.subject = format!("Short SysEx 0x{kind:02X}");
432 ev.value = hex(payload);
433 }
434 }
435 }
436
437 fn apply_notify(&mut self, ev: &mut Event, what: u8, payload: &[u8]) {
438 match what {
439 0x19 => {
440 if payload.len() >= 6 {
441 self.drum.pattern = Some(payload[1]);
442 self.drum.bpm = Some(u14(payload[4], payload[5]));
443 }
444 ev.category = Category::Drum;
445 ev.subject = "Tempo".into();
446 ev.value = self.drum.bpm.map(|b| format!("{b} BPM")).unwrap_or_default();
447 ev.coalesce = key(&[ev.origin as u64, 9]);
448 }
449 0x0B => {
450 let i = payload.first().copied().unwrap_or(0);
451 ev.category = Category::Preset;
452 ev.subject = "Saved on pedal".into();
453 ev.value = preset_label(i);
454 }
455 0x6F => {
456 ev.category = Category::Tuner;
457 ev.subject = "Tuner".into();
458 ev.value = "toggled on pedal".into();
459 }
460 _ => {
461 ev.category = Category::Unknown;
462 ev.subject = format!("Notify 0x{what:02X} ({})", Cmd::label(what));
463 ev.value = hex(payload);
464 }
465 }
466 }
467
468 fn apply_nep(&mut self, ev: &mut Event, cmd: u8, dir: u8, payload: &[u8]) {
469 ev.category = Category::Protocol;
470 ev.subject = format!("{} {}", Cmd::label(cmd), Dir::label(dir));
471 let data = dir == Dir::Reply as u8 || dir == Dir::Write as u8;
472 let host_write = dir == Dir::Write as u8;
473 match Cmd::from_u8(cmd) {
474 Some(Cmd::Preset) if data => {
475 if let Some(p) = Preset::from_packed(payload) {
476 let i = p.index;
477 ev.value = format!("{} · {}", preset_label(i), p.name);
478 if host_write {
479 ev.category = Category::Preset;
480 ev.subject = "Preset written".into();
481 }
482 let is_current = self.current == Some(i);
483 if let Some(slot) = self.presets.get_mut(i as usize) {
484 *slot = Some(p.clone());
485 }
486 if is_current && (!self.live_known || host_write) {
487 self.live = p;
488 self.live_known = true;
489 }
490 }
491 }
492 Some(Cmd::EditBuffer) if data => {
493 if let Some(mut p) = Preset::from_packed(payload) {
494 if self.current.is_none() {
497 self.current = self
498 .presets
499 .iter()
500 .position(|s| s.as_ref().is_some_and(|s| s.name == p.name && s.same_sound(&p)))
501 .map(|i| i as u8);
502 }
503 p.index = self.current.unwrap_or(0);
504 ev.value = p.name.clone();
505 self.live = p;
506 self.live_known = true;
507 }
508 }
509 Some(Cmd::PresetName) if host_write => {
510 let name = crate::records::ascii(payload.get(1..).unwrap_or_default());
511 ev.category = Category::Preset;
512 ev.subject = "Rename".into();
513 ev.value = format!("“{name}”");
514 self.live.name = name;
515 }
516 Some(Cmd::System) if data => {
517 if let Some(s) = Settings::from_packed(payload) {
518 ev.value = format!(
519 "trim {:+} dB · EQ {}/{}/{} · wireless {} · loopback {}",
520 s.input_trim_db(),
521 s.eq_bass,
522 s.eq_mid,
523 s.eq_treble,
524 if s.wireless { "on" } else { "off" },
525 if s.loopback { "on" } else { "off" },
526 );
527 if host_write {
528 ev.category = Category::Settings;
529 ev.subject = "System settings".into();
530 ev.coalesce = key(&[ev.origin as u64, 10]);
531 }
532 self.settings = Some(s);
533 }
534 }
535 Some(Cmd::IrSlot) if data => {
536 if let Some((slot, name)) = parse_ir_slot(payload) {
537 ev.value = format!("USER IR {:02} · {name}", slot + 1);
538 if host_write {
539 ev.category = Category::Effect(Module::Cab);
540 ev.subject = "User IR slot".into();
541 }
542 if let Some(s) = self.user_irs.get_mut(slot as usize) {
543 *s = Some(name);
544 }
545 }
546 }
547 Some(Cmd::UserAmp) if dir == Dir::Reply as u8 || host_write => {
548 use crate::upload::{Chunk, UserAmpInfo, amp_sub};
549 let chunk = Chunk::parse(payload);
550 if let Some(info) = UserAmpInfo::parse(payload) {
551 ev.value = format!("USER AMP {:02} · {}", info.slot + 1, info.name);
552 if host_write {
553 ev.category = Category::Effect(Module::Amp);
554 ev.subject = "User AMP slot".into();
555 }
556 if let Some(s) = self.user_amps.get_mut(info.slot as usize) {
557 *s = Some(info.name);
558 }
559 } else if let Some(c) = chunk.filter(|c| c.sub == amp_sub::DELETE) {
560 ev.category = Category::Effect(Module::Amp);
561 ev.subject = "User AMP deleted".into();
562 ev.value = format!("USER AMP {:02}", c.slot + 1);
563 } else if host_write {
564 ev.subject = format!("{} upload", Cmd::label(cmd));
565 ev.value = upload_progress(payload);
566 ev.coalesce = key(&[ev.origin as u64, 11, cmd as u64]);
567 }
568 }
569 Some(Cmd::IrData) if host_write => {
570 ev.subject = format!("{} upload", Cmd::label(cmd));
571 ev.value = upload_progress(payload);
572 ev.coalesce = key(&[ev.origin as u64, 11, cmd as u64]);
573 }
574 Some(Cmd::Loop) if data => {
575 if payload.len() >= 4 {
576 self.looper.rec_level = Some(payload[1]);
577 self.looper.play_level = Some(payload[2]);
578 self.looper.set(payload[3]);
579 ev.value = format!(
580 "{} · rec {} · play {}",
581 self.looper.state_name(),
582 payload[1],
583 payload[2]
584 );
585 }
586 }
587 Some(Cmd::Drum) if data => {
588 if payload.len() >= 6 {
589 self.drum.pattern = Some(payload[1]);
590 self.drum.level = Some(payload[2]);
591 self.drum.bpm = Some(u14(payload[4], payload[5]));
592 ev.value = format!(
593 "{} · level {} · {} BPM",
594 drum_pattern_name(payload[1]),
595 payload[2],
596 u14(payload[4], payload[5])
597 );
598 }
599 }
600 Some(Cmd::Tuner) if data => {
601 if let Some(&s) = payload.first() {
602 self.tuner.active = s != 0;
603 ev.value = if s != 0 { "active" } else { "off" }.into();
604 }
605 }
606 _ => {
607 if !payload.is_empty() {
608 let shown = &payload[..payload.len().min(16)];
609 ev.value = hex(shown) + if payload.len() > 16 { " …" } else { "" };
610 }
611 }
612 }
613 }
614}