Skip to main content

open_stomp/
lib.rs

1//! A library for the SORAN Dual Stomp (Lite) guitar multi-effects pedal over USB-MIDI.
2//!
3//! The protocol was reverse engineered from the vendor's *Stomp Manager* app; `docs/` in the
4//! repository describes it in full, and `docs/10-library-guide.md` is the guide to this crate.
5//! The crate has two layers:
6//!
7//! * **Protocol** (always available, no I/O, no dependencies): [`pack`] (8-in-7 bitstream),
8//!   [`midi`] (message parsing, SysEx framing, stream reassembly, request and command builders),
9//!   [`catalog`] (modules, models, parameters), [`records`] (preset and settings records),
10//!   [`state`] (a model of the pedal that every message is applied to), [`queue`] (the
11//!   one-request-at-a-time pacing the pedal needs), [`upload`] (user AMP/IR upload framing) and
12//!   [`capture`] (capture log reader).
13//! * **NAM** (feature `nam`, on by default): [`nam`] loads and runs Neural Amp Modeler `.nam`
14//!   models and converts them into the pedal's user AMP format, as the official app does.
15//! * **Device** (feature `device`, on by default): [`device`] runs a background engine that finds
16//!   the pedal, connects, performs the startup sync, sends edits and reports everything that
17//!   happens, or replays a capture log instead of a real pedal.
18//!
19//! ```no_run
20//! # #[cfg(feature = "device")] {
21//! use open_stomp::device::{Config, Device, Update};
22//! use open_stomp::DeviceState;
23//!
24//! let dev = Device::spawn(Config::default()); // finds and connects to the pedal by itself
25//! let mut state = DeviceState::default();
26//! while let Ok(update) = dev.recv() {
27//!     match update {
28//!         Update::Reset => state = DeviceState::default(),
29//!         Update::Message { origin, bytes, .. } => {
30//!             let ev = state.apply(&bytes, origin);
31//!             println!("{:8} {} = {}", ev.category.label(), ev.subject, ev.value);
32//!         }
33//!         _ => {}
34//!     }
35//! }
36//! // and to control it: dev.select_preset(4); dev.set_param(Module::Amp, 0, 100); ...
37//! # }
38//! ```
39//!
40//! Tested against the Dual Stomp Lite on firmware V1.1.1. The capture logs in `captures/logs/`
41//! are the test vectors.
42
43#[cfg(feature = "ble")]
44mod ble;
45pub mod blemidi;
46pub mod capture;
47pub mod catalog;
48pub mod ir;
49#[cfg(feature = "device")]
50pub mod device;
51pub mod midi;
52#[cfg(feature = "nam")]
53pub mod nam;
54pub mod pack;
55pub mod queue;
56pub mod records;
57pub mod state;
58pub mod upload;
59
60pub use catalog::Module;
61pub use midi::{Assembler, Msg};
62pub use records::{Preset, Settings};
63pub use state::{Category, DeviceState, Event, Origin};
64
65/// USB vendor and product id of the pedal (`3962:2000`, "JME STOMPBOX").
66pub const USB_ID: (u16, u16) = (0x3962, 0x2000);
67
68/// Whether a MIDI port name belongs to the pedal. Matches what the official app matches
69/// (`.*JME.*`, `.*SORAN AMPEY.*`) plus the product name, ignoring case.
70pub fn is_stomp_port(name: &str) -> bool {
71    let n = name.to_ascii_uppercase();
72    n.contains("JME") || n.contains("SORAN") || n.contains("STOMPBOX")
73}
74
75/// Suffix of the port name the engine gives a pedal found over Bluetooth LE MIDI (feature `ble`):
76/// `"JME STOMPBOX (Bluetooth)"`.
77pub const BLE_PORT_SUFFIX: &str = " (Bluetooth)";
78
79/// Whether a port name is a pedal found over Bluetooth (see [`BLE_PORT_SUFFIX`]).
80pub fn is_ble_port(name: &str) -> bool {
81    name.ends_with(BLE_PORT_SUFFIX)
82}
83
84#[cfg(test)]
85mod tests {
86    #[test]
87    fn port_names() {
88        for n in ["JME STOMPBOX MIDI 1", "JME STOMPBOX:JME STOMPBOX MIDI 1 24:0", "jme stompbox"] {
89            assert!(super::is_stomp_port(n), "{n}");
90        }
91        assert!(!super::is_stomp_port("IAC Driver Bus 1"));
92        assert!(super::is_stomp_port("JME STOMPBOX (Bluetooth)") && super::is_ble_port("JME STOMPBOX (Bluetooth)"));
93        assert!(!super::is_ble_port("JME STOMPBOX MIDI 1"));
94    }
95}