valentine

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

sources.rs (6684B)


      1 //! The full catalog of input *sources* on the device — every signal that can be
      2 //! metered, named, faded into the monitor mix, and routed: analogue preamps,
      3 //! ADAT, S/PDIF, and the DAW (PCM) returns.
      4 //!
      5 //! The old Inputs page only listed the 8 analogue preamps, so ADAT/SPDIF/PCM
      6 //! sources were invisible. This module is the single source of truth for "what
      7 //! inputs exist and what each can do", built from the device descriptor.
      8 
      9 use crate::model::{DeviceInfo, PortType};
     10 
     11 /// One input source: its kind, zero-based index within that kind, and which
     12 /// preamp features it supports (only analogue preamps have these).
     13 #[derive(Debug, Clone, PartialEq, Eq)]
     14 pub struct Source {
     15     pub kind: PortType,
     16     /// 0-based index within the kind (analogue 0 = "Analogue 1").
     17     pub index: u16,
     18     /// Default display name, e.g. "Analogue 1", "ADAT 3", "PCM 12".
     19     pub name: String,
     20     /// Preamp capabilities (false for everything except analogue preamps).
     21     pub has_air: bool,
     22     pub has_pad: bool,
     23     pub has_inst: bool,
     24     /// 48 V phantom group this source belongs to, if any (analogue only).
     25     pub phantom_group: Option<u8>,
     26 }
     27 
     28 impl Source {
     29     /// True if this source has any preamp switch worth showing.
     30     pub fn has_preamp(&self) -> bool {
     31         self.has_air || self.has_pad || self.has_inst || self.phantom_group.is_some()
     32     }
     33 
     34     /// The device hardware ID for this source (kind base | index), matching the
     35     /// mux/routing encoding. Used to find where the source is routed.
     36     pub fn hw_id(&self) -> u16 {
     37         let base = match self.kind {
     38             PortType::Analogue => 0x080,
     39             PortType::Spdif => 0x180,
     40             PortType::Adat => 0x200,
     41             PortType::Mix => 0x300,
     42             PortType::Pcm => 0x600,
     43         };
     44         base | self.index
     45     }
     46 }
     47 
     48 fn kind_word(kind: PortType) -> &'static str {
     49     match kind {
     50         PortType::Analogue => "Analogue",
     51         PortType::Spdif => "S/PDIF",
     52         PortType::Adat => "ADAT",
     53         PortType::Pcm => "PCM",
     54         PortType::Mix => "Mix",
     55     }
     56 }
     57 
     58 /// Build the ordered source catalog for a device: analogue (incl. talkback as
     59 /// the last analogue), then ADAT, S/PDIF, then PCM/DAW returns.
     60 pub fn catalog(info: &DeviceInfo) -> Vec<Source> {
     61     let mut out = Vec::new();
     62 
     63     // Analogue sources (the descriptor's analogue *source* count includes the
     64     // talkback mic as the last one on the 18i20 g3).
     65     let analogue_srcs = info.port_count(PortType::Analogue).0 as u16;
     66     for i in 0..analogue_srcs {
     67         let is_talkback = info.has_talkback && i + 1 == analogue_srcs;
     68         let name = if is_talkback {
     69             "Talkback".to_string()
     70         } else {
     71             format!("Analogue {}", i + 1)
     72         };
     73         out.push(Source {
     74             kind: PortType::Analogue,
     75             index: i,
     76             name,
     77             has_air: i < info.air_input_count as u16 && !is_talkback,
     78             has_pad: i < info.pad_input_count as u16 && !is_talkback,
     79             has_inst: i < info.level_input_count as u16 && !is_talkback,
     80             phantom_group: if !is_talkback && info.inputs_per_phantom > 0 {
     81                 let g = (i / info.inputs_per_phantom as u16) as u8;
     82                 (g < info.phantom_count).then_some(g)
     83             } else {
     84                 None
     85             },
     86         });
     87     }
     88 
     89     // ADAT, then S/PDIF, then PCM — meter/route/name only, no preamp.
     90     for kind in [PortType::Adat, PortType::Spdif, PortType::Pcm] {
     91         let n = info.port_count(kind).0 as u16;
     92         for i in 0..n {
     93             out.push(Source {
     94                 kind,
     95                 index: i,
     96                 name: format!("{} {}", kind_word(kind), i + 1),
     97                 has_air: false,
     98                 has_pad: false,
     99                 has_inst: false,
    100                 phantom_group: None,
    101             });
    102         }
    103     }
    104 
    105     out
    106 }
    107 
    108 /// A stereo pairing of catalog entries: consecutive odd/even within the same
    109 /// kind (1-2, 3-4 …). An odd trailing source becomes a mono pair.
    110 #[derive(Debug, Clone, PartialEq, Eq)]
    111 pub struct Pair {
    112     pub left: usize,
    113     /// `None` for a leftover mono source (or talkback).
    114     pub right: Option<usize>,
    115 }
    116 
    117 /// Group catalog indices into stereo pairs by kind. Talkback stays mono.
    118 pub fn stereo_pairs(catalog: &[Source]) -> Vec<Pair> {
    119     let mut pairs = Vec::new();
    120     let mut i = 0;
    121     while i < catalog.len() {
    122         let a = &catalog[i];
    123         let mono = a.name == "Talkback";
    124         if !mono
    125             && i + 1 < catalog.len()
    126             && catalog[i + 1].kind == a.kind
    127             && a.index % 2 == 0
    128             && catalog[i + 1].index == a.index + 1
    129         {
    130             pairs.push(Pair { left: i, right: Some(i + 1) });
    131             i += 2;
    132         } else {
    133             pairs.push(Pair { left: i, right: None });
    134             i += 1;
    135         }
    136     }
    137     pairs
    138 }
    139 
    140 #[cfg(test)]
    141 mod tests {
    142     use super::*;
    143     use crate::model::S18I20_GEN3;
    144 
    145     #[test]
    146     fn catalog_includes_adat_spdif_pcm_not_just_analogue() {
    147         let c = catalog(&S18I20_GEN3);
    148         let kinds: std::collections::HashSet<_> = c.iter().map(|s| s.kind).collect();
    149         assert!(kinds.contains(&PortType::Analogue));
    150         assert!(kinds.contains(&PortType::Adat)); // the user's missing inputs
    151         assert!(kinds.contains(&PortType::Spdif));
    152         assert!(kinds.contains(&PortType::Pcm));
    153         // 9 analogue + 8 adat + 2 spdif + 20 pcm = 39
    154         assert_eq!(c.len(), 9 + 8 + 2 + 20);
    155     }
    156 
    157     #[test]
    158     fn talkback_is_last_analogue_and_has_no_preamp() {
    159         let c = catalog(&S18I20_GEN3);
    160         let tb = c.iter().find(|s| s.name == "Talkback").unwrap();
    161         assert_eq!(tb.kind, PortType::Analogue);
    162         assert!(!tb.has_preamp());
    163     }
    164 
    165     #[test]
    166     fn analogue_1_has_full_preamp_adat_has_none() {
    167         let c = catalog(&S18I20_GEN3);
    168         let a1 = &c[0];
    169         assert_eq!(a1.name, "Analogue 1");
    170         assert!(a1.has_air && a1.has_pad && a1.has_inst);
    171         assert_eq!(a1.phantom_group, Some(0));
    172 
    173         let adat = c.iter().find(|s| s.kind == PortType::Adat).unwrap();
    174         assert!(!adat.has_preamp());
    175     }
    176 
    177     #[test]
    178     fn stereo_pairs_group_odd_even_within_kind() {
    179         let c = catalog(&S18I20_GEN3);
    180         let pairs = stereo_pairs(&c);
    181         // Analogue 1-2 should be a stereo pair.
    182         let first = &pairs[0];
    183         assert_eq!(c[first.left].name, "Analogue 1");
    184         assert_eq!(first.right.map(|r| c[r].name.clone()), Some("Analogue 2".into()));
    185         // Talkback (9th analogue, odd one out) must be mono.
    186         let tb_pair = pairs.iter().find(|p| c[p.left].name == "Talkback").unwrap();
    187         assert_eq!(tb_pair.right, None);
    188     }
    189 }