valentine

Terminal control panel for the Focusrite Scarlett 18i20 — a from-scratch replacement for Focusrite Control.
Log | Files | Refs | README | LICENSE

preset.rs (7075B)


      1 //! Presets — capture the device's user-settable state to a serializable snapshot,
      2 //! and apply one back. This powers "save/load a configuration" in the TUI and is
      3 //! independent of the device's own standalone (NVRAM) save.
      4 //!
      5 //! What's captured: the per-input switches (Air / Pad / Inst), the 48 V phantom
      6 //! groups, the monitor Mute / Dim, and the full mixer matrix. **Not** captured:
      7 //! the master monitor level (it's read-only — owned by the hardware knob) and
      8 //! routing (edit path isn't validated yet). Applying only writes fields present,
      9 //! so older/newer presets degrade gracefully.
     10 
     11 use serde::{Deserialize, Serialize};
     12 
     13 use crate::controls::{InputState, InputSwitch, MonitorButton, MonitorState};
     14 use crate::matrix::db_to_mixer_value;
     15 use crate::protocol::Scarlett;
     16 use crate::transport::{Transport, TransportError};
     17 
     18 /// Snapshot format version, so future readers can adapt.
     19 pub const PRESET_VERSION: u32 = 1;
     20 
     21 /// A saved device configuration.
     22 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
     23 pub struct Preset {
     24     pub version: u32,
     25     /// Free-text label shown in the UI.
     26     pub name: String,
     27     /// Device product id this was captured from (sanity check on load).
     28     pub pid: u16,
     29     pub air: Vec<bool>,
     30     pub pad: Vec<bool>,
     31     pub inst: Vec<bool>,
     32     pub phantom: Vec<bool>,
     33     pub mute: bool,
     34     pub dim: bool,
     35     /// Mixer matrix as `[bus][input]` in dB.
     36     pub mixer: Vec<Vec<f32>>,
     37     /// Full routing as a flat `mux[dest_num] = source_num` array (port numbers).
     38     /// Empty in older presets; when present, applying restores routing too.
     39     #[serde(default)]
     40     pub routing: Vec<u16>,
     41 }
     42 
     43 impl Preset {
     44     /// Build a preset from already-read state (no device I/O). `routing` is the
     45     /// flat `mux[dest]=src` port-number array (empty = don't capture routing).
     46     pub fn from_state(
     47         name: impl Into<String>,
     48         pid: u16,
     49         inputs: &InputState,
     50         monitor: &MonitorState,
     51         mixer: &[Vec<f32>],
     52         routing: &[u16],
     53     ) -> Self {
     54         Preset {
     55             version: PRESET_VERSION,
     56             name: name.into(),
     57             pid,
     58             air: inputs.air.clone(),
     59             pad: inputs.pad.clone(),
     60             inst: inputs.inst.clone(),
     61             phantom: inputs.phantom.clone(),
     62             mute: monitor.mute,
     63             dim: monitor.dim,
     64             mixer: mixer.to_vec(),
     65             routing: routing.to_vec(),
     66         }
     67     }
     68 
     69     /// Serialize to pretty JSON (human-editable on disk).
     70     pub fn to_json(&self) -> String {
     71         serde_json::to_string_pretty(self).unwrap_or_default()
     72     }
     73 
     74     /// Parse from JSON.
     75     pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
     76         serde_json::from_str(s)
     77     }
     78 }
     79 
     80 impl<T: Transport> Scarlett<T> {
     81     /// Apply a preset to the device. Writes each switch + activates, sets phantom
     82     /// groups, monitor mute/dim, and every mixer crosspoint. Returns the number
     83     /// of device writes performed (useful for a status line).
     84     pub fn apply_preset(&mut self, p: &Preset) -> Result<usize, TransportError> {
     85         let mut writes = 0;
     86 
     87         for (i, &on) in p.air.iter().enumerate() {
     88             self.set_input_switch(InputSwitch::Air, i as u8, on)?;
     89             writes += 1;
     90         }
     91         for (i, &on) in p.pad.iter().enumerate() {
     92             self.set_input_switch(InputSwitch::Pad, i as u8, on)?;
     93             writes += 1;
     94         }
     95         for (i, &on) in p.inst.iter().enumerate() {
     96             self.set_input_switch(InputSwitch::Inst, i as u8, on)?;
     97             writes += 1;
     98         }
     99         for (g, &on) in p.phantom.iter().enumerate() {
    100             self.set_phantom(g as u8, on)?;
    101             writes += 1;
    102         }
    103 
    104         self.set_monitor_button(MonitorButton::Mute, p.mute)?;
    105         self.set_monitor_button(MonitorButton::Dim, p.dim)?;
    106         writes += 2;
    107 
    108         for (bus, inputs) in p.mixer.iter().enumerate() {
    109             let levels: Vec<u16> = inputs.iter().map(|&db| db_to_mixer_value(db)).collect();
    110             if !levels.is_empty() {
    111                 self.set_mix(bus as u16, &levels)?;
    112                 writes += 1;
    113             }
    114         }
    115 
    116         // Routing last, as one atomic write (the big visible change).
    117         if !p.routing.is_empty() {
    118             use crate::mux::{mux_assignment_18i20_gen3, MuxState, PORT_COUNT_18I20_GEN3};
    119             let pc = PORT_COUNT_18I20_GEN3;
    120             let state = MuxState { pc, mux: p.routing.clone() };
    121             let tables = state.encode_all(&mux_assignment_18i20_gen3());
    122             self.write_routing_tables(&tables)?;
    123             writes += 1;
    124         }
    125 
    126         Ok(writes)
    127     }
    128 }
    129 
    130 #[cfg(test)]
    131 mod tests {
    132     use super::*;
    133     use crate::protocol::op;
    134     use crate::transport::mock::MockTransport;
    135 
    136     fn sample() -> Preset {
    137         Preset {
    138             version: PRESET_VERSION,
    139             name: "Vocal chain".into(),
    140             pid: 0x8215,
    141             air: vec![true, false],
    142             pad: vec![false],
    143             inst: vec![true],
    144             phantom: vec![true, false],
    145             mute: false,
    146             dim: true,
    147             mixer: vec![vec![0.0, -6.0]],
    148             routing: vec![],
    149         }
    150     }
    151 
    152     #[test]
    153     fn json_round_trips() {
    154         let p = sample();
    155         let json = p.to_json();
    156         let back = Preset::from_json(&json).unwrap();
    157         assert_eq!(p, back);
    158         assert!(json.contains("\"name\": \"Vocal chain\""));
    159     }
    160 
    161     #[test]
    162     fn from_state_captures_fields() {
    163         let inputs = InputState {
    164             air: vec![true, true],
    165             pad: vec![false, false],
    166             inst: vec![false, true],
    167             phantom: vec![true, false],
    168         };
    169         let mon = MonitorState { master_db: -3, mute: true, dim: false };
    170         let mixer = vec![vec![0.0, 0.0]];
    171         let p = Preset::from_state("test", 0x8215, &inputs, &mon, &mixer, &[]);
    172         assert_eq!(p.air, vec![true, true]);
    173         assert_eq!(p.inst, vec![false, true]);
    174         assert!(p.mute);
    175         assert_eq!(p.pid, 0x8215);
    176         // master volume is intentionally not stored (read-only hardware knob)
    177     }
    178 
    179     #[test]
    180     fn apply_preset_writes_and_activates() {
    181         // apply_preset interleaves set_data/activate/set_mix; an echoing mock
    182         // returns a matching empty success for each, so we just assert the count
    183         // of high-level writes and that the right commands were emitted.
    184         let mut dev = Scarlett::new(MockTransport::echoing());
    185         let writes = dev.apply_preset(&sample()).unwrap();
    186         // 2 air + 1 pad + 1 inst + 2 phantom + 2 monitor + 1 mixer bus = 9
    187         assert_eq!(writes, 9);
    188 
    189         let m = dev.into_transport();
    190         let set_mix = m.sent.iter().filter(|(c, _)| *c == op::SET_MIX).count();
    191         let set_data = m.sent.iter().filter(|(c, _)| *c == op::SET_DATA).count();
    192         assert_eq!(set_mix, 1); // one mixer bus
    193         // SET_DATA = 2 air + 1 pad + 1 inst + 2 phantom + 2 monitor(mute,dim) = 8
    194         // (phantom and monitor buttons are byte writes via set_data too)
    195         assert_eq!(set_data, 8);
    196     }
    197 }