valentine

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

commit d6965e725d2eba1c17f2bd45d4b747f4b520cb3a
parent 5a53c4fc3ff00d1e2c003db19882eb19fad6b08a
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Mon,  1 Jun 2026 14:18:08 -0500

feat(spike): adatcheck — diagnose hot ADAT output (routing + DAW levels)

Read-only: shows what feeds each ADAT out (PCM-direct like FC, vs a mix bus
that would sum the wall-of-unity mixer to a hot level) and compares DAW send
levels PCM 13-20 (ADAT) vs PCM 1-2 (main/HP). Pinpoints whether the loud ADAT
is induced routing, DAW levels, or the external converter.

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

Diffstat:
Mspike/Cargo.toml | 5+++++
Aspike/src/bin/adatcheck.rs | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 90 insertions(+), 0 deletions(-)

diff --git a/spike/Cargo.toml b/spike/Cargo.toml @@ -37,6 +37,11 @@ path = "src/bin/muxcheck.rs" name = "presetcheck" path = "src/bin/presetcheck.rs" +# Read-only ADAT-output routing + level diagnostic. +[[bin]] +name = "adatcheck" +path = "src/bin/adatcheck.rs" + [dependencies] rusb = { version = "0.9", features = ["vendored"] } anyhow.workspace = true diff --git a/spike/src/bin/adatcheck.rs b/spike/src/bin/adatcheck.rs @@ -0,0 +1,85 @@ +//! ADAT-output diagnostic (READ-ONLY). Answers: are the ADAT outputs fed +//! straight from the DAW (PCM, like a normal Focusrite Control setup) or through +//! a mix bus (which, with the wall-of-unity mixer, would sum to a hot level)? +//! Also compares the meter levels feeding ADAT (PCM 13-20) vs headphones/main +//! (PCM 1-2) so we can see if the DAW itself is sending ADAT hotter. +//! +//! Run with Focusrite Control quit, ideally while playing audio: +//! cargo run -p spike --bin adatcheck + +use scarlett_core::matrix::mixer_value_to_db; +use scarlett_core::meter::pcm_capture_level; +use scarlett_core::model::S18I20_GEN3; +use scarlett_core::mux::{id_to_num, num_dsts, Dir, MuxState, PORT_COUNT_18I20_GEN3}; +use scarlett_core::ports::source_name; +use scarlett_core::{Scarlett, UsbTransport}; + +fn main() { + if let Err(e) = run() { + eprintln!("\x1b[31mADATCHECK FAILED:\x1b[0m {e}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box<dyn std::error::Error>> { + let mut dev = Scarlett::new(UsbTransport::open_default()?); + dev.init()?; + let pc = PORT_COUNT_18I20_GEN3; + println!("connected: {}\n", S18I20_GEN3.name); + + // 1. What feeds each ADAT output right now? + let entries = dev.get_mux(num_dsts(&pc))?; + let st = MuxState::from_entries(pc, &entries); + println!("=== ADAT OUTPUT ROUTING ==="); + let mut via_mix = 0; + for i in 0..8u16 { + let out_num = id_to_num(&pc, Dir::Out, 0x200 + i).unwrap_or(0); + let src_num = st.get(out_num); + let src_id = id_to_num(&pc, Dir::In, src_num) // round-trip for label + .map(|_| src_num) + .unwrap_or(src_num); + let src_hw = scarlett_core::mux::num_to_id(&pc, Dir::In, src_id); + let name = source_name(src_hw); + let is_mix = (src_hw & 0xf00) == 0x300; + if is_mix { + via_mix += 1; + } + println!(" ADAT Out {} ← {name}{}", i + 1, if is_mix { " <-- via MIXER" } else { "" }); + } + if via_mix > 0 { + println!( + "\n\x1b[33m{via_mix}/8 ADAT outs are fed by a MIX bus.\x1b[0m With the wall-of-unity\n\ + mixer this sums many sources → HOT. Focusrite Control feeds ADAT from\n\ + PCM directly, which is why FC wasn't loud. Fix: route ADAT ← PCM\n\ + (Routing tab), or lower those mix buses." + ); + } else { + println!("\n\x1b[32mAll ADAT outs are fed directly from PCM (like FC).\x1b[0m"); + println!("So the level is the DAW's output or the external converter, not routing."); + } + + // 2. Compare DAW send levels: PCM 13-20 (ADAT) vs PCM 1-2 (main/HP). + println!("\n=== DAW SEND LEVELS (play audio; peak over ~4s) ==="); + let mut peak = vec![0u32; S18I20_GEN3.meter_count as usize]; + for _ in 0..20 { + let raw = dev.get_meters(S18I20_GEN3.meter_count)?; + for (i, &v) in raw.iter().enumerate() { + if v > peak[i] { + peak[i] = v; + } + } + for _ in 0..4 { + let _ = dev.get_meters(4); + } + } + let db = |raw: u32| scarlett_core::meter::raw_to_dbfs(raw); + let pk = |n| pcm_capture_level(&peak, n); + println!(" PCM 1 (main/HP L): {:>6.1} dBFS", db(pk(1))); + println!(" PCM 2 (main/HP R): {:>6.1} dBFS", db(pk(2))); + println!(" PCM 13 (ADAT 1): {:>6.1} dBFS", db(pk(13))); + println!(" PCM 14 (ADAT 2): {:>6.1} dBFS", db(pk(14))); + let _ = mixer_value_to_db; // (kept for parity) + + println!("\nADATCHECK DONE (read-only)."); + Ok(()) +}