valentine

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

controls.rs (10299B)


      1 //! High-level, named controls built on the config-offset map ([`crate::model`])
      2 //! and the raw primitives ([`crate::protocol`]). This is the layer the TUI talks
      3 //! to: "turn on Air for input 3" rather than "write 1 at offset 0x8f, activate 8".
      4 //!
      5 //! Every setter writes the value then sends the parameter's activation code, so
      6 //! the change takes effect immediately (the device's two-step set/activate).
      7 
      8 use crate::model::{Param, S18I20_GEN3};
      9 use crate::protocol::Scarlett;
     10 use crate::transport::{Transport, TransportError};
     11 
     12 /// The device stores signed monitor volume in dB as `dB + VOLUME_BIAS`, giving
     13 /// a 0..=127 range where 127 = 0 dB (unity) and 0 = -127 dB (≈ off).
     14 pub const VOLUME_BIAS: i16 = 127;
     15 
     16 /// The two monitor hardware buttons, in device index order (`DIM_MUTE` config).
     17 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     18 pub enum MonitorButton {
     19     Mute = 0,
     20     Dim = 1,
     21 }
     22 
     23 /// The per-channel boolean switches on the input strip.
     24 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     25 pub enum InputSwitch {
     26     /// Air (ISA-preamp emulation). Per input (8).
     27     Air,
     28     /// 10 dB pad. Per input (8).
     29     Pad,
     30     /// Inst (vs Line) level/impedance. Per input (first 2).
     31     Inst,
     32 }
     33 
     34 impl InputSwitch {
     35     fn param(self) -> Param {
     36         match self {
     37             InputSwitch::Air => Param::AirSwitch,
     38             InputSwitch::Pad => Param::PadSwitch,
     39             InputSwitch::Inst => Param::LevelSwitch,
     40         }
     41     }
     42 
     43     /// How many input channels this switch applies to on the 18i20 g3.
     44     pub fn channel_count(self) -> u8 {
     45         match self {
     46             InputSwitch::Air => S18I20_GEN3.air_input_count,
     47             InputSwitch::Pad => S18I20_GEN3.pad_input_count,
     48             InputSwitch::Inst => S18I20_GEN3.level_input_count,
     49         }
     50     }
     51 }
     52 
     53 /// A snapshot of the monitor/output section.
     54 #[derive(Debug, Clone, Default, PartialEq, Eq)]
     55 pub struct MonitorState {
     56     /// Master monitor level in dB (0 = unity).
     57     pub master_db: i16,
     58     pub mute: bool,
     59     pub dim: bool,
     60 }
     61 
     62 /// A snapshot of the whole input strip, as read from the device.
     63 #[derive(Debug, Clone, Default, PartialEq, Eq)]
     64 pub struct InputState {
     65     pub air: Vec<bool>,
     66     pub pad: Vec<bool>,
     67     pub inst: Vec<bool>,
     68     /// Per phantom *group* (2 groups of 4 inputs each on the 18i20 g3).
     69     pub phantom: Vec<bool>,
     70 }
     71 
     72 impl<T: Transport> Scarlett<T> {
     73     /// Read a single byte-sized switch for `channel` (0-based).
     74     pub fn get_input_switch(
     75         &mut self,
     76         switch: InputSwitch,
     77         channel: u8,
     78     ) -> Result<bool, TransportError> {
     79         let cfg = switch.param().config();
     80         let off = cfg.channel_offset(channel as u32);
     81         let data = self.get_data(off, cfg.byte_width())?;
     82         Ok(data.first().is_some_and(|&b| b != 0))
     83     }
     84 
     85     /// Set a byte-sized switch for `channel`, then activate it.
     86     pub fn set_input_switch(
     87         &mut self,
     88         switch: InputSwitch,
     89         channel: u8,
     90         on: bool,
     91     ) -> Result<(), TransportError> {
     92         let cfg = switch.param().config();
     93         let off = cfg.channel_offset(channel as u32);
     94         self.set_data(off, cfg.byte_width(), on as u32)?;
     95         self.activate(cfg.activate as u32)
     96     }
     97 
     98     /// Read a 48V phantom *group* (0-based). Phantom is bit-sized, one byte per
     99     /// group on this model.
    100     pub fn get_phantom(&mut self, group: u8) -> Result<bool, TransportError> {
    101         let cfg = Param::PhantomSwitch.config();
    102         let off = cfg.channel_offset(group as u32);
    103         let data = self.get_data(off, 1)?;
    104         Ok(data.first().is_some_and(|&b| b != 0))
    105     }
    106 
    107     /// Set a 48V phantom group, then activate it.
    108     pub fn set_phantom(&mut self, group: u8, on: bool) -> Result<(), TransportError> {
    109         let cfg = Param::PhantomSwitch.config();
    110         let off = cfg.channel_offset(group as u32);
    111         self.set_data(off, 1, on as u32)?;
    112         self.activate(cfg.activate as u32)
    113     }
    114 
    115     // ---- Monitor / output section -------------------------------------------
    116 
    117     /// Read the master monitor volume as dB (signed; 0 = unity, negative = quieter).
    118     /// `MASTER_VOLUME` is read-only — it tracks the hardware monitor knob.
    119     pub fn get_master_volume_db(&mut self) -> Result<i16, TransportError> {
    120         let cfg = Param::MasterVolume.config();
    121         let data = self.get_data(cfg.offset as u32, 2)?;
    122         let raw = i16::from_le_bytes([
    123             *data.first().unwrap_or(&0),
    124             *data.get(1).unwrap_or(&0),
    125         ]);
    126         Ok(raw)
    127     }
    128 
    129     /// Read a monitor button (Mute or Dim) state.
    130     pub fn get_monitor_button(&mut self, btn: MonitorButton) -> Result<bool, TransportError> {
    131         let cfg = Param::DimMute.config();
    132         let off = cfg.offset as u32 + btn as u32;
    133         let data = self.get_data(off, 1)?;
    134         Ok(data.first().is_some_and(|&b| b != 0))
    135     }
    136 
    137     /// Set a monitor button (Mute or Dim), then activate it.
    138     pub fn set_monitor_button(
    139         &mut self,
    140         btn: MonitorButton,
    141         on: bool,
    142     ) -> Result<(), TransportError> {
    143         let cfg = Param::DimMute.config();
    144         let off = cfg.offset as u32 + btn as u32;
    145         self.set_data(off, 1, on as u32)?;
    146         self.activate(cfg.activate as u32)
    147     }
    148 
    149     /// A snapshot of the monitor section for a UI refresh.
    150     pub fn read_monitor_state(&mut self) -> Result<MonitorState, TransportError> {
    151         Ok(MonitorState {
    152             master_db: self.get_master_volume_db().unwrap_or(0),
    153             mute: self.get_monitor_button(MonitorButton::Mute).unwrap_or(false),
    154             dim: self.get_monitor_button(MonitorButton::Dim).unwrap_or(false),
    155         })
    156     }
    157 
    158     /// Read the entire input strip in one call (for a UI refresh).
    159     pub fn read_input_state(&mut self) -> Result<InputState, TransportError> {
    160         let mut s = InputState::default();
    161         for ch in 0..InputSwitch::Air.channel_count() {
    162             s.air.push(self.get_input_switch(InputSwitch::Air, ch)?);
    163         }
    164         for ch in 0..InputSwitch::Pad.channel_count() {
    165             s.pad.push(self.get_input_switch(InputSwitch::Pad, ch)?);
    166         }
    167         for ch in 0..InputSwitch::Inst.channel_count() {
    168             s.inst.push(self.get_input_switch(InputSwitch::Inst, ch)?);
    169         }
    170         for g in 0..S18I20_GEN3.phantom_count {
    171             s.phantom.push(self.get_phantom(g)?);
    172         }
    173         Ok(s)
    174     }
    175 }
    176 
    177 #[cfg(test)]
    178 mod tests {
    179     use super::*;
    180     use crate::protocol::op;
    181     use crate::transport::mock::MockTransport;
    182 
    183     #[test]
    184     fn set_air_writes_offset_then_activates() {
    185         let mut m = MockTransport::new();
    186         m.push_response(op::SET_DATA, &[]); // the write
    187         m.push_response(op::DATA_CMD, &[]); // the activate
    188         let mut dev = Scarlett::new(m);
    189 
    190         // Air on input 2 (0-based) -> offset 0x8c + 2 = 0x8e, value 1, activate 8.
    191         dev.set_input_switch(InputSwitch::Air, 2, true).unwrap();
    192 
    193         let m = dev.into_transport();
    194         assert_eq!(m.sent[0].0, op::SET_DATA);
    195         // offset 0x8e LE, size 1 LE, value 1
    196         assert_eq!(m.sent[0].1, vec![0x8e, 0, 0, 0, 1, 0, 0, 0, 1]);
    197         assert_eq!(m.sent[1], (op::DATA_CMD, 8u32.to_le_bytes().to_vec()));
    198     }
    199 
    200     #[test]
    201     fn get_air_reads_byte_as_bool() {
    202         let mut m = MockTransport::new();
    203         m.push_response(op::GET_DATA, &[1]);
    204         let mut dev = Scarlett::new(m);
    205         assert!(dev.get_input_switch(InputSwitch::Air, 0).unwrap());
    206 
    207         let mut m2 = MockTransport::new();
    208         m2.push_response(op::GET_DATA, &[0]);
    209         let mut dev2 = Scarlett::new(m2);
    210         assert!(!dev2.get_input_switch(InputSwitch::Air, 0).unwrap());
    211     }
    212 
    213     #[test]
    214     fn phantom_group_offsets_and_activate() {
    215         let mut m = MockTransport::new();
    216         m.push_response(op::SET_DATA, &[]);
    217         m.push_response(op::DATA_CMD, &[]);
    218         let mut dev = Scarlett::new(m);
    219 
    220         // group 1 -> offset 0x9c + 1 = 0x9d, activate 8
    221         dev.set_phantom(1, true).unwrap();
    222 
    223         let m = dev.into_transport();
    224         assert_eq!(m.sent[0].1, vec![0x9d, 0, 0, 0, 1, 0, 0, 0, 1]);
    225         assert_eq!(m.sent[1], (op::DATA_CMD, 8u32.to_le_bytes().to_vec()));
    226     }
    227 
    228     #[test]
    229     fn monitor_button_offsets_and_activate() {
    230         let mut m = MockTransport::new();
    231         m.push_response(op::SET_DATA, &[]);
    232         m.push_response(op::DATA_CMD, &[]);
    233         let mut dev = Scarlett::new(m);
    234 
    235         // Dim = index 1 -> offset 0x31 + 1 = 0x32, activate 2
    236         dev.set_monitor_button(MonitorButton::Dim, true).unwrap();
    237 
    238         let m = dev.into_transport();
    239         assert_eq!(m.sent[0].1, vec![0x32, 0, 0, 0, 1, 0, 0, 0, 1]);
    240         assert_eq!(m.sent[1], (op::DATA_CMD, 2u32.to_le_bytes().to_vec()));
    241     }
    242 
    243     #[test]
    244     fn master_volume_reads_signed_db() {
    245         let mut m = MockTransport::new();
    246         // -6 dB as i16 LE
    247         m.push_response(op::GET_DATA, &(-6i16).to_le_bytes());
    248         let mut dev = Scarlett::new(m);
    249         assert_eq!(dev.get_master_volume_db().unwrap(), -6);
    250     }
    251 
    252     #[test]
    253     fn read_monitor_state_collects_vol_mute_dim() {
    254         let mut m = MockTransport::new();
    255         m.push_response(op::GET_DATA, &0i16.to_le_bytes()); // master 0 dB
    256         m.push_response(op::GET_DATA, &[1]); // mute on
    257         m.push_response(op::GET_DATA, &[0]); // dim off
    258         let mut dev = Scarlett::new(m);
    259         let s = dev.read_monitor_state().unwrap();
    260         assert_eq!(s.master_db, 0);
    261         assert!(s.mute);
    262         assert!(!s.dim);
    263     }
    264 
    265     #[test]
    266     fn read_input_state_fills_all_channels() {
    267         let mut m = MockTransport::new();
    268         // 8 air + 8 pad + 2 inst + 2 phantom = 20 GET_DATA responses
    269         for _ in 0..8 {
    270             m.push_response(op::GET_DATA, &[0]);
    271         }
    272         for _ in 0..8 {
    273             m.push_response(op::GET_DATA, &[1]); // all pads on
    274         }
    275         for _ in 0..2 {
    276             m.push_response(op::GET_DATA, &[0]);
    277         }
    278         for _ in 0..2 {
    279             m.push_response(op::GET_DATA, &[1]); // both phantom groups on
    280         }
    281         let mut dev = Scarlett::new(m);
    282 
    283         let s = dev.read_input_state().unwrap();
    284         assert_eq!(s.air.len(), 8);
    285         assert_eq!(s.pad, vec![true; 8]);
    286         assert_eq!(s.inst, vec![false, false]);
    287         assert_eq!(s.phantom, vec![true, true]);
    288     }
    289 }