valentine

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

model.rs (7537B)


      1 //! Device model for the **Scarlett 18i20 3rd Gen** — the config-parameter map and
      2 //! the I/O topology, transcribed from the kernel driver's `gen3c` config set and
      3 //! `s18i20_gen3_info` descriptor. These are wire facts (offsets/sizes/activation
      4 //! codes), the contract the hardware exposes.
      5 //!
      6 //! Most front-panel controls are a `(offset, size, activate)` triple: write the
      7 //! value at `base_offset + channel * byte_width`, then send the `activate` code.
      8 
      9 /// A readable/writable configuration parameter, located by byte `offset` in the
     10 /// device data space. `bits` is the on-wire width (1, 8, 16, or 32); `activate`
     11 /// is the [`crate::protocol::Scarlett::activate`] code that applies a write.
     12 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     13 pub struct Config {
     14     pub offset: u16,
     15     pub bits: u8,
     16     pub activate: u8,
     17 }
     18 
     19 impl Config {
     20     /// Width in bytes for a `set_data` write (bit-sized params write one byte).
     21     pub fn byte_width(&self) -> u32 {
     22         match self.bits {
     23             1 => 1,
     24             n => (n as u32) / 8,
     25         }
     26     }
     27 
     28     /// Byte offset of element `channel` in a multi-channel parameter (air per
     29     /// input, line-out volume per output, …). Bit-packed params (`bits == 1`,
     30     /// e.g. phantom) keep the base offset; the caller addresses the group.
     31     pub fn channel_offset(&self, channel: u32) -> u32 {
     32         if self.bits == 1 {
     33             self.offset as u32 + channel // group index, one byte each here
     34         } else {
     35             self.offset as u32 + channel * self.byte_width()
     36         }
     37     }
     38 }
     39 
     40 /// The gen3c configuration parameters present on the 18i20 g3. Indices are our
     41 /// own; the driver's enum order is irrelevant once we carry explicit offsets.
     42 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     43 pub enum Param {
     44     DimMute,
     45     LineOutVolume,
     46     MuteSwitch,
     47     SwHwSwitch,
     48     MasterVolume,
     49     LevelSwitch,     // Inst/Line per input (2 inputs)
     50     PadSwitch,       // 10 dB pad per input (8 inputs)
     51     AirSwitch,       // Air per input (8 inputs)
     52     StandaloneSwitch,
     53     PhantomSwitch,   // 48V per phantom group (2 groups, 4 inputs each)
     54     MsdSwitch,
     55     PhantomPersistence,
     56     MonitorOtherSwitch,
     57     MonitorOtherEnable,
     58     TalkbackMap,
     59     SpdifMode,
     60 }
     61 
     62 impl Param {
     63     /// The `gen3c` config entry for this parameter (offsets confirmed against
     64     /// the kernel driver: `scarlett2_config_set_gen3c`).
     65     pub fn config(self) -> Config {
     66         use Param::*;
     67         let (offset, bits, activate) = match self {
     68             DimMute => (0x31, 8, 2),
     69             LineOutVolume => (0x34, 16, 1),
     70             MuteSwitch => (0x5c, 8, 1),
     71             SwHwSwitch => (0x66, 8, 3),
     72             MasterVolume => (0x76, 16, 0), // read-only (hardware-controlled)
     73             LevelSwitch => (0x7c, 8, 7),
     74             PadSwitch => (0x84, 8, 8),
     75             AirSwitch => (0x8c, 8, 8),
     76             StandaloneSwitch => (0x95, 8, 6),
     77             PhantomSwitch => (0x9c, 1, 8),
     78             MsdSwitch => (0x9d, 8, 6),
     79             PhantomPersistence => (0x9e, 8, 6),
     80             MonitorOtherSwitch => (0x9f, 1, 10),
     81             MonitorOtherEnable => (0xa0, 1, 10),
     82             TalkbackMap => (0xb0, 16, 10),
     83             SpdifMode => (0x94, 8, 6),
     84         };
     85         Config { offset, bits, activate }
     86     }
     87 }
     88 
     89 /// A category of physical or virtual signal port. `(inputs, outputs)` counts are
     90 /// from the device descriptor — "inputs" are sources into the routing matrix,
     91 /// "outputs" are sinks out of it.
     92 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
     93 pub enum PortType {
     94     Analogue,
     95     Spdif,
     96     Adat,
     97     /// The internal mixer matrix.
     98     Mix,
     99     /// DAW playback/capture channels.
    100     Pcm,
    101 }
    102 
    103 /// Static topology of the Scarlett 18i20 3rd Gen.
    104 #[derive(Debug, Clone, Copy)]
    105 pub struct DeviceInfo {
    106     pub name: &'static str,
    107     pub vid: u16,
    108     pub pid: u16,
    109     /// Inputs that support Inst/Line switching.
    110     pub level_input_count: u8,
    111     /// Inputs that support the 10 dB pad.
    112     pub pad_input_count: u8,
    113     /// Inputs that support Air.
    114     pub air_input_count: u8,
    115     /// Number of independent 48V phantom groups.
    116     pub phantom_count: u8,
    117     /// Physical inputs fed by each phantom group.
    118     pub inputs_per_phantom: u8,
    119     pub has_talkback: bool,
    120     pub has_speaker_switching: bool,
    121     /// Total metering points returned by GET_METER (sum of the device's
    122     /// `meter_map` spans: 8 + 10 + 20 + 2 + 25 on the 18i20 g3).
    123     pub meter_count: u16,
    124 }
    125 
    126 impl DeviceInfo {
    127     /// Number of mixer inputs (signals that can feed a mix bus) — the MIX port
    128     /// type's sink count.
    129     pub fn mixer_inputs(&self) -> u16 {
    130         self.port_count(PortType::Mix).1 as u16
    131     }
    132 
    133     /// Number of mix buses (mixer outputs) — the MIX port type's source count.
    134     pub fn mix_buses(&self) -> u16 {
    135         self.port_count(PortType::Mix).0 as u16
    136     }
    137 
    138     /// Total routing destinations (sinks) = sum of every port type's sink count.
    139     /// Used as the `count` for reading the full mux/routing table.
    140     pub fn mux_dst_count(&self) -> usize {
    141         [
    142             PortType::Analogue,
    143             PortType::Spdif,
    144             PortType::Adat,
    145             PortType::Mix,
    146             PortType::Pcm,
    147         ]
    148         .iter()
    149         .map(|&t| self.port_count(t).1 as usize)
    150         .sum()
    151     }
    152 
    153     /// `(sources_into_matrix, sinks_out_of_matrix)` for a port type, transcribed
    154     /// from the kernel's `s18i20_gen3_info.port_count[type][IN/OUT]`. Note the 9th
    155     /// analogue *source* is the talkback mic.
    156     pub fn port_count(&self, ty: PortType) -> (u8, u8) {
    157         match ty {
    158             PortType::Analogue => (9, 10),
    159             PortType::Spdif => (2, 2),
    160             PortType::Adat => (8, 8),
    161             PortType::Mix => (12, 25),
    162             PortType::Pcm => (20, 20),
    163         }
    164     }
    165 }
    166 
    167 /// The Scarlett 18i20 3rd Gen.
    168 pub const S18I20_GEN3: DeviceInfo = DeviceInfo {
    169     name: "Scarlett 18i20 3rd Gen",
    170     vid: 0x1235,
    171     pid: 0x8215,
    172     level_input_count: 2,
    173     pad_input_count: 8,
    174     air_input_count: 8,
    175     phantom_count: 2,
    176     inputs_per_phantom: 4,
    177     has_talkback: true,
    178     has_speaker_switching: true,
    179     meter_count: 65,
    180 };
    181 
    182 #[cfg(test)]
    183 mod tests {
    184     use super::*;
    185 
    186     #[test]
    187     fn air_offsets_step_one_byte_per_input() {
    188         let c = Param::AirSwitch.config();
    189         assert_eq!(c.offset, 0x8c);
    190         assert_eq!(c.byte_width(), 1);
    191         assert_eq!(c.channel_offset(0), 0x8c);
    192         assert_eq!(c.channel_offset(7), 0x8c + 7); // 8th input
    193     }
    194 
    195     #[test]
    196     fn line_out_volume_is_16bit_two_bytes_per_channel() {
    197         let c = Param::LineOutVolume.config();
    198         assert_eq!(c.bits, 16);
    199         assert_eq!(c.byte_width(), 2);
    200         assert_eq!(c.channel_offset(0), 0x34);
    201         assert_eq!(c.channel_offset(1), 0x36); // +2 bytes
    202         assert_eq!(c.activate, 1);
    203     }
    204 
    205     #[test]
    206     fn phantom_is_bit_sized_per_group() {
    207         let c = Param::PhantomSwitch.config();
    208         assert_eq!(c.bits, 1);
    209         assert_eq!(c.byte_width(), 1);
    210         assert_eq!(c.channel_offset(0), 0x9c); // group 0
    211         assert_eq!(c.channel_offset(1), 0x9d); // group 1
    212         assert_eq!(c.activate, 8);
    213     }
    214 
    215     #[test]
    216     fn descriptor_matches_18i20_topology() {
    217         let d = S18I20_GEN3;
    218         assert_eq!(d.port_count(PortType::Analogue), (9, 10)); // 9th = talkback
    219         assert_eq!(d.port_count(PortType::Pcm), (20, 20));
    220         assert_eq!(d.air_input_count, 8);
    221         assert_eq!(d.phantom_count, 2);
    222         assert_eq!(d.meter_count, 65);
    223         assert!(d.has_talkback);
    224     }
    225 }