Skip to main content

mx_remote/types/
audio.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The audio endpoint tree a V2IP device or amplifier reports.
5
6use std::collections::BTreeMap;
7
8use crate::wire::DeviceUid;
9
10use super::V2ipStreamSource;
11
12/// What one audio endpoint can do.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct AudioFeatures(u32);
15
16impl AudioFeatures {
17    /// Accepts audio.
18    pub const INPUT: Self = Self(1 << 0);
19    /// Produces audio.
20    pub const OUTPUT: Self = Self(1 << 1);
21    /// Sends a V2IP audio stream.
22    pub const V2IP_TX: Self = Self(1 << 2);
23    /// Receives a V2IP audio stream.
24    pub const V2IP_RX: Self = Self(1 << 3);
25    /// Carries HDMI audio.
26    pub const HDMI: Self = Self(1 << 4);
27    /// Is an analogue RCA connector.
28    pub const RCA: Self = Self(1 << 5);
29    /// Is an S/PDIF connector.
30    pub const SPDIF: Self = Self(1 << 6);
31    /// Drives a trigger output.
32    pub const TRIGGER: Self = Self(1 << 7);
33    /// Can be muted.
34    pub const MUTE: Self = Self(1 << 8);
35    /// Can be routed to as an input.
36    pub const ROUTE_INPUT: Self = Self(1 << 9);
37    /// Can be routed from as an output.
38    pub const ROUTE_OUTPUT: Self = Self(1 << 10);
39    /// Accepts "no input" as a route.
40    pub const ROUTE_IN_NONE: Self = Self(1 << 11);
41    /// Is an amplifier output.
42    pub const AMP_OUTPUT: Self = Self(1 << 12);
43    /// Has a volume control.
44    pub const VOLUME_CONTROL: Self = Self(1 << 13);
45    /// Has a gain control.
46    pub const GAIN_CONTROL: Self = Self(1 << 14);
47    /// Keeps its audio source when the video route changes, while locked.
48    pub const AUDIO_LOCK: Self = Self(1 << 15);
49
50    /// Wraps the raw wire bits, including ones this library has no name for.
51    pub const fn from_bits(bits: u32) -> Self {
52        Self(bits)
53    }
54
55    /// Returns the raw wire bits.
56    pub const fn bits(self) -> u32 {
57        self.0
58    }
59
60    /// Reports whether every bit of `other` is set.
61    pub const fn has(self, other: Self) -> bool {
62        self.0 & other.0 == other.0
63    }
64
65    /// Reports whether this endpoint is either end of a V2IP audio stream.
66    pub const fn is_v2ip(self) -> bool {
67        self.has(Self::V2IP_TX) || self.has(Self::V2IP_RX)
68    }
69}
70
71/// One audio endpoint: an input, an output, or a processing node between them.
72///
73/// The tree is held by id rather than by reference: `parent` and `children`
74/// name endpoints in the same [`AudioEndpoints`] collection.
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
76pub struct AudioEndpoint {
77    /// The endpoint's id within its device.
78    pub id: u8,
79    /// What this endpoint can do.
80    pub features: AudioFeatures,
81    /// The stream this endpoint sends or receives, when it has one.
82    pub address: Option<V2ipStreamSource>,
83    /// The endpoint this one feeds into.
84    pub parent: Option<u8>,
85    /// The endpoints feeding into this one.
86    pub children: Vec<u8>,
87    /// Bitmask of the endpoints that may be routed to this one.
88    pub inputs_available: Option<u32>,
89    /// Bitmask of the endpoints currently routed to this one.
90    pub inputs_routed: Option<u32>,
91    /// The device holding the endpoint this one is linked to.
92    pub linked_device: DeviceUid,
93    /// The endpoint on `linked_device` this one is linked to.
94    pub linked_endpoint: Option<u8>,
95}
96
97impl AudioEndpoint {
98    /// The endpoint currently routed to this one, or `None` when none is.
99    pub fn input(&self) -> Option<u8> {
100        let routed = self.inputs_routed?;
101        (0..32).find(|id| routed & (1 << id) != 0)
102    }
103
104    /// The endpoints that may be routed to this one.
105    pub fn available_inputs(&self) -> Vec<u8> {
106        let Some(mask) = self.inputs_available else {
107            return Vec::new();
108        };
109        (0..32).filter(|id| mask & (1 << id) != 0).collect()
110    }
111}
112
113/// The audio endpoints a device reports, in the order it reported them.
114#[derive(Clone, Debug, Default, PartialEq, Eq)]
115pub struct AudioEndpoints {
116    order: Vec<u8>,
117    endpoints: BTreeMap<u8, AudioEndpoint>,
118    status: BTreeMap<u8, AudioFeatures>,
119}
120
121impl AudioEndpoints {
122    /// Adds an endpoint, replacing one with the same id and keeping its place
123    /// in the reported order.
124    pub(crate) fn add(&mut self, endpoint: AudioEndpoint) {
125        if !self.endpoints.contains_key(&endpoint.id) {
126            self.order.push(endpoint.id);
127        }
128        self.endpoints.insert(endpoint.id, endpoint);
129    }
130
131    /// The state endpoint `id` reports, on the bits of its features: `MUTE`
132    /// while it is muted, `TRIGGER` while its trigger is active, and
133    /// `AUDIO_LOCK` while its audio source is locked. `None` for an endpoint
134    /// the device did not report.
135    pub fn status(&self, id: u8) -> Option<AudioFeatures> {
136        self.status.get(&id).copied()
137    }
138
139    pub(crate) fn set_status(&mut self, id: u8, status: AudioFeatures) {
140        self.status.insert(id, status);
141    }
142
143    /// The endpoint with the given id.
144    pub fn get(&self, id: u8) -> Option<&AudioEndpoint> {
145        self.endpoints.get(&id)
146    }
147
148    pub(crate) fn get_mut(&mut self, id: u8) -> Option<&mut AudioEndpoint> {
149        self.endpoints.get_mut(&id)
150    }
151
152    /// Every endpoint, in the order the device reported them.
153    pub fn list(&self) -> impl Iterator<Item = &AudioEndpoint> {
154        self.order.iter().filter_map(|id| self.endpoints.get(id))
155    }
156
157    /// The endpoints with no parent: the roots of the device's audio tree.
158    pub fn roots(&self) -> impl Iterator<Item = &AudioEndpoint> {
159        self.list().filter(|ep| ep.parent.is_none())
160    }
161
162    /// The first root that accepts audio.
163    pub fn first_root_input(&self) -> Option<&AudioEndpoint> {
164        self.roots()
165            .find(|ep| ep.features.has(AudioFeatures::INPUT))
166    }
167
168    /// The first root that produces audio.
169    pub fn first_root_output(&self) -> Option<&AudioEndpoint> {
170        self.roots()
171            .find(|ep| ep.features.has(AudioFeatures::OUTPUT))
172    }
173
174    /// Reports whether two collections describe the same tree.
175    ///
176    /// Only the id, the features and the parent are compared: the routing and
177    /// link fields change on every report, and a device that re-sends an
178    /// unchanged tree must not read as a new one.
179    pub(crate) fn same_tree(&self, other: &Self) -> bool {
180        self.endpoints.len() == other.endpoints.len()
181            && self.endpoints.iter().all(|(id, ep)| {
182                other
183                    .endpoints
184                    .get(id)
185                    .is_some_and(|o| o.features == ep.features && o.parent == ep.parent)
186            })
187    }
188
189    /// Reports whether every endpoint reports the same status in both.
190    pub(crate) fn same_status(&self, other: &Self) -> bool {
191        self.status == other.status
192    }
193
194    /// Records the endpoint and device one of these endpoints is linked to.
195    pub(crate) fn apply_link(&mut self, link: &AudioLink) {
196        if let Some(ep) = self.endpoints.get_mut(&link.endpoint) {
197            ep.linked_device = link.linked_device;
198            ep.linked_endpoint = Some(link.linked_endpoint);
199        }
200    }
201}
202
203/// A link from an audio endpoint on this device to one on another.
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
205pub struct AudioLink {
206    /// The local endpoint.
207    pub endpoint: u8,
208    /// The endpoint it is linked to.
209    pub linked_endpoint: u8,
210    /// The device holding the linked endpoint.
211    pub linked_device: DeviceUid,
212}