valentine

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

commit ec11802b58ca5856299b2473146e63c30c0dadce
parent 019b2184f709572d24c47b51cbd50164d2c82761
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Sun, 31 May 2026 21:23:16 -0500

feat(core): port kernel meter-level remap (dest->raw index)

scarlett-core/src/meter.rs faithfully reimplements
scarlett2_update_meter_level_map: build_level_map() turns the live routing
(mux[dest]=src) + the device meter_map into dest->raw-index, with source
dedup and Off->zero. Tested (4 cases). Foundation for per-channel meters;
source->meter still needs routing inversion + hardware verification.

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

Diffstat:
Mscarlett-core/src/lib.rs | 1+
Ascarlett-core/src/meter.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 130 insertions(+), 0 deletions(-)

diff --git a/scarlett-core/src/lib.rs b/scarlett-core/src/lib.rs @@ -22,6 +22,7 @@ pub mod controls; pub mod matrix; +pub mod meter; pub mod model; pub mod packet; pub mod ports; diff --git a/scarlett-core/src/meter.rs b/scarlett-core/src/meter.rs @@ -0,0 +1,129 @@ +//! Meter level mapping — turn the raw `GET_METER` array into per-destination +//! levels, faithfully porting the kernel's `scarlett2_update_meter_level_map`. +//! +//! The device returns `meter_count` raw u32 levels in **`meter_map` order** (a +//! list of `(start_destination, count)` spans), NOT in a friendly channel order. +//! To read "the level at destination D" you need a map `dest -> raw_index`, and +//! because the firmware reports a shared source only once, building that map +//! requires the **current routing** (`mux[dest] = source`). This module builds +//! that map so the UI can show a meter for any destination (and thus any input, +//! by finding a destination it feeds). +//! +//! All values here are interoperability facts from the GPL kernel driver; the +//! algorithm is re-implemented, not copied. + +/// One span of the device's meter report: `count` consecutive destinations +/// starting at destination index `start`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MeterSpan { + pub start: u16, + pub count: u16, +} + +/// The 18i20 g3 `meter_map` (from `s18i20_gen3_info.meter_map`). Sum of counts +/// = 65 = the GET_METER length. +pub const METER_MAP_18I20_GEN3: &[MeterSpan] = &[ + MeterSpan { start: 45, count: 8 }, + MeterSpan { start: 55, count: 10 }, + MeterSpan { start: 0, count: 20 }, + MeterSpan { start: 53, count: 2 }, + MeterSpan { start: 20, count: 25 }, +]; + +/// Sentinel meaning "no live source → level is zero". +pub const MAP_NONE: u16 = u16::MAX; + +/// Build `meter_level_map[dest] = raw_index` for `num_dsts` destinations. +/// +/// `mux[dest]` is the source number currently routed to that destination (the +/// value `0` conventionally meaning "Off"/None). Mirrors the kernel: walk the +/// meter_map spans assigning each destination the raw index `i`; if a source is +/// already seen at an earlier raw index, reuse it; sources that are Off map to +/// [`MAP_NONE`]. +pub fn build_level_map(map: &[MeterSpan], mux: &[u16], num_dsts: usize) -> Vec<u16> { + let mut level_map = vec![MAP_NONE; num_dsts]; + + // seen_src[src] -> Some(raw_index) once a source has been assigned. Source 0 + // ("Off") is pre-seen as None so it always maps to zero. + let max_src = mux.iter().copied().max().unwrap_or(0) as usize + 1; + let mut seen: Vec<Option<u16>> = vec![None; max_src.max(1)]; + seen[0] = Some(MAP_NONE); // "Off" → zero level + + let mut i: u16 = 0; // index into the raw GET_METER response + for span in map { + for j in 0..span.count { + let dest = (span.start + j) as usize; + let src = mux.get(dest).copied().unwrap_or(0) as usize; + + if src < seen.len() && seen[src].is_none() { + seen[src] = Some(i); + } + let raw_idx = src.lt(&seen.len()).then(|| seen[src]).flatten().unwrap_or(MAP_NONE); + if dest < level_map.len() { + level_map[dest] = raw_idx; + } + i += 1; + } + } + level_map +} + +/// Resolve the level at `dest` given a built `level_map` and the raw meter array. +pub fn level_at(level_map: &[u16], raw: &[u32], dest: usize) -> u32 { + match level_map.get(dest).copied() { + Some(MAP_NONE) | None => 0, + Some(idx) => raw.get(idx as usize).copied().unwrap_or(0), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn map_sums_to_65() { + let total: u16 = METER_MAP_18I20_GEN3.iter().map(|s| s.count).sum(); + assert_eq!(total, 65); + } + + #[test] + fn identity_routing_assigns_increasing_raw_indices() { + // A simple map: one span of 4 destinations starting at 0, each fed by a + // distinct nonzero source. + let map = [MeterSpan { start: 0, count: 4 }]; + let mux = vec![1u16, 2, 3, 4]; // dest0<-src1, dest1<-src2, ... + let lm = build_level_map(&map, &mux, 4); + // raw index increments with position + assert_eq!(lm, vec![0, 1, 2, 3]); + // and level_at pulls the right raw value + let raw = vec![10u32, 20, 30, 40]; + assert_eq!(level_at(&lm, &raw, 2), 30); + } + + #[test] + fn off_source_maps_to_zero() { + let map = [MeterSpan { start: 0, count: 3 }]; + let mux = vec![0u16, 5, 0]; // dest0 Off, dest1<-src5, dest2 Off + let lm = build_level_map(&map, &mux, 3); + assert_eq!(lm[0], MAP_NONE); + assert_eq!(lm[2], MAP_NONE); + // raw index increments for EVERY destination (incl. Off ones), so src5 + // at dest1 sits at raw index 1. + assert_eq!(lm[1], 1); + let raw = vec![99u32, 77, 55]; + assert_eq!(level_at(&lm, &raw, 0), 0); // Off → 0 + assert_eq!(level_at(&lm, &raw, 1), 77); // src5 at raw index 1 + } + + #[test] + fn shared_source_reuses_first_raw_index() { + // Same source feeds two destinations; both should read the SAME raw idx + // (the first time it was seen), per the kernel's dedup. + let map = [MeterSpan { start: 0, count: 3 }]; + let mux = vec![7u16, 7, 8]; // dest0<-src7, dest1<-src7, dest2<-src8 + let lm = build_level_map(&map, &mux, 3); + assert_eq!(lm[0], 0); // src7 first seen at raw 0 + assert_eq!(lm[1], 0); // src7 again → reuse raw 0 + assert_eq!(lm[2], 2); // src8 at its own position (raw idx 2) + } +}