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 }