valentine

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

matrix.rs (23136B)


      1 //! The mixer matrix, routing matrix (mux), and metering — the three parts of the
      2 //! device that aren't simple config offsets. Request/response framing transcribed
      3 //! from the kernel driver and confirmed against the gen3 device.
      4 //!
      5 //! - **Mixer**  (`GET_MIX`/`SET_MIX`): per mix bus, a `u16` gain for each input.
      6 //! - **Routing** (`GET_MUX`/`SET_MUX`): a flat list of `u32` entries, each packing
      7 //!   `destination | (source << 12)` — destination in the low 12 bits, source in
      8 //!   the high 12 (confirmed against the kernel's `populate_mux`/`set_mux`).
      9 //! - **Meters** (`GET_METER`): a snapshot of `u32` levels, one per metering point.
     10 
     11 use crate::protocol::{op, Scarlett};
     12 use crate::transport::{Transport, TransportError};
     13 
     14 /// Mixer crosspoint gain range, in dB (−80 … +12, matching the device).
     15 pub const MIXER_MIN_DB: f32 = -80.0;
     16 pub const MIXER_MAX_DB: f32 = 12.0;
     17 /// Raw mixer value that means 0 dB (unity). The device's curve is
     18 /// `value = 8192 · 10^(dB/20)` (confirmed from the kernel's generating formula).
     19 const MIXER_UNITY: f32 = 8192.0;
     20 
     21 /// Convert a raw mixer value (as returned by [`Scarlett::get_mix`]) to dB,
     22 /// clamped to the device's range. 0 → silence floor (`MIXER_MIN_DB`).
     23 pub fn mixer_value_to_db(value: u16) -> f32 {
     24     if value == 0 {
     25         return MIXER_MIN_DB;
     26     }
     27     (20.0 * (value as f32 / MIXER_UNITY).log10()).clamp(MIXER_MIN_DB, MIXER_MAX_DB)
     28 }
     29 
     30 /// Convert a dB gain to the raw mixer value for [`Scarlett::set_mix`].
     31 pub fn db_to_mixer_value(db: f32) -> u16 {
     32     let db = db.clamp(MIXER_MIN_DB, MIXER_MAX_DB);
     33     (MIXER_UNITY * 10f32.powf(db / 20.0)) as u16
     34 }
     35 
     36 /// Bit shift separating source (high bits) from destination (low bits).
     37 const MUX_SRC_SHIFT: u32 = 12;
     38 /// Mask for the destination field (low 12 bits).
     39 const MUX_DST_MASK: u32 = (1 << MUX_SRC_SHIFT) - 1;
     40 
     41 /// One routing assignment: which `source` feeds which `dest` (sink). Both are
     42 /// device-internal hardware port IDs.
     43 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     44 pub struct MuxEntry {
     45     pub source: u16,
     46     pub dest: u16,
     47 }
     48 
     49 impl MuxEntry {
     50     /// Pack into the device's `dest | (source << 12)` u32.
     51     pub fn pack(self) -> u32 {
     52         (self.dest as u32 & MUX_DST_MASK) | ((self.source as u32) << MUX_SRC_SHIFT)
     53     }
     54 
     55     /// Unpack a device u32 into source/dest.
     56     pub fn unpack(raw: u32) -> Self {
     57         MuxEntry {
     58             dest: (raw & MUX_DST_MASK) as u16,
     59             source: (raw >> MUX_SRC_SHIFT) as u16,
     60         }
     61     }
     62 }
     63 
     64 /// A set of output channels that can be monitored "via the mixer" so a software
     65 /// fader controls their level. Each channel `i` (0..count) uses output hardware
     66 /// id `out_id_base+i`, mix bus `bus_base+i`, mixer-input `mix_in_base+i`, fed by
     67 /// PCM `pcm_base+i`. Bases are chosen so groups don't collide on buses/inputs.
     68 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     69 pub struct MonitorGroup {
     70     pub name: &'static str,
     71     pub out_id_base: u16, // hardware id of first output (e.g. 0x080 analogue, 0x200 ADAT)
     72     pub bus_base: u16,    // first mix bus index (0-based)
     73     pub mix_in_base: u16, // first mixer-input index (0-based)
     74     pub pcm_base: u16,    // first PCM source index (0-based; matches the DAW channel)
     75     pub count: u16,       // number of channels
     76 }
     77 
     78 /// The 18i20 g3 monitor groups. Analogue 1-2 on buses 0-1 / mixer-in 0-1 / PCM
     79 /// 1-2; ADAT 1-8 on buses 2-9 / mixer-in 2-9 / PCM 13-20 (the user's ADAT
     80 /// monitoring path). Non-overlapping so both can be active at once (10 of 12
     81 /// buses, 10 of 25 mixer-inputs).
     82 pub const MONITOR_GROUPS: &[MonitorGroup] = &[
     83     MonitorGroup {
     84         name: "Analogue 1-2",
     85         out_id_base: 0x080,
     86         bus_base: 0,
     87         mix_in_base: 0,
     88         pcm_base: 0, // PCM 1-2
     89         count: 2,
     90     },
     91     MonitorGroup {
     92         name: "ADAT 1-8",
     93         out_id_base: 0x200,
     94         bus_base: 2,
     95         mix_in_base: 2,
     96         pcm_base: 12, // PCM 13-20
     97         count: 8,
     98     },
     99 ];
    100 
    101 /// Convenience operations layered on a connected [`Scarlett`].
    102 impl<T: Transport> Scarlett<T> {
    103     /// Read the input gains feeding mix bus `mix_num`. Returns one raw `u16`
    104     /// mixer value per input (`num_inputs`); convert to dB with the device's
    105     /// mixer curve at the UI layer.
    106     pub fn get_mix(&mut self, mix_num: u16, num_inputs: usize) -> Result<Vec<u16>, TransportError> {
    107         // Request is { mix_num: u16, count: u16 } (the kernel sends both; sending
    108         // only mix_num gets device error 0x9).
    109         let mut req = Vec::with_capacity(4);
    110         req.extend_from_slice(&mix_num.to_le_bytes());
    111         req.extend_from_slice(&(num_inputs as u16).to_le_bytes());
    112         let resp = self.command(op::GET_MIX, &req, num_inputs * 2)?;
    113         Ok(le_u16s(&resp.payload, num_inputs))
    114     }
    115 
    116     /// Set every input gain for mix bus `mix_num` at once (raw mixer `u16`s).
    117     pub fn set_mix(&mut self, mix_num: u16, levels: &[u16]) -> Result<(), TransportError> {
    118         let mut payload = Vec::with_capacity(2 + levels.len() * 2);
    119         payload.extend_from_slice(&mix_num.to_le_bytes());
    120         for &l in levels {
    121             payload.extend_from_slice(&l.to_le_bytes());
    122         }
    123         self.command(op::SET_MIX, &payload, 0)?;
    124         Ok(())
    125     }
    126 
    127     /// Route a monitor group's outputs THROUGH the mixer so their level is
    128     /// controllable by a software fader: for each channel `i`,
    129     /// `MixerIn(mix_in_base+i) ← PCM(pcm_base+i)` and `Out(out_hw+i) ← Mix(bus_base+i)`.
    130     /// With [`Self::set_group_level`], the buses' gains are the group's volume.
    131     /// One atomic routing write. See [`MonitorGroup`].
    132     pub fn route_group_via_mixer(
    133         &mut self,
    134         pc: [(u16, u16); 6],
    135         g: &MonitorGroup,
    136     ) -> Result<crate::mux::MuxState, TransportError> {
    137         use crate::mux::{id_to_num, Dir};
    138         let n = |dir, id| id_to_num(&pc, dir, id).unwrap_or(0);
    139         let mut changes = Vec::with_capacity(g.count as usize * 2);
    140         for i in 0..g.count {
    141             // MixerIn(base+i) ← PCM(pcm_base+i)
    142             changes.push((
    143                 n(Dir::Out, 0x300 + g.mix_in_base + i),
    144                 n(Dir::In, 0x600 + g.pcm_base + i),
    145             ));
    146             // Out(base+i) ← Mix(bus_base+i)
    147             changes.push((
    148                 n(Dir::Out, g.out_id_base + i),
    149                 n(Dir::In, 0x300 + g.bus_base + i),
    150             ));
    151         }
    152         self.set_routes(pc, &changes)
    153     }
    154 
    155     /// Route a group's outputs straight from the DAW again
    156     /// (`Out(out_hw+i) ← PCM(pcm_base+i)`), undoing [`Self::route_group_via_mixer`].
    157     pub fn route_group_direct(
    158         &mut self,
    159         pc: [(u16, u16); 6],
    160         g: &MonitorGroup,
    161     ) -> Result<crate::mux::MuxState, TransportError> {
    162         use crate::mux::{id_to_num, Dir};
    163         let n = |dir, id| id_to_num(&pc, dir, id).unwrap_or(0);
    164         let changes: Vec<(u16, u16)> = (0..g.count)
    165             .map(|i| (n(Dir::Out, g.out_id_base + i), n(Dir::In, 0x600 + g.pcm_base + i)))
    166             .collect();
    167         self.set_routes(pc, &changes)
    168     }
    169 
    170     /// Set a group's level (dB): each of the group's buses passes only its own
    171     /// mixer-input at `db`; all other inputs on those buses are silenced for a
    172     /// clean monitor path. `inputs` = mixer input count.
    173     pub fn set_group_level(
    174         &mut self,
    175         g: &MonitorGroup,
    176         db: f32,
    177         inputs: usize,
    178     ) -> Result<(), TransportError> {
    179         let v = db_to_mixer_value(db);
    180         for i in 0..g.count {
    181             let mut bus = vec![0u16; inputs];
    182             let in_idx = (g.mix_in_base + i) as usize;
    183             if in_idx < inputs {
    184                 bus[in_idx] = v;
    185             }
    186             self.set_mix(g.bus_base + i, &bus)?;
    187         }
    188         Ok(())
    189     }
    190 
    191     /// Read a group's current level (dB) — the gain of its first bus on its
    192     /// first mixer-input. Meaningful only when the group is routed via the mixer.
    193     pub fn get_group_level(
    194         &mut self,
    195         g: &MonitorGroup,
    196         inputs: usize,
    197     ) -> Result<f32, TransportError> {
    198         let raw = self.get_mix(g.bus_base, inputs)?;
    199         let idx = g.mix_in_base as usize;
    200         Ok(raw.get(idx).map(|&v| mixer_value_to_db(v)).unwrap_or(MIXER_MIN_DB))
    201     }
    202 
    203     /// True if a group is currently routed via the mixer (its first output is fed
    204     /// by its first mix bus).
    205     pub fn group_is_via_mixer(
    206         &mut self,
    207         pc: [(u16, u16); 6],
    208         g: &MonitorGroup,
    209     ) -> Result<bool, TransportError> {
    210         use crate::mux::{id_to_num, Dir, MuxState};
    211         let entries = self.get_mux(crate::mux::num_dsts(&pc))?;
    212         let st = MuxState::from_entries(pc, &entries);
    213         let out0 = id_to_num(&pc, Dir::Out, g.out_id_base).unwrap_or(0);
    214         let bus0 = id_to_num(&pc, Dir::In, 0x300 + g.bus_base).unwrap_or(0);
    215         Ok(st.get(out0) == bus0)
    216     }
    217 
    218     /// Read the full routing table: `count` destination assignments.
    219     pub fn get_mux(&mut self, count: usize) -> Result<Vec<MuxEntry>, TransportError> {
    220         let mut payload = Vec::with_capacity(4);
    221         payload.extend_from_slice(&0u16.to_le_bytes()); // num (start index)
    222         payload.extend_from_slice(&(count as u16).to_le_bytes());
    223         let resp = self.command(op::GET_MUX, &payload, count * 4)?;
    224         Ok(le_u32s(&resp.payload, count)
    225             .into_iter()
    226             .map(MuxEntry::unpack)
    227             .collect())
    228     }
    229 
    230     /// Read the routing table and decode each entry to `(sink_name, source_name)`
    231     /// using [`crate::ports`]. `count` is the number of destinations to read.
    232     ///
    233     /// NOTE: read-only/display use. Editing routing needs the kernel's 3-table
    234     /// (per sample-rate-band) write semantics, which [`Self::set_mux`] does not
    235     /// yet replicate — verify on hardware before exposing edits.
    236     pub fn read_routing(&mut self, count: usize) -> Result<Vec<(String, String)>, TransportError> {
    237         Ok(self
    238             .get_mux(count)?
    239             .into_iter()
    240             .map(|e| {
    241                 (
    242                     crate::ports::sink_name(e.dest),
    243                     crate::ports::source_name(e.source),
    244                 )
    245             })
    246             .collect())
    247     }
    248 
    249     /// Change one routing assignment: route `src` (source port number, 0 = Off)
    250     /// to `dst` (destination port number), then write the full mux back. Reads
    251     /// current routing first so only the one destination changes. Returns the
    252     /// updated [`crate::mux::MuxState`]. Uses the hardware-verified write path.
    253     pub fn set_route(
    254         &mut self,
    255         pc: [(u16, u16); 6],
    256         dst: u16,
    257         src: u16,
    258     ) -> Result<crate::mux::MuxState, TransportError> {
    259         use crate::mux::{mux_assignment_18i20_gen3, num_dsts, MuxState};
    260         let count = num_dsts(&pc);
    261         let entries = self.get_mux(count)?;
    262         let mut state = MuxState::from_entries(pc, &entries);
    263         state.set(dst, src);
    264         let tables = state.encode_all(&mux_assignment_18i20_gen3());
    265         self.write_routing_tables(&tables)?;
    266         Ok(state)
    267     }
    268 
    269     /// Apply several routing changes in ONE atomic write. `changes` is a list of
    270     /// `(dest_port, source_port)`. All are set in the model, then the full mux is
    271     /// written once — so a stereo pair never passes through a half-routed state.
    272     pub fn set_routes(
    273         &mut self,
    274         pc: [(u16, u16); 6],
    275         changes: &[(u16, u16)],
    276     ) -> Result<crate::mux::MuxState, TransportError> {
    277         use crate::mux::{mux_assignment_18i20_gen3, num_dsts, MuxState};
    278         let count = num_dsts(&pc);
    279         let entries = self.get_mux(count)?;
    280         let mut state = MuxState::from_entries(pc, &entries);
    281         for &(dst, src) in changes {
    282             state.set(dst, src);
    283         }
    284         let tables = state.encode_all(&mux_assignment_18i20_gen3());
    285         self.write_routing_tables(&tables)?;
    286         Ok(state)
    287     }
    288 
    289     /// Read routing and build a map `source_hw_id -> PCM-capture channel (1-based)`
    290     /// for every source routed to a PCM capture destination. This is how we meter
    291     /// physical inputs: each input is normally routed to a DAW (PCM) capture, and
    292     /// the PCM-capture meters carry the real signal (the mixer-input meters only
    293     /// cover sources routed into the mixer). `count` = total routing destinations.
    294     pub fn source_to_pcm_capture(
    295         &mut self,
    296         count: usize,
    297     ) -> Result<std::collections::HashMap<u16, u16>, TransportError> {
    298         // PCM hardware destination IDs are 0x600.. ; capture channel = id & 0xff + 1.
    299         const PCM_BASE: u16 = 0x600;
    300         let mut map = std::collections::HashMap::new();
    301         for e in self.get_mux(count)? {
    302             if e.source != 0 && (e.dest & 0xf00) == PCM_BASE {
    303                 let pcm_ch = (e.dest - PCM_BASE) + 1; // 1-based PCM capture channel
    304                 map.entry(e.source).or_insert(pcm_ch);
    305             }
    306         }
    307         Ok(map)
    308     }
    309 
    310     /// Write a full routing state to the device, exactly as the kernel does:
    311     /// one SET_MUX message per sample-rate-band table, each carrying
    312     /// `{pad:u16, table:u16, data:[u32]}`. This OVERWRITES all routing.
    313     ///
    314     /// SAFETY: this is the most destructive write in the protocol — a wrong
    315     /// table scrambles routing until restored. Prefer [`Self::dry_run_routing`]
    316     /// first to inspect the exact payloads. Pass the per-table encodings from
    317     /// [`crate::mux::MuxState::encode_table`] over `mux::mux_assignment_*`.
    318     pub fn write_routing_tables(&mut self, tables: &[Vec<u32>]) -> Result<(), TransportError> {
    319         for (table_num, data) in tables.iter().enumerate() {
    320             let mut payload = Vec::with_capacity(4 + data.len() * 4);
    321             payload.extend_from_slice(&0u16.to_le_bytes()); // pad
    322             payload.extend_from_slice(&(table_num as u16).to_le_bytes()); // table index
    323             for v in data {
    324                 payload.extend_from_slice(&v.to_le_bytes());
    325             }
    326             self.command(op::SET_MUX, &payload, 0)?;
    327         }
    328         Ok(())
    329     }
    330 
    331     /// Write a single routing table by index — diagnostics: pinpoint which of the
    332     /// 3 sample-rate-band tables the device rejects.
    333     pub fn write_routing_table(
    334         &mut self,
    335         table_num: u16,
    336         data: &[u32],
    337     ) -> Result<(), TransportError> {
    338         let mut payload = Vec::with_capacity(4 + data.len() * 4);
    339         payload.extend_from_slice(&0u16.to_le_bytes());
    340         payload.extend_from_slice(&table_num.to_le_bytes());
    341         for v in data {
    342             payload.extend_from_slice(&v.to_le_bytes());
    343         }
    344         self.command(op::SET_MUX, &payload, 0)?;
    345         Ok(())
    346     }
    347 
    348     /// Build (but do NOT send) the per-table SET_MUX payload byte-vectors for a
    349     /// routing state — for inspection / dry-run verification before writing.
    350     pub fn dry_run_routing(tables: &[Vec<u32>]) -> Vec<Vec<u8>> {
    351         tables
    352             .iter()
    353             .enumerate()
    354             .map(|(table_num, data)| {
    355                 let mut payload = Vec::with_capacity(4 + data.len() * 4);
    356                 payload.extend_from_slice(&0u16.to_le_bytes());
    357                 payload.extend_from_slice(&(table_num as u16).to_le_bytes());
    358                 for v in data {
    359                     payload.extend_from_slice(&v.to_le_bytes());
    360                 }
    361                 payload
    362             })
    363             .collect()
    364     }
    365 
    366     /// Legacy single-table writer (kept for the unit tests / simple cases).
    367     pub fn set_mux(&mut self, entries: &[MuxEntry]) -> Result<(), TransportError> {
    368         let mut payload = Vec::with_capacity(4 + entries.len() * 4);
    369         payload.extend_from_slice(&0u16.to_le_bytes()); // pad
    370         payload.extend_from_slice(&0u16.to_le_bytes()); // num (start index)
    371         for e in entries {
    372             payload.extend_from_slice(&e.pack().to_le_bytes());
    373         }
    374         self.command(op::SET_MUX, &payload, 0)?;
    375         Ok(())
    376     }
    377 
    378     /// Set every crosspoint of every mix bus to silence (all inputs off). This
    379     /// is the bulk "clear mixer" — useful to tame a factory wall-of-unity matrix.
    380     /// `buses`/`inputs` are the device's mix dimensions.
    381     pub fn clear_mixer(&mut self, buses: u16, inputs: usize) -> Result<(), TransportError> {
    382         let silence = vec![0u16; inputs];
    383         for bus in 0..buses {
    384             self.set_mix(bus, &silence)?;
    385         }
    386         Ok(())
    387     }
    388 
    389     /// Read every mix bus into a `buses × inputs` grid of dB gains (for a UI
    390     /// refresh of the whole mixer matrix).
    391     pub fn read_mixer_db(
    392         &mut self,
    393         buses: u16,
    394         inputs: usize,
    395     ) -> Result<Vec<Vec<f32>>, TransportError> {
    396         let mut grid = Vec::with_capacity(buses as usize);
    397         for bus in 0..buses {
    398             let raw = self.get_mix(bus, inputs)?;
    399             grid.push(raw.into_iter().map(mixer_value_to_db).collect());
    400         }
    401         Ok(grid)
    402     }
    403 
    404     /// Set one crosspoint: input `input` → mix bus `bus` at `db`. Reads the bus's
    405     /// current levels, changes the one input, and writes the bus back.
    406     pub fn set_mix_point_db(
    407         &mut self,
    408         bus: u16,
    409         input: usize,
    410         db: f32,
    411         num_inputs: usize,
    412     ) -> Result<(), TransportError> {
    413         let mut levels = self.get_mix(bus, num_inputs)?;
    414         if let Some(slot) = levels.get_mut(input) {
    415             *slot = db_to_mixer_value(db);
    416         }
    417         self.set_mix(bus, &levels)
    418     }
    419 
    420     /// Snapshot `num_meters` metering points. Each level is a raw `u32`.
    421     pub fn get_meters(&mut self, num_meters: u16) -> Result<Vec<u32>, TransportError> {
    422         const METER_MAGIC: u32 = 1;
    423         let mut payload = Vec::with_capacity(8);
    424         payload.extend_from_slice(&0u16.to_le_bytes()); // pad
    425         payload.extend_from_slice(&num_meters.to_le_bytes());
    426         payload.extend_from_slice(&METER_MAGIC.to_le_bytes());
    427         let resp = self.command(op::GET_METER, &payload, num_meters as usize * 4)?;
    428         Ok(le_u32s(&resp.payload, num_meters as usize))
    429     }
    430 }
    431 
    432 fn le_u16s(bytes: &[u8], count: usize) -> Vec<u16> {
    433     bytes
    434         .chunks_exact(2)
    435         .take(count)
    436         .map(|c| u16::from_le_bytes([c[0], c[1]]))
    437         .collect()
    438 }
    439 
    440 fn le_u32s(bytes: &[u8], count: usize) -> Vec<u32> {
    441     bytes
    442         .chunks_exact(4)
    443         .take(count)
    444         .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
    445         .collect()
    446 }
    447 
    448 #[cfg(test)]
    449 mod tests {
    450     use super::*;
    451     use crate::transport::mock::MockTransport;
    452 
    453     #[test]
    454     fn mixer_db_unity_and_extremes() {
    455         // 8192 is unity = 0 dB.
    456         assert!((mixer_value_to_db(8192) - 0.0).abs() < 0.05);
    457         assert_eq!(mixer_value_to_db(0), MIXER_MIN_DB);
    458         // round-trip 0 dB
    459         assert_eq!(db_to_mixer_value(0.0), 8192);
    460         // +12 dB is the ceiling
    461         assert!(db_to_mixer_value(20.0) <= db_to_mixer_value(MIXER_MAX_DB) + 1);
    462     }
    463 
    464     #[test]
    465     fn mixer_db_round_trips_within_quantization() {
    466         for &db in &[-60.0_f32, -20.0, -6.0, 0.0, 6.0, 12.0] {
    467             let v = db_to_mixer_value(db);
    468             let back = mixer_value_to_db(v);
    469             assert!((back - db).abs() < 0.5, "db {db} -> {v} -> {back}");
    470         }
    471     }
    472 
    473     #[test]
    474     fn read_mixer_db_builds_grid() {
    475         let mut m = MockTransport::new();
    476         // 2 buses × 2 inputs, all unity
    477         for _ in 0..2 {
    478             let mut p = Vec::new();
    479             p.extend_from_slice(&8192u16.to_le_bytes());
    480             p.extend_from_slice(&8192u16.to_le_bytes());
    481             m.push_response(op::GET_MIX, &p);
    482         }
    483         let mut dev = Scarlett::new(m);
    484         let grid = dev.read_mixer_db(2, 2).unwrap();
    485         assert_eq!(grid.len(), 2);
    486         assert!((grid[0][0]).abs() < 0.05); // ~0 dB
    487     }
    488 
    489     #[test]
    490     fn mux_entry_round_trips_through_packing() {
    491         // dest in low 12 bits, source in high 12 (kernel order)
    492         let e = MuxEntry { source: 5, dest: 9 };
    493         let packed = e.pack();
    494         assert_eq!(packed, 9 | (5 << 12));
    495         assert_eq!(MuxEntry::unpack(packed), e);
    496     }
    497 
    498     #[test]
    499     fn get_mix_requests_bus_and_parses_levels() {
    500         let mut m = MockTransport::new();
    501         // two inputs: levels 0x1000, 0x2000
    502         m.push_response(op::GET_MIX, &[0x00, 0x10, 0x00, 0x20]);
    503         let mut dev = Scarlett::new(m);
    504 
    505         let levels = dev.get_mix(3, 2).unwrap();
    506         assert_eq!(levels, vec![0x1000, 0x2000]);
    507 
    508         let m = dev.into_transport();
    509         assert_eq!(m.sent[0].0, op::GET_MIX);
    510         // request = mix_num(3) + count(2), both u16 LE
    511         assert_eq!(m.sent[0].1, vec![0x03, 0x00, 0x02, 0x00]);
    512     }
    513 
    514     #[test]
    515     fn set_mix_packs_bus_then_levels() {
    516         let mut m = MockTransport::new();
    517         m.push_response(op::SET_MIX, &[]);
    518         let mut dev = Scarlett::new(m);
    519 
    520         dev.set_mix(1, &[0xaabb, 0xccdd]).unwrap();
    521 
    522         let m = dev.into_transport();
    523         let (cmd, payload) = &m.sent[0];
    524         assert_eq!(*cmd, op::SET_MIX);
    525         assert_eq!(payload, &vec![0x01, 0x00, 0xbb, 0xaa, 0xdd, 0xcc]);
    526     }
    527 
    528     #[test]
    529     fn get_mux_parses_packed_entries() {
    530         let mut m = MockTransport::new();
    531         // dest|src<<12 : (dest 0, src 2) and (dest 3, src 7)
    532         let raw0 = (0u32 | (2 << 12)).to_le_bytes();
    533         let raw1 = (3u32 | (7 << 12)).to_le_bytes();
    534         let mut payload = Vec::new();
    535         payload.extend_from_slice(&raw0);
    536         payload.extend_from_slice(&raw1);
    537         m.push_response(op::GET_MUX, &payload);
    538         let mut dev = Scarlett::new(m);
    539 
    540         let entries = dev.get_mux(2).unwrap();
    541         assert_eq!(entries[0], MuxEntry { source: 2, dest: 0 });
    542         assert_eq!(entries[1], MuxEntry { source: 7, dest: 3 });
    543 
    544         let m = dev.into_transport();
    545         // request = num(0) + count(2)
    546         assert_eq!(m.sent[0].1, vec![0x00, 0x00, 0x02, 0x00]);
    547     }
    548 
    549     #[test]
    550     fn set_mux_writes_pad_num_then_packed_entries() {
    551         let mut m = MockTransport::new();
    552         m.push_response(op::SET_MUX, &[]);
    553         let mut dev = Scarlett::new(m);
    554 
    555         dev.set_mux(&[MuxEntry { source: 1, dest: 4 }]).unwrap();
    556 
    557         let m = dev.into_transport();
    558         let (cmd, payload) = &m.sent[0];
    559         assert_eq!(*cmd, op::SET_MUX);
    560         let entry = (4u32 | (1 << 12)).to_le_bytes(); // dest 4, source 1
    561         let mut expect = vec![0x00, 0x00, 0x00, 0x00]; // pad, num
    562         expect.extend_from_slice(&entry);
    563         assert_eq!(payload, &expect);
    564     }
    565 
    566     #[test]
    567     fn get_meters_sends_magic_and_parses_u32_levels() {
    568         let mut m = MockTransport::new();
    569         let mut payload = Vec::new();
    570         payload.extend_from_slice(&0x0000_1234u32.to_le_bytes());
    571         payload.extend_from_slice(&0x0000_5678u32.to_le_bytes());
    572         m.push_response(op::GET_METER, &payload);
    573         let mut dev = Scarlett::new(m);
    574 
    575         let meters = dev.get_meters(2).unwrap();
    576         assert_eq!(meters, vec![0x1234, 0x5678]);
    577 
    578         let m = dev.into_transport();
    579         let (cmd, sent) = &m.sent[0];
    580         assert_eq!(*cmd, op::GET_METER);
    581         // pad(0,u16) + num_meters(2,u16) + magic(1,u32)
    582         assert_eq!(sent, &vec![0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00]);
    583     }
    584 }