valentine

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

mux.rs (19108B)


      1 //! Routing (mux) model and write encoding — the highest-risk part of the device
      2 //! protocol, built deliberately and conservatively.
      3 //!
      4 //! The device routing is a flat array `mux[dest] = source`, where `dest` and
      5 //! `source` are **port numbers**: a flattened index across all port types in a
      6 //! fixed order (None, Analogue, S/PDIF, ADAT, Mix, PCM), counting that type's
      7 //! *outputs* (for destinations) or *inputs* (for sources). Hardware IDs
      8 //! (`base | index`) are a separate encoding used on the wire.
      9 //!
     10 //! Writing routing means re-emitting the **entire** mux as 3 tables (one per
     11 //! sample-rate band), each in `mux_assignment` order, every entry packed as
     12 //! `dest_id | (source_id << 12)`. We port the kernel's `scarlett2_usb_set_mux`
     13 //! exactly. **Nothing here writes to the device** — this module only models and
     14 //! encodes; the caller decides when (and a dry-run verifies first).
     15 
     16 use crate::matrix::MuxEntry;
     17 
     18 /// Port types in the device's canonical enumeration order. The numeric order is
     19 /// load-bearing: port-number flattening walks types in exactly this sequence.
     20 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     21 pub enum PortKind {
     22     None = 0,
     23     Analogue,
     24     Spdif,
     25     Adat,
     26     Mix,
     27     Pcm,
     28 }
     29 
     30 impl PortKind {
     31     pub const ORDER: [PortKind; 6] = [
     32         PortKind::None,
     33         PortKind::Analogue,
     34         PortKind::Spdif,
     35         PortKind::Adat,
     36         PortKind::Mix,
     37         PortKind::Pcm,
     38     ];
     39 
     40     /// Hardware ID base for this kind (matches `scarlett2_ports[].id`).
     41     pub fn id_base(self) -> u16 {
     42         match self {
     43             PortKind::None => 0x000,
     44             PortKind::Analogue => 0x080,
     45             PortKind::Spdif => 0x180,
     46             PortKind::Adat => 0x200,
     47             PortKind::Mix => 0x300,
     48             PortKind::Pcm => 0x600,
     49         }
     50     }
     51 }
     52 
     53 /// `(inputs, outputs)` counts per kind for the 18i20 g3, in [`PortKind::ORDER`].
     54 /// From `s18i20_gen3_info.port_count`.
     55 pub const PORT_COUNT_18I20_GEN3: [(u16, u16); 6] = [
     56     (1, 0),   // None
     57     (9, 10),  // Analogue (9 sources incl. talkback; 10 line outs)
     58     (2, 2),   // S/PDIF
     59     (8, 8),   // ADAT
     60     (12, 25), // Mix
     61     (20, 20), // PCM
     62 ];
     63 
     64 /// Direction selector for port-number ↔ id conversions.
     65 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     66 pub enum Dir {
     67     In,
     68     Out,
     69 }
     70 
     71 fn count(pc: &[(u16, u16); 6], kind: PortKind, dir: Dir) -> u16 {
     72     let (i, o) = pc[kind as usize];
     73     match dir {
     74         Dir::In => i,
     75         Dir::Out => o,
     76     }
     77 }
     78 
     79 /// Convert a flat port number to a hardware id, in the given direction.
     80 /// Mirrors `scarlett2_mux_src_num_to_id` (and the OUT equivalent).
     81 pub fn num_to_id(pc: &[(u16, u16); 6], dir: Dir, mut num: u16) -> u16 {
     82     for kind in PortKind::ORDER {
     83         let c = count(pc, kind, dir);
     84         if num < c {
     85             return kind.id_base() | num;
     86         }
     87         num -= c;
     88     }
     89     0 // out of range → Off
     90 }
     91 
     92 /// Convert a hardware id to a flat port number, in the given direction.
     93 /// Mirrors `scarlett2_mux_id_to_num`. Returns None if the id isn't in range.
     94 pub fn id_to_num(pc: &[(u16, u16); 6], dir: Dir, id: u16) -> Option<u16> {
     95     let mut port_num = 0u16;
     96     for kind in PortKind::ORDER {
     97         let base = kind.id_base();
     98         let c = count(pc, kind, dir);
     99         if id >= base && id < base + c {
    100             return Some(port_num + (id - base));
    101         }
    102         port_num += c;
    103     }
    104     None
    105 }
    106 
    107 /// Total destination count (sum of outputs across kinds) = mux length.
    108 pub fn num_dsts(pc: &[(u16, u16); 6]) -> usize {
    109     pc.iter().map(|(_, o)| *o as usize).sum()
    110 }
    111 
    112 /// One `mux_assignment` entry: a run of `count` destinations of `kind` starting
    113 /// at output index `start`.
    114 #[derive(Debug, Clone, Copy)]
    115 pub struct Assign {
    116     pub kind: PortKind,
    117     pub start: u16,
    118     pub count: u16,
    119 }
    120 
    121 /// The 18i20 g3 mux_assignment tables (3 sample-rate bands), from
    122 /// `s18i20_gen3_info.mux_assignment`. Terminating `{0,0,0}` entries omitted.
    123 pub fn mux_assignment_18i20_gen3() -> [Vec<Assign>; 3] {
    124     use PortKind::*;
    125     let a = |kind, start, count| Assign { kind, start, count };
    126     [
    127         vec![
    128             a(Pcm, 0, 8), a(Pcm, 10, 10), a(Analogue, 0, 10), a(Spdif, 0, 2),
    129             a(Adat, 0, 8), a(Pcm, 8, 2), a(Mix, 0, 25), a(None, 0, 12),
    130         ],
    131         vec![
    132             a(Pcm, 0, 8), a(Pcm, 10, 8), a(Analogue, 0, 10), a(Spdif, 0, 2),
    133             a(Adat, 0, 8), a(Pcm, 8, 2), a(Mix, 0, 25), a(None, 0, 10),
    134         ],
    135         vec![
    136             a(Pcm, 0, 10), a(Analogue, 0, 10), a(Spdif, 0, 2), a(None, 0, 24),
    137         ],
    138     ]
    139 }
    140 
    141 /// Start port-number (OUT direction) of a kind — `scarlett2_get_port_start_num`.
    142 fn port_start_out(pc: &[(u16, u16); 6], kind: PortKind) -> u16 {
    143     let mut n = 0;
    144     for k in PortKind::ORDER {
    145         if k == kind {
    146             break;
    147         }
    148         n += count(pc, k, Dir::Out);
    149     }
    150     n
    151 }
    152 
    153 /// The full routing state: `mux[dest_num] = source_num`.
    154 #[derive(Debug, Clone, PartialEq, Eq)]
    155 pub struct MuxState {
    156     pub pc: [(u16, u16); 6],
    157     pub mux: Vec<u16>,
    158 }
    159 
    160 impl MuxState {
    161     /// Build from the device's `GET_MUX` entries (hardware ids), decoding each
    162     /// to port numbers. Unknown ids are ignored (left as Off).
    163     pub fn from_entries(pc: [(u16, u16); 6], entries: &[MuxEntry]) -> Self {
    164         let mut mux = vec![0u16; num_dsts(&pc)];
    165         for e in entries {
    166             if let (Some(dst), Some(src)) = (
    167                 id_to_num(&pc, Dir::Out, e.dest),
    168                 id_to_num(&pc, Dir::In, e.source),
    169             ) {
    170                 if (dst as usize) < mux.len() {
    171                     mux[dst as usize] = src;
    172                 }
    173             }
    174         }
    175         Self { pc, mux }
    176     }
    177 
    178     /// Set the source feeding a destination (both as port numbers).
    179     pub fn set(&mut self, dst: u16, src: u16) {
    180         if (dst as usize) < self.mux.len() {
    181             self.mux[dst as usize] = src;
    182         }
    183     }
    184 
    185     /// Current source port number feeding `dst`.
    186     pub fn get(&self, dst: u16) -> u16 {
    187         self.mux.get(dst as usize).copied().unwrap_or(0)
    188     }
    189 
    190     /// Encode all 3 sample-rate-band tables, ready for `write_routing_tables`.
    191     pub fn encode_all(&self, assign: &[Vec<Assign>; 3]) -> Vec<Vec<u32>> {
    192         assign.iter().map(|a| self.encode_table(a)).collect()
    193     }
    194 
    195     /// Encode one mux table to the `u32` payload values for SET_MUX, exactly as
    196     /// the kernel does: walk the assignment, pack `dst_id | (src_id << 12)`,
    197     /// empty (None/id 0) slots as 0.
    198     pub fn encode_table(&self, assign: &[Assign]) -> Vec<u32> {
    199         let mut out = Vec::new();
    200         for entry in assign {
    201             let base_dst_id = entry.kind.id_base() + entry.start;
    202             let mux_start = port_start_out(&self.pc, entry.kind) + entry.start;
    203             if entry.kind.id_base() == 0 {
    204                 // None: empty slots
    205                 for _ in 0..entry.count {
    206                     out.push(0);
    207                 }
    208                 continue;
    209             }
    210             for j in 0..entry.count {
    211                 let mux_idx = (mux_start + j) as usize;
    212                 let src_num = self.mux.get(mux_idx).copied().unwrap_or(0);
    213                 let src_id = num_to_id(&self.pc, Dir::In, src_num);
    214                 let dst_id = base_dst_id + j;
    215                 out.push(dst_id as u32 | ((src_id as u32) << 12));
    216             }
    217         }
    218         out
    219     }
    220 
    221     /// Round-trip a set of device entries through encode and decode the result
    222     /// back to `dest_id -> source_id`, for offline verification that our encoding
    223     /// reproduces the device's routing without loss. Pure; no device.
    224     pub fn roundtrip_decode(entries: &[MuxEntry], pc: [(u16, u16); 6]) -> Vec<(u16, u16)> {
    225         let st = MuxState::from_entries(pc, entries);
    226         let assign = mux_assignment_18i20_gen3();
    227         let mut out = Vec::new();
    228         for v in st.encode_table(&assign[0]) {
    229             let dst = (v & 0xfff) as u16;
    230             let src = ((v >> 12) & 0xfff) as u16;
    231             if dst != 0 {
    232                 out.push((dst, src));
    233             }
    234         }
    235         out
    236     }
    237 }
    238 
    239 /// A left/right grouping of ports: `right` is None for a lone mono port.
    240 #[derive(Debug, Clone, PartialEq, Eq)]
    241 pub struct PairRow {
    242     pub left: u16,
    243     pub right: Option<u16>,
    244     pub name: String,
    245 }
    246 
    247 /// Group ports of one direction into stereo pairs by kind: consecutive same-kind
    248 /// ports at even/odd index collapse into one row ("Analogue Out 1-2"); a lone
    249 /// trailing port stays mono. Used for both destinations (Dir::Out) and sources
    250 /// (Dir::In). For sources, a leading Off row (port 0) is included.
    251 fn pair_ports(pc: &[(u16, u16); 6], dir: Dir, name: impl Fn(u16) -> String) -> Vec<PairRow> {
    252     // Flat list of (port_num, kind, index_within_kind) in enumeration order.
    253     let mut flat: Vec<(u16, PortKind, u16)> = Vec::new();
    254     let mut port = 0u16;
    255     for kind in PortKind::ORDER {
    256         let c = count(pc, kind, dir);
    257         for idx in 0..c {
    258             flat.push((port, kind, idx));
    259             port += 1;
    260         }
    261     }
    262 
    263     let mut rows = Vec::new();
    264     let mut i = 0;
    265     while i < flat.len() {
    266         let (pl, kl, il) = flat[i];
    267         // None/Off ports stay mono (the single Off entry).
    268         let can_pair = kl != PortKind::None
    269             && i + 1 < flat.len()
    270             && flat[i + 1].1 == kl
    271             && il % 2 == 0
    272             && flat[i + 1].2 == il + 1;
    273         if can_pair {
    274             let pr = flat[i + 1].0;
    275             // "Analogue Out 1-2": kind word from the left name, indices joined.
    276             let lname = name(pl);
    277             let word = lname.rsplit_once(' ').map(|(w, _)| w).unwrap_or(&lname);
    278             rows.push(PairRow {
    279                 left: pl,
    280                 right: Some(pr),
    281                 name: format!("{word} {}-{}", il + 1, il + 2),
    282             });
    283             i += 2;
    284         } else {
    285             rows.push(PairRow { left: pl, right: None, name: name(pl) });
    286             i += 1;
    287         }
    288     }
    289     rows
    290 }
    291 
    292 /// Destinations grouped into stereo pairs (for the routing panel's default view).
    293 pub fn dest_pairs(pc: &[(u16, u16); 6]) -> Vec<PairRow> {
    294     pair_ports(pc, Dir::Out, |d| crate::ports::sink_name(num_to_id(pc, Dir::Out, d)))
    295 }
    296 
    297 /// Selectable sources grouped into stereo pairs, with Off first.
    298 pub fn source_pairs(pc: &[(u16, u16); 6]) -> Vec<PairRow> {
    299     pair_ports(pc, Dir::In, |s| {
    300         if s == 0 {
    301             "Off".to_string()
    302         } else {
    303             crate::ports::source_name(num_to_id(pc, Dir::In, s))
    304         }
    305     })
    306 }
    307 
    308 /// Enumerate every destination as `(dest_port_num, display_name)` in port order.
    309 pub fn dest_list(pc: &[(u16, u16); 6]) -> Vec<(u16, String)> {
    310     (0..num_dsts(pc) as u16)
    311         .map(|d| (d, crate::ports::sink_name(num_to_id(pc, Dir::Out, d))))
    312         .collect()
    313 }
    314 
    315 /// Enumerate every selectable source as `(src_port_num, display_name)`, starting
    316 /// with Off (port 0). Order matches the device's source numbering.
    317 pub fn source_list(pc: &[(u16, u16); 6]) -> Vec<(u16, String)> {
    318     let total_in: u16 = pc.iter().map(|(i, _)| *i).sum();
    319     (0..total_in)
    320         .map(|s| {
    321             let name = if s == 0 {
    322                 "Off".to_string()
    323             } else {
    324                 crate::ports::source_name(num_to_id(pc, Dir::In, s))
    325             };
    326             (s, name)
    327         })
    328         .collect()
    329 }
    330 
    331 #[cfg(test)]
    332 mod tests {
    333     use super::*;
    334 
    335     const PC: [(u16, u16); 6] = PORT_COUNT_18I20_GEN3;
    336 
    337     #[test]
    338     fn roundtrip_preserves_real_device_routing() {
    339         // The user's actual routing (from adatverify), incl. ADAT 7/8 ← PCM 19/20.
    340         // The round-trip MUST reproduce every entry — if any drops to Off, that's
    341         // the corruption that zeroed ADAT 7/8 on hardware.
    342         let e = |dst: u16, src: u16| MuxEntry { dest: dst, source: src };
    343         let entries = vec![
    344             // ADAT Out 1-8 (0x200..0x207) ← PCM 13-20 (0x60c..0x613)
    345             e(0x200, 0x60c), e(0x201, 0x60d), e(0x202, 0x60e), e(0x203, 0x60f),
    346             e(0x204, 0x610), e(0x205, 0x611), e(0x206, 0x612), e(0x207, 0x613),
    347             // Analogue Out 1-2 ← PCM 1-2
    348             e(0x080, 0x600), e(0x081, 0x601),
    349             // Mixer In 1-2 ← Analogue 1-2
    350             e(0x300, 0x080), e(0x301, 0x081),
    351         ];
    352         let got = MuxState::roundtrip_decode(&entries, PC);
    353         for me in &entries {
    354             let (dst, src) = (me.dest, me.source);
    355             let found = got.iter().find(|(d, _)| *d == dst).map(|(_, s)| *s);
    356             assert_eq!(
    357                 found,
    358                 Some(src),
    359                 "dest {dst:#05x} should map to src {src:#05x}, got {found:?}"
    360             );
    361         }
    362     }
    363 
    364     #[test]
    365     fn per_table_entry_counts_match_assignment() {
    366         // Each encoded table's length must equal the sum of its assignment
    367         // entry counts (the kernel sends exactly that many words). A mismatch —
    368         // especially an over-long table 1/2 — is a prime 0x3 suspect.
    369         let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
    370         let assign = mux_assignment_18i20_gen3();
    371         for (t, a) in assign.iter().enumerate() {
    372             let expected: usize = a.iter().map(|e| e.count as usize).sum();
    373             let got = st.encode_table(a).len();
    374             assert_eq!(got, expected, "table {t}: encoded {got} != assignment sum {expected}");
    375         }
    376         // Print the actual per-table sizes for the record.
    377         let sizes: Vec<usize> = assign.iter().map(|a| st.encode_table(a).len()).collect();
    378         // Kernel/Focusrite: tables are 77, 75, 36-ish depending on band. Just
    379         // assert table 0 is the largest and all are <= SCARLETT2_MUX_MAX (77).
    380         assert!(sizes.iter().all(|&s| s <= 77), "a table exceeds MUX_MAX 77: {sizes:?}");
    381     }
    382 
    383     #[test]
    384     fn encoded_table_covers_all_adat_destinations() {
    385         // Every ADAT Out (0x200..0x207) must appear as a destination in the
    386         // encoded table 0. If 7/8 are missing, the assignment table is wrong and
    387         // a write would corrupt/zero them — the suspected cause.
    388         let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
    389         let table = st.encode_table(&mux_assignment_18i20_gen3()[0]);
    390         let dests: std::collections::HashSet<u16> =
    391             table.iter().map(|v| (v & 0xfff) as u16).collect();
    392         for i in 0..8u16 {
    393             assert!(
    394                 dests.contains(&(0x200 + i)),
    395                 "ADAT Out {} (id {:#05x}) missing from encoded table",
    396                 i + 1,
    397                 0x200 + i
    398             );
    399         }
    400     }
    401 
    402     #[test]
    403     fn encoded_table_has_no_duplicate_destinations() {
    404         // Each destination must appear at most once; a dup means two entries
    405         // fight over one dest (device may reject → 0x3, or last-wins corrupts).
    406         let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
    407         let table = st.encode_table(&mux_assignment_18i20_gen3()[0]);
    408         let mut seen = std::collections::HashSet::new();
    409         for v in &table {
    410             let dst = (v & 0xfff) as u16;
    411             if dst != 0 {
    412                 assert!(seen.insert(dst), "duplicate destination {dst:#05x} in table");
    413             }
    414         }
    415     }
    416 
    417     #[test]
    418     fn dest_pairs_group_stereo_and_keep_mono() {
    419         let dp = dest_pairs(&PC);
    420         // First row: Analogue Out 1-2 (paired).
    421         assert_eq!(dp[0].left, 0);
    422         assert_eq!(dp[0].right, Some(1));
    423         assert!(dp[0].name.contains("1-2"));
    424         // Every destination appears exactly once across the rows.
    425         let mut seen = std::collections::HashSet::new();
    426         for r in &dp {
    427             assert!(seen.insert(r.left));
    428             if let Some(rt) = r.right {
    429                 assert!(seen.insert(rt));
    430             }
    431         }
    432         assert_eq!(seen.len(), num_dsts(&PC));
    433     }
    434 
    435     #[test]
    436     fn source_pairs_start_with_off_mono() {
    437         let sp = source_pairs(&PC);
    438         assert_eq!(sp[0].left, 0);
    439         assert_eq!(sp[0].right, None);
    440         assert_eq!(sp[0].name, "Off");
    441     }
    442 
    443     #[test]
    444     fn dest_and_source_lists_are_complete() {
    445         let dests = dest_list(&PC);
    446         assert_eq!(dests.len(), 65);
    447         assert_eq!(dests[0].1, "Analogue Out 1");
    448         let srcs = source_list(&PC);
    449         // 1 off-as-port0 already counted in the (1,0) None inputs; total inputs
    450         // = 1+9+2+8+12+20 = 52.
    451         assert_eq!(srcs.len(), 52);
    452         assert_eq!(srcs[0].1, "Off");
    453     }
    454 
    455     #[test]
    456     fn source_num_to_id_matches_kernel_walk() {
    457         // src 0 = Off; src 1 = Analogue 1 (0x080); src 9 = Analogue 9 (0x088);
    458         // then S/PDIF, ADAT, Mix, PCM.
    459         assert_eq!(num_to_id(&PC, Dir::In, 0), 0x000);
    460         assert_eq!(num_to_id(&PC, Dir::In, 1), 0x080);
    461         assert_eq!(num_to_id(&PC, Dir::In, 9), 0x088); // talkback
    462         assert_eq!(num_to_id(&PC, Dir::In, 10), 0x180); // S/PDIF 1
    463         assert_eq!(num_to_id(&PC, Dir::In, 12), 0x200); // ADAT 1
    464         assert_eq!(num_to_id(&PC, Dir::In, 20), 0x300); // Mix A
    465         assert_eq!(num_to_id(&PC, Dir::In, 32), 0x600); // PCM 1
    466     }
    467 
    468     #[test]
    469     fn dest_id_to_num_matches_probe_layout() {
    470         // Verified against the metermap probe: Analogue Out 0..9, S/PDIF 10..11,
    471         // ADAT 12..19, Mixer In 20..44, PCM cap 45..64.
    472         assert_eq!(id_to_num(&PC, Dir::Out, 0x080), Some(0)); // Analogue Out 1
    473         assert_eq!(id_to_num(&PC, Dir::Out, 0x200), Some(12)); // ADAT Out 1
    474         assert_eq!(id_to_num(&PC, Dir::Out, 0x300), Some(20)); // Mixer In 1
    475         assert_eq!(id_to_num(&PC, Dir::Out, 0x600), Some(45)); // PCM cap 1
    476     }
    477 
    478     #[test]
    479     fn num_id_round_trip_in_and_out() {
    480         for n in 0..num_dsts(&PC) as u16 {
    481             let id = num_to_id(&PC, Dir::Out, n);
    482             assert_eq!(id_to_num(&PC, Dir::Out, id), Some(n), "out dst {n}");
    483         }
    484     }
    485 
    486     #[test]
    487     fn total_dst_count_is_65() {
    488         assert_eq!(num_dsts(&PC), 65);
    489     }
    490 
    491     #[test]
    492     fn encode_then_decode_reproduces_state() {
    493         // The core safety property: build a state, encode every table, decode the
    494         // packed entries back, and confirm we recover the same routing. If this
    495         // holds, our write faithfully represents the model.
    496         let entries = vec![
    497             MuxEntry { dest: 0x080, source: 0x600 }, // Analogue Out 1 <- PCM 1
    498             MuxEntry { dest: 0x302, source: 0x082 }, // Mixer In 3 <- Analogue 3
    499             MuxEntry { dest: 0x60c, source: 0x200 }, // PCM cap 13 <- ADAT 1
    500         ];
    501         let st = MuxState::from_entries(PC, &entries);
    502         let assign = mux_assignment_18i20_gen3();
    503         // Decode table 0 and check our three routes survive the pack/unpack.
    504         let packed = st.encode_table(&assign[0]);
    505         let mut found = std::collections::HashMap::new();
    506         for v in packed {
    507             let dst_id = (v & 0xfff) as u16;
    508             let src_id = ((v >> 12) & 0xfff) as u16;
    509             if src_id != 0 {
    510                 found.insert(dst_id, src_id);
    511             }
    512         }
    513         assert_eq!(found.get(&0x080), Some(&0x600));
    514         assert_eq!(found.get(&0x302), Some(&0x082));
    515         assert_eq!(found.get(&0x60c), Some(&0x200));
    516     }
    517 }