valentine

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

ports.rs (3969B)


      1 //! Hardware port identity: decode the 12-bit port IDs used in mux (routing)
      2 //! entries into a human-readable (port-type, index, name) — the data the kernel
      3 //! driver carries in `scarlett2_ports[]`.
      4 //!
      5 //! Every routing assignment is `dest_id | (src_id << 12)` (see [`crate::matrix`]).
      6 //! Each `*_id` is a 12-bit value `base | index`, where the bases are spaced far
      7 //! enough apart that the type is recoverable by range — no direction-count math
      8 //! needed, which makes this layer pure and fully testable.
      9 
     10 /// A category of physical/virtual port, with its ID base and naming.
     11 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     12 pub struct PortKind {
     13     pub base: u16,
     14     /// How a *source* of this kind is named (e.g. "Analogue", "Mix", "PCM").
     15     pub src_word: &'static str,
     16     /// How a *sink* of this kind is named (e.g. "Analogue Output").
     17     pub sink_word: &'static str,
     18 }
     19 
     20 /// The 18i20 g3 port kinds, in mux-enumeration order (matches the kernel's
     21 /// `scarlett2_ports[]`). Order matters: source flat-indexing walks these in turn.
     22 pub const PORT_KINDS: &[PortKind] = &[
     23     PortKind { base: 0x000, src_word: "Off", sink_word: "Off" },
     24     PortKind { base: 0x080, src_word: "Analogue", sink_word: "Analogue Out" },
     25     PortKind { base: 0x180, src_word: "S/PDIF", sink_word: "S/PDIF Out" },
     26     PortKind { base: 0x200, src_word: "ADAT", sink_word: "ADAT Out" },
     27     PortKind { base: 0x300, src_word: "Mix", sink_word: "Mixer In" },
     28     PortKind { base: 0x600, src_word: "PCM", sink_word: "PCM" },
     29 ];
     30 
     31 /// Find the port kind whose ID range contains `id`, plus the index within it.
     32 /// Returns `(kind, index)`. The ranges are the half-open spans between
     33 /// consecutive bases (the last extends to 0xFFF).
     34 fn locate(id: u16) -> Option<(&'static PortKind, u16)> {
     35     let id = id & 0x0fff;
     36     // NONE/Off is exactly the 0x000 region but only index 0 is meaningful.
     37     let mut best: Option<&'static PortKind> = None;
     38     for k in PORT_KINDS {
     39         if id >= k.base {
     40             best = Some(k);
     41         }
     42     }
     43     best.map(|k| (k, id - k.base))
     44 }
     45 
     46 /// Human name for a **source** hardware id (e.g. 0x088 → "Analogue 9",
     47 /// 0x300 → "Mix A", 0x000 → "Off").
     48 pub fn source_name(id: u16) -> String {
     49     name(id, true)
     50 }
     51 
     52 /// Human name for a **sink/destination** hardware id (e.g. 0x080 →
     53 /// "Analogue Out 1", 0x30a → "Mixer In 11").
     54 pub fn sink_name(id: u16) -> String {
     55     name(id, false)
     56 }
     57 
     58 fn name(id: u16, source: bool) -> String {
     59     match locate(id) {
     60         None => format!("?{id:#05x}"),
     61         Some((k, idx)) => {
     62             if k.base == 0x000 {
     63                 return "Off".to_string();
     64             }
     65             let word = if source { k.src_word } else { k.sink_word };
     66             if k.base == 0x300 && source {
     67                 // Mix sources are lettered: Mix A, Mix B, …
     68                 let letter = (b'A' + (idx as u8 % 26)) as char;
     69                 format!("{word} {letter}")
     70             } else {
     71                 format!("{word} {}", idx + 1)
     72             }
     73         }
     74     }
     75 }
     76 
     77 #[cfg(test)]
     78 mod tests {
     79     use super::*;
     80 
     81     #[test]
     82     fn decodes_analogue_source_including_talkback() {
     83         assert_eq!(source_name(0x080), "Analogue 1");
     84         assert_eq!(source_name(0x088), "Analogue 9"); // talkback mic
     85     }
     86 
     87     #[test]
     88     fn decodes_off() {
     89         assert_eq!(source_name(0x000), "Off");
     90         assert_eq!(sink_name(0x000), "Off");
     91     }
     92 
     93     #[test]
     94     fn decodes_mix_sources_as_letters() {
     95         assert_eq!(source_name(0x300), "Mix A");
     96         assert_eq!(source_name(0x309), "Mix J");
     97     }
     98 
     99     #[test]
    100     fn decodes_pcm_and_adat_and_spdif() {
    101         assert_eq!(source_name(0x600), "PCM 1");
    102         assert_eq!(source_name(0x200), "ADAT 1");
    103         assert_eq!(source_name(0x181), "S/PDIF 2");
    104     }
    105 
    106     #[test]
    107     fn decodes_sinks() {
    108         assert_eq!(sink_name(0x080), "Analogue Out 1");
    109         assert_eq!(sink_name(0x30a), "Mixer In 11");
    110         assert_eq!(sink_name(0x600), "PCM 1");
    111     }
    112 }