valentine

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

commit 0bcf3fd3f6d2d95b646e6d4025567e301c3eb53a
parent 872781d3d87cfc3d871c0c628a5ce49c9c96eb49
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Mon,  1 Jun 2026 10:25:48 -0500

feat(spike): metermap v2 — dump routing + peak-hold every raw index

Prints full decoded routing (sink<-source) and peak-holds all 65 GET_METER
indices for ~12s, labeling each by destination + routed source. Lets us see
exactly where ADAT is routed and which raw index moves (channel mapping) plus
real signal peaks (full-scale calibration vs DAW). Read-only.

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

Diffstat:
Mspike/src/bin/metermap.rs | 165++++++++++++++++++++++++++++++++++++++++++-------------------------------------
1 file changed, 87 insertions(+), 78 deletions(-)

diff --git a/spike/src/bin/metermap.rs b/spike/src/bin/metermap.rs @@ -1,17 +1,16 @@ -//! 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. +//! Meter-map probe v2 (read-only). Two parts: +//! 1. Prints your full decoded routing (sink ← source) so we can see where +//! each physical input (esp. ADAT) is actually routed. +//! 2. Peak-holds all 65 raw GET_METER indices over ~12s and prints each with +//! its destination + the source currently routed there — so when you feed a +//! specific input we can see EXACTLY which raw index moves and how high +//! (for both channel mapping AND full-scale calibration vs your DAW). //! //! 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; +//! Feed ONE input you can name (e.g. ADAT 1) the whole time. Reads only. -use scarlett_core::meter::{build_level_map, level_at, METER_MAP_18I20_GEN3}; -use scarlett_core::model::S18I20_GEN3; +use scarlett_core::model::{PortType, S18I20_GEN3}; +use scarlett_core::ports::{sink_name, source_name}; use scarlett_core::{Scarlett, UsbTransport}; fn main() { @@ -22,98 +21,108 @@ fn main() { } } +/// Destination index → human label, in the gen3c destination numbering +/// (Analogue Out 1-10, S/PDIF Out 1-2, ADAT Out 1-8, Mixer In 1-25, PCM cap 1-20). +fn dest_label(d: usize) -> String { + let an = S18I20_GEN3.port_count(PortType::Analogue).1 as usize; // 10 + let sp = S18I20_GEN3.port_count(PortType::Spdif).1 as usize; // 2 + let ad = S18I20_GEN3.port_count(PortType::Adat).1 as usize; // 8 + let mx = S18I20_GEN3.port_count(PortType::Mix).1 as usize; // 25 + let mut base = 0; + if d < base + an { + return format!("AnalogueOut {}", d - base + 1); + } + base += an; + if d < base + sp { + return format!("S/PDIF Out {}", d - base + 1); + } + base += sp; + if d < base + ad { + return format!("ADAT Out {}", d - base + 1); + } + base += ad; + if d < base + mx { + return format!("MixerIn {}", d - base + 1); + } + base += mx; + format!("PCM cap {}", d - base + 1) +} + fn run() -> Result<(), Box<dyn std::error::Error>> { let mut dev = Scarlett::new(UsbTransport::open_default()?); dev.init()?; - println!("connected: {}", S18I20_GEN3.name); + println!("connected: {}\n", 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. - // CORRECTED destination layout (verified 2026-06-01): destinations number in - // port order — Analogue Out 1-10 (0..9), S/PDIF Out (10..11), ADAT Out 1-8 - // (12..19), Mixer Inputs 1-25 (20..44), PCM capture 1-20 (45..64). - let span_hint = [ - "span0 (dst 45..52) — PCM capture 1-8 (DAW)", - "span1 (dst 55..64) — PCM capture 11-20 (DAW)", - "span2 (dst 0..19) — Analogue Out 1-10, S/PDIF Out, ADAT Out 1-8", - "span3 (dst 53..54) — PCM capture 9-10", - "span4 (dst 20..44) — Mixer Inputs 1-25 ← per-input meters (raw 40..64)", - ]; - - // 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). + // --- Part 1: full routing, decoded --- 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 mux = dev.get_mux(num_dsts)?; + println!("=== ROUTING (sink ← source), non-Off only ==="); + for e in &mux { + if e.source != 0 { + println!( + " {:<16} ← {:<16} (dest hw {:#05x} / src hw {:#05x})", + sink_name(e.dest), + source_name(e.source), + e.dest, + 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. + // --- Part 2: peak-hold every raw index over ~12s while you feed an input --- let n = S18I20_GEN3.meter_count as usize; let mut peak = vec![0u32; n]; - - for s in 0..20 { + println!("\n=== Feed ONE input now. Holding peaks for ~12s… ==="); + // ~12s: 60 reads with 4 throwaway reads between each as a crude delay. + for _ in 0..60 { 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 40..64) - let live: Vec<String> = raw - .iter() - .skip(40) - .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 { + for _ in 0..6 { 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(); - } + // Print every raw index that saw signal, with which destination it is and + // what source is routed there — sorted by peak descending. + println!("\n=== RAW INDICES THAT MOVED (peak desc) ==="); + // raw index i corresponds to the i-th destination in meter_map order; we + // reconstruct that destination list to label each raw index. + let meter_map: [(u16, u16); 5] = [(45, 8), (55, 10), (0, 20), (53, 2), (20, 25)]; + let mut raw_to_dest = Vec::with_capacity(n); + for (start, count) in meter_map { + for j in 0..count { + raw_to_dest.push((start + j) as usize); } - if !line.is_empty() { - println!(" {line}"); + } + // source routed to a dest (for labeling), from the mux table + let mut src_at = vec![0u16; num_dsts]; + for e in &mux { + if (e.dest as usize) < src_at.len() { + src_at[e.dest as usize] = e.source; } - 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}"); + let mut rows: Vec<(u32, usize, usize)> = peak + .iter() + .enumerate() + .filter(|(_, &p)| p > 20) + .map(|(i, &p)| (p, i, raw_to_dest.get(i).copied().unwrap_or(usize::MAX))) + .collect(); + rows.sort_by(|a, b| b.0.cmp(&a.0)); + + for (p, raw_i, dest) in rows { + let src = src_at.get(dest).copied().unwrap_or(0); + let src_txt = if src == 0 { "(off/none)".to_string() } else { source_name(src) }; + let dlabel = if dest == usize::MAX { "?".to_string() } else { dest_label(dest) }; + println!( + " raw{raw_i:>2} peak {p:>5} dest{dest:>2} {dlabel:<16} source: {src_txt}" + ); } - println!("\nMETERMAP DONE (read-only; nothing changed)."); + println!("\nMETERMAP DONE (read-only). Tell me which input you fed."); Ok(()) }