valentine

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

meter.rs (10011B)


      1 //! Meter level mapping — turn the raw `GET_METER` array into per-destination
      2 //! levels, faithfully porting the kernel's `scarlett2_update_meter_level_map`.
      3 //!
      4 //! The device returns `meter_count` raw u32 levels in **`meter_map` order** (a
      5 //! list of `(start_destination, count)` spans), NOT in a friendly channel order.
      6 //! To read "the level at destination D" you need a map `dest -> raw_index`, and
      7 //! because the firmware reports a shared source only once, building that map
      8 //! requires the **current routing** (`mux[dest] = source`). This module builds
      9 //! that map so the UI can show a meter for any destination (and thus any input,
     10 //! by finding a destination it feeds).
     11 //!
     12 //! All values here are interoperability facts from the GPL kernel driver; the
     13 //! algorithm is re-implemented, not copied.
     14 
     15 /// One span of the device's meter report: `count` consecutive destinations
     16 /// starting at destination index `start`.
     17 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     18 pub struct MeterSpan {
     19     pub start: u16,
     20     pub count: u16,
     21 }
     22 
     23 /// The 18i20 g3 `meter_map` (from `s18i20_gen3_info.meter_map`). Sum of counts
     24 /// = 65 = the GET_METER length.
     25 pub const METER_MAP_18I20_GEN3: &[MeterSpan] = &[
     26     MeterSpan { start: 45, count: 8 },
     27     MeterSpan { start: 55, count: 10 },
     28     MeterSpan { start: 0, count: 20 },
     29     MeterSpan { start: 53, count: 2 },
     30     MeterSpan { start: 20, count: 25 },
     31 ];
     32 
     33 /// Sentinel meaning "no live source → level is zero".
     34 pub const MAP_NONE: u16 = u16::MAX;
     35 
     36 /// Build `meter_level_map[dest] = raw_index` for `num_dsts` destinations.
     37 ///
     38 /// `mux[dest]` is the source number currently routed to that destination (the
     39 /// value `0` conventionally meaning "Off"/None). Mirrors the kernel: walk the
     40 /// meter_map spans assigning each destination the raw index `i`; if a source is
     41 /// already seen at an earlier raw index, reuse it; sources that are Off map to
     42 /// [`MAP_NONE`].
     43 pub fn build_level_map(map: &[MeterSpan], mux: &[u16], num_dsts: usize) -> Vec<u16> {
     44     let mut level_map = vec![MAP_NONE; num_dsts];
     45 
     46     // seen_src[src] -> Some(raw_index) once a source has been assigned. Source 0
     47     // ("Off") is pre-seen as None so it always maps to zero.
     48     let max_src = mux.iter().copied().max().unwrap_or(0) as usize + 1;
     49     let mut seen: Vec<Option<u16>> = vec![None; max_src.max(1)];
     50     seen[0] = Some(MAP_NONE); // "Off" → zero level
     51 
     52     let mut i: u16 = 0; // index into the raw GET_METER response
     53     for span in map {
     54         for j in 0..span.count {
     55             let dest = (span.start + j) as usize;
     56             let src = mux.get(dest).copied().unwrap_or(0) as usize;
     57 
     58             if src < seen.len() && seen[src].is_none() {
     59                 seen[src] = Some(i);
     60             }
     61             let raw_idx = src.lt(&seen.len()).then(|| seen[src]).flatten().unwrap_or(MAP_NONE);
     62             if dest < level_map.len() {
     63                 level_map[dest] = raw_idx;
     64             }
     65             i += 1;
     66         }
     67     }
     68     level_map
     69 }
     70 
     71 /// Resolve the level at `dest` given a built `level_map` and the raw meter array.
     72 pub fn level_at(level_map: &[u16], raw: &[u32], dest: usize) -> u32 {
     73     match level_map.get(dest).copied() {
     74         Some(MAP_NONE) | None => 0,
     75         Some(idx) => raw.get(idx as usize).copied().unwrap_or(0),
     76     }
     77 }
     78 
     79 /// Raw GET_METER index for **Mixer Input `n`** (1-based) on the 18i20 g3.
     80 ///
     81 /// Verified on hardware (2026-06-01): the mixer-input destinations (20..44) are
     82 /// reported as a contiguous span beginning at raw index 40, in order — so mixer
     83 /// input 1 = raw 40, input 25 = raw 64. Feeding analogue input 3 lit raw 42
     84 /// (mixer input 3), confirming the analogue-N → mixer-input-N default and this
     85 /// direct mapping (no routing inversion needed).
     86 pub const MIXER_INPUT_METER_BASE: usize = 40;
     87 
     88 /// Raw meter index for 1-based mixer input `n` (1..=25). Returns None out of range.
     89 pub fn mixer_input_raw_index(n: u16) -> Option<usize> {
     90     if (1..=25).contains(&n) {
     91         Some(MIXER_INPUT_METER_BASE + (n as usize - 1))
     92     } else {
     93         None
     94     }
     95 }
     96 
     97 /// Level for 1-based mixer input `n` from a raw GET_METER array.
     98 pub fn mixer_input_level(raw: &[u32], n: u16) -> u32 {
     99     mixer_input_raw_index(n)
    100         .and_then(|i| raw.get(i).copied())
    101         .unwrap_or(0)
    102 }
    103 
    104 /// Raw GET_METER index for **PCM capture channel `n`** (1-based, 1..=20) — what
    105 /// the DAW records. This is the right per-input meter: every physical input is
    106 /// routed to a PCM capture, whereas only some reach the mixer.
    107 ///
    108 /// Destination layout (verified 2026-06-01): PCM-capture destinations are
    109 /// numbered 45..64; their positions in the meter report are:
    110 ///   PCM 1-8  → raw 0-7,  PCM 9-10 → raw 38-39,  PCM 11-20 → raw 8-17.
    111 /// Confirmed: Analogue 3 → PCM cap 3 lit raw 2; ADAT 1 → PCM cap 13 lit raw 10.
    112 pub fn pcm_capture_raw_index(n: u16) -> Option<usize> {
    113     match n {
    114         1..=8 => Some((n - 1) as usize),
    115         9..=10 => Some(38 + (n - 9) as usize),
    116         11..=20 => Some(8 + (n - 11) as usize),
    117         _ => None,
    118     }
    119 }
    120 
    121 /// Level for 1-based PCM capture channel `n` from a raw GET_METER array.
    122 pub fn pcm_capture_level(raw: &[u32], n: u16) -> u32 {
    123     pcm_capture_raw_index(n)
    124         .and_then(|i| raw.get(i).copied())
    125         .unwrap_or(0)
    126 }
    127 
    128 /// Linear full-scale (0 dBFS) of the raw meter value. The device's meter is
    129 /// 12-bit: it saturates at ~4095 = 0 dBFS, so an overloading input pegs there.
    130 /// Calibrated against a user overload reference (clipping → red). A small error
    131 /// just shifts the dB readout by a constant.
    132 pub const METER_FULL_SCALE: f32 = 4095.0;
    133 
    134 /// dBFS floor returned for silence / unrouted inputs.
    135 pub const METER_DB_FLOOR: f32 = -90.0;
    136 
    137 /// Convert a raw (linear-amplitude) meter value to dBFS. Returns
    138 /// [`METER_DB_FLOOR`] for zero. Use a dB scale for bars so they match a DAW VU.
    139 pub fn raw_to_dbfs(raw: u32) -> f32 {
    140     if raw == 0 {
    141         return METER_DB_FLOOR;
    142     }
    143     (20.0 * (raw as f32 / METER_FULL_SCALE).log10()).max(METER_DB_FLOOR)
    144 }
    145 
    146 #[cfg(test)]
    147 mod tests {
    148     use super::*;
    149 
    150     #[test]
    151     fn map_sums_to_65() {
    152         let total: u16 = METER_MAP_18I20_GEN3.iter().map(|s| s.count).sum();
    153         assert_eq!(total, 65);
    154     }
    155 
    156     #[test]
    157     fn identity_routing_assigns_increasing_raw_indices() {
    158         // A simple map: one span of 4 destinations starting at 0, each fed by a
    159         // distinct nonzero source.
    160         let map = [MeterSpan { start: 0, count: 4 }];
    161         let mux = vec![1u16, 2, 3, 4]; // dest0<-src1, dest1<-src2, ...
    162         let lm = build_level_map(&map, &mux, 4);
    163         // raw index increments with position
    164         assert_eq!(lm, vec![0, 1, 2, 3]);
    165         // and level_at pulls the right raw value
    166         let raw = vec![10u32, 20, 30, 40];
    167         assert_eq!(level_at(&lm, &raw, 2), 30);
    168     }
    169 
    170     #[test]
    171     fn off_source_maps_to_zero() {
    172         let map = [MeterSpan { start: 0, count: 3 }];
    173         let mux = vec![0u16, 5, 0]; // dest0 Off, dest1<-src5, dest2 Off
    174         let lm = build_level_map(&map, &mux, 3);
    175         assert_eq!(lm[0], MAP_NONE);
    176         assert_eq!(lm[2], MAP_NONE);
    177         // raw index increments for EVERY destination (incl. Off ones), so src5
    178         // at dest1 sits at raw index 1.
    179         assert_eq!(lm[1], 1);
    180         let raw = vec![99u32, 77, 55];
    181         assert_eq!(level_at(&lm, &raw, 0), 0); // Off → 0
    182         assert_eq!(level_at(&lm, &raw, 1), 77); // src5 at raw index 1
    183     }
    184 
    185     #[test]
    186     fn mixer_input_meter_index_matches_hardware() {
    187         // Verified on the real 18i20: mixer input 1 = raw 40, input 3 = raw 42,
    188         // input 25 = raw 64.
    189         assert_eq!(mixer_input_raw_index(1), Some(40));
    190         assert_eq!(mixer_input_raw_index(3), Some(42));
    191         assert_eq!(mixer_input_raw_index(25), Some(64));
    192         assert_eq!(mixer_input_raw_index(0), None);
    193         assert_eq!(mixer_input_raw_index(26), None);
    194 
    195         let mut raw = vec![0u32; 65];
    196         raw[42] = 307; // what feeding analogue input 3 produced
    197         assert_eq!(mixer_input_level(&raw, 3), 307);
    198         assert_eq!(mixer_input_level(&raw, 4), 0);
    199     }
    200 
    201     #[test]
    202     fn pcm_capture_meter_index_matches_hardware() {
    203         // Verified on the real 18i20 (2026-06-01):
    204         //   Analogue 3 → PCM cap 3 lit raw 2; ADAT 1 → PCM cap 13 lit raw 10.
    205         assert_eq!(pcm_capture_raw_index(3), Some(2));
    206         assert_eq!(pcm_capture_raw_index(13), Some(10));
    207         assert_eq!(pcm_capture_raw_index(1), Some(0));
    208         assert_eq!(pcm_capture_raw_index(8), Some(7));
    209         assert_eq!(pcm_capture_raw_index(9), Some(38));
    210         assert_eq!(pcm_capture_raw_index(20), Some(17));
    211         assert_eq!(pcm_capture_raw_index(0), None);
    212         assert_eq!(pcm_capture_raw_index(21), None);
    213 
    214         let mut raw = vec![0u32; 65];
    215         raw[2] = 548; // analogue 3 reading from the probe
    216         raw[10] = 73; // adat 1 reading
    217         assert_eq!(pcm_capture_level(&raw, 3), 548);
    218         assert_eq!(pcm_capture_level(&raw, 13), 73);
    219     }
    220 
    221     #[test]
    222     fn dbfs_scale_anchored_at_full_scale() {
    223         // Calibrated so the 12-bit clip point (~4095) = 0 dBFS (verified: an
    224         // overloading input pegs the meter and reads ~0 dB / red).
    225         assert!(raw_to_dbfs(METER_FULL_SCALE as u32).abs() < 0.01);
    226         assert_eq!(raw_to_dbfs(0), METER_DB_FLOOR);
    227         // half amplitude ≈ −6 dB
    228         assert!((raw_to_dbfs((METER_FULL_SCALE / 2.0) as u32) - (-6.0)).abs() < 0.2);
    229     }
    230 
    231     #[test]
    232     fn shared_source_reuses_first_raw_index() {
    233         // Same source feeds two destinations; both should read the SAME raw idx
    234         // (the first time it was seen), per the kernel's dedup.
    235         let map = [MeterSpan { start: 0, count: 3 }];
    236         let mux = vec![7u16, 7, 8]; // dest0<-src7, dest1<-src7, dest2<-src8
    237         let lm = build_level_map(&map, &mux, 3);
    238         assert_eq!(lm[0], 0); // src7 first seen at raw 0
    239         assert_eq!(lm[1], 0); // src7 again → reuse raw 0
    240         assert_eq!(lm[2], 2); // src8 at its own position (raw idx 2)
    241     }
    242 }