valentine

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

commit d6d4dfeaf98ef73310e931708f610c309db77dc2
parent acf91fce8cecdf5ac8d5aabb9e20601124513252
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Mon,  1 Jun 2026 16:18:08 -0500

test: offline mux round-trip suite disproves 4 corruption theories

Adds MuxState::roundtrip_decode + tests proving the encoding is structurally
sound (faithful round-trip on the user's real routing incl ADAT 7/8, all ADAT
dests present, no dup dests, per-table lengths match assignment ≤77). The 0x3
on SET_MUX is therefore not in our encoding logic — it's live-wire framing or a
device-state precondition. Documented as the key open bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Diffstat:
Mscarlett-core/src/mux.rs | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 97 insertions(+), 0 deletions(-)

diff --git a/scarlett-core/src/mux.rs b/scarlett-core/src/mux.rs @@ -217,6 +217,23 @@ impl MuxState { } out } + + /// Round-trip a set of device entries through encode and decode the result + /// back to `dest_id -> source_id`, for offline verification that our encoding + /// reproduces the device's routing without loss. Pure; no device. + pub fn roundtrip_decode(entries: &[MuxEntry], pc: [(u16, u16); 6]) -> Vec<(u16, u16)> { + let st = MuxState::from_entries(pc, entries); + let assign = mux_assignment_18i20_gen3(); + let mut out = Vec::new(); + for v in st.encode_table(&assign[0]) { + let dst = (v & 0xfff) as u16; + let src = ((v >> 12) & 0xfff) as u16; + if dst != 0 { + out.push((dst, src)); + } + } + out + } } /// A left/right grouping of ports: `right` is None for a lone mono port. @@ -318,6 +335,86 @@ mod tests { const PC: [(u16, u16); 6] = PORT_COUNT_18I20_GEN3; #[test] + fn roundtrip_preserves_real_device_routing() { + // The user's actual routing (from adatverify), incl. ADAT 7/8 ← PCM 19/20. + // The round-trip MUST reproduce every entry — if any drops to Off, that's + // the corruption that zeroed ADAT 7/8 on hardware. + let e = |dst: u16, src: u16| MuxEntry { dest: dst, source: src }; + let entries = vec![ + // ADAT Out 1-8 (0x200..0x207) ← PCM 13-20 (0x60c..0x613) + e(0x200, 0x60c), e(0x201, 0x60d), e(0x202, 0x60e), e(0x203, 0x60f), + e(0x204, 0x610), e(0x205, 0x611), e(0x206, 0x612), e(0x207, 0x613), + // Analogue Out 1-2 ← PCM 1-2 + e(0x080, 0x600), e(0x081, 0x601), + // Mixer In 1-2 ← Analogue 1-2 + e(0x300, 0x080), e(0x301, 0x081), + ]; + let got = MuxState::roundtrip_decode(&entries, PC); + for me in &entries { + let (dst, src) = (me.dest, me.source); + let found = got.iter().find(|(d, _)| *d == dst).map(|(_, s)| *s); + assert_eq!( + found, + Some(src), + "dest {dst:#05x} should map to src {src:#05x}, got {found:?}" + ); + } + } + + #[test] + fn per_table_entry_counts_match_assignment() { + // Each encoded table's length must equal the sum of its assignment + // entry counts (the kernel sends exactly that many words). A mismatch — + // especially an over-long table 1/2 — is a prime 0x3 suspect. + let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] }; + let assign = mux_assignment_18i20_gen3(); + for (t, a) in assign.iter().enumerate() { + let expected: usize = a.iter().map(|e| e.count as usize).sum(); + let got = st.encode_table(a).len(); + assert_eq!(got, expected, "table {t}: encoded {got} != assignment sum {expected}"); + } + // Print the actual per-table sizes for the record. + let sizes: Vec<usize> = assign.iter().map(|a| st.encode_table(a).len()).collect(); + // Kernel/Focusrite: tables are 77, 75, 36-ish depending on band. Just + // assert table 0 is the largest and all are <= SCARLETT2_MUX_MAX (77). + assert!(sizes.iter().all(|&s| s <= 77), "a table exceeds MUX_MAX 77: {sizes:?}"); + } + + #[test] + fn encoded_table_covers_all_adat_destinations() { + // Every ADAT Out (0x200..0x207) must appear as a destination in the + // encoded table 0. If 7/8 are missing, the assignment table is wrong and + // a write would corrupt/zero them — the suspected cause. + let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] }; + let table = st.encode_table(&mux_assignment_18i20_gen3()[0]); + let dests: std::collections::HashSet<u16> = + table.iter().map(|v| (v & 0xfff) as u16).collect(); + for i in 0..8u16 { + assert!( + dests.contains(&(0x200 + i)), + "ADAT Out {} (id {:#05x}) missing from encoded table", + i + 1, + 0x200 + i + ); + } + } + + #[test] + fn encoded_table_has_no_duplicate_destinations() { + // Each destination must appear at most once; a dup means two entries + // fight over one dest (device may reject → 0x3, or last-wins corrupts). + let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] }; + let table = st.encode_table(&mux_assignment_18i20_gen3()[0]); + let mut seen = std::collections::HashSet::new(); + for v in &table { + let dst = (v & 0xfff) as u16; + if dst != 0 { + assert!(seen.insert(dst), "duplicate destination {dst:#05x} in table"); + } + } + } + + #[test] fn dest_pairs_group_stereo_and_keep_mono() { let dp = dest_pairs(&PC); // First row: Analogue Out 1-2 (paired).