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 }