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:
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(())
+}