valentine

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

commit 406fbc57ff9927f45af34480776e35ea1cfd9e5a
parent ec11802b58ca5856299b2473146e63c30c0dadce
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Sun, 31 May 2026 21:30:11 -0500

feat(spike): read-only metermap probe to verify source->meter mapping

Samples GET_METER 20x and prints raw levels grouped by meter_map spans (with
raw index + destination), plus a peak summary, so we can empirically see which
index moves when an input is fed — verifying the mixer-input meter mapping
before wiring meters into the inputs UI. Reads only; changes nothing.

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

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

diff --git a/spike/Cargo.toml b/spike/Cargo.toml @@ -22,6 +22,11 @@ path = "src/main.rs" name = "hwcheck" path = "src/bin/hwcheck.rs" +# Read-only meter-map probe: watch which raw index moves when you feed an input. +[[bin]] +name = "metermap" +path = "src/bin/metermap.rs" + [dependencies] rusb = { version = "0.9", features = ["vendored"] } anyhow.workspace = true diff --git a/spike/src/bin/metermap.rs b/spike/src/bin/metermap.rs @@ -0,0 +1,116 @@ +//! Meter-map probe (read-only). Samples GET_METER repeatedly and prints the raw +//! levels grouped by the device's meter_map spans, so you can SEE which raw +//! index moves when you feed a physical input — empirically verifying the +//! source→meter mapping before we wire meters into the inputs UI. +//! +//! Run with Focusrite Control quit: cargo run -p spike --bin metermap +//! Then make noise on, e.g., an ADAT input and watch which numbers jump. +//! +//! It only READS (GET_METER + GET_MUX). It changes nothing on the device. + +use std::io::Write; + +use scarlett_core::meter::{build_level_map, level_at, METER_MAP_18I20_GEN3}; +use scarlett_core::model::S18I20_GEN3; +use scarlett_core::{Scarlett, UsbTransport}; + +fn main() { + if let Err(e) = run() { + eprintln!("\x1b[31mMETERMAP FAILED:\x1b[0m {e}"); + eprintln!("(If access/busy: quit Focusrite Control first.)"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box<dyn std::error::Error>> { + let mut dev = Scarlett::new(UsbTransport::open_default()?); + dev.init()?; + println!("connected: {}", S18I20_GEN3.name); + + // Span labels in meter_map order (start, count) → a human hint of what that + // span of destinations is, based on the gen3c destination layout. + let span_hint = [ + "span0 (dst 45..52) — likely Analogue Out 1-8", + "span1 (dst 55..64) — likely PCM/loopback returns", + "span2 (dst 0..19) — likely PCM 1-20", + "span3 (dst 53..54) — likely S/PDIF out", + "span4 (dst 20..44) — likely Mixer Inputs 1-25 ← per-input meters live here", + ]; + + // Read routing once so we can also print the kernel-style level map. + let mux_entries = dev.get_mux(S18I20_GEN3.mux_dst_count())?; + // Build a dest->source array (mux[dest] = source hardware id, low value 0=off). + let num_dsts = S18I20_GEN3.mux_dst_count(); + let mut mux = vec![0u16; num_dsts]; + for e in &mux_entries { + if (e.dest as usize) < mux.len() { + mux[e.dest as usize] = e.source; + } + } + let level_map = build_level_map(METER_MAP_18I20_GEN3, &mux, num_dsts); + + println!("\nSampling meters 20× (~4s). Feed an input and watch the numbers.\n"); + println!("meter_map spans:"); + for h in span_hint { + println!(" {h}"); + } + println!(); + + // Track peak per raw index across samples so brief signals are visible. + let n = S18I20_GEN3.meter_count as usize; + let mut peak = vec![0u32; n]; + + for s 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; + } + } + // live one-liner of the mixer-input span (span4: raw indices 45..65) + let live: Vec<String> = raw + .iter() + .skip(45) + .take(25) + .map(|v| format!("{:>4}", v / 256)) // scale down for readability + .collect(); + print!("\rsample {s:>2}: mix-in[1..25] {}", live.join(" ")); + std::io::stdout().flush().ok(); + // ~200ms between samples without using a sleep helper + for _ in 0..4 { + let _ = dev.get_meters(4); + } + } + println!("\n"); + + // Peak summary, grouped by span, with raw index so you can map exactly. + let mut i = 0usize; + for (si, span) in METER_MAP_18I20_GEN3.iter().enumerate() { + println!("── {} ──", span_hint.get(si).copied().unwrap_or("span")); + let mut line = String::new(); + for j in 0..span.count { + let raw_idx = i + j as usize; + let dest = span.start + j; + line += &format!("[raw{raw_idx:>2} dst{dest:>2}={:>5}] ", peak.get(raw_idx).copied().unwrap_or(0)); + if (j + 1) % 4 == 0 { + println!(" {line}"); + line.clear(); + } + } + if !line.is_empty() { + println!(" {line}"); + } + i += span.count as usize; + } + + // Sanity: show a couple of destinations resolved through the level map. + println!("\nlevel_map sanity (dest → raw idx, 255=off):"); + for dest in [20usize, 21, 22, 45, 46] { + let idx = level_map.get(dest).copied().unwrap_or(u16::MAX); + let lv = level_at(&level_map, &peak, dest); + println!(" dest {dest:>2} → raw {idx} → peak {lv}"); + } + + println!("\nMETERMAP DONE (read-only; nothing changed)."); + Ok(()) +}