Skip to main content

sim_lib_stream_host/
router.rs

1//! Audio placement router for registered audio sites.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use sim_kernel::{Cx, Error, Result, Symbol};
7
8use crate::placement::{AudioPlacementRequest, AudioSiteKey};
9use crate::site::AudioSite;
10use crate::{HostOpenPlan, HostOpenStream, stream_host_capability};
11
12/// A router row for an audio site and the provider that owns it.
13#[derive(Clone)]
14pub struct RegisteredAudioSite {
15    /// Provider or local fixture that owns this site key.
16    pub owner: Symbol,
17    /// Runtime-openable audio site.
18    pub site: Arc<dyn AudioSite>,
19}
20
21/// Registry and dispatcher for audio placement sites.
22pub struct AudioRouter {
23    sites: HashMap<AudioSiteKey, RegisteredAudioSite>,
24}
25
26impl Default for AudioRouter {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl AudioRouter {
33    /// Builds an empty audio placement router.
34    pub fn new() -> Self {
35        Self {
36            sites: HashMap::new(),
37        }
38    }
39
40    /// Registers a local modeled site by its stable key.
41    ///
42    /// Provider loading uses [`Self::register_owned`] so duplicate site keys can
43    /// be reported as load errors instead of silently replacing an owner.
44    pub fn register(&mut self, site: Arc<dyn AudioSite>) {
45        self.register_owned(local_audio_site_owner_symbol(), site)
46            .expect("duplicate local audio site key");
47    }
48
49    /// Registers a provider-owned site by its stable key.
50    pub fn register_owned(&mut self, owner: Symbol, site: Arc<dyn AudioSite>) -> Result<()> {
51        let key = site.key().clone();
52        if self.sites.contains_key(&key) {
53            return Err(Error::Eval(format!(
54                "audio site {} is already registered",
55                key.0
56            )));
57        }
58        self.sites.insert(key, RegisteredAudioSite { owner, site });
59        Ok(())
60    }
61
62    /// Explicitly reloads a provider-owned site, replacing only that owner's row.
63    pub fn reload_owned(&mut self, owner: Symbol, site: Arc<dyn AudioSite>) -> Result<()> {
64        let key = site.key().clone();
65        if let Some(existing) = self.sites.get(&key)
66            && existing.owner != owner
67        {
68            return Err(Error::Eval(format!(
69                "audio site {} is already owned by {}",
70                key.0, existing.owner
71            )));
72        }
73        self.sites.insert(key, RegisteredAudioSite { owner, site });
74        Ok(())
75    }
76
77    /// Removes every site owned by `owner`, returning the removed row count.
78    pub fn unregister_owner(&mut self, owner: &Symbol) -> usize {
79        let before = self.sites.len();
80        self.sites
81            .retain(|_, registered| &registered.owner != owner);
82        before - self.sites.len()
83    }
84
85    /// Returns an audio site by key.
86    pub fn site(&self, key: &AudioSiteKey) -> Option<&Arc<dyn AudioSite>> {
87        self.sites.get(key).map(|registered| &registered.site)
88    }
89
90    /// Returns the provider owner for a registered audio site.
91    pub fn site_owner(&self, key: &AudioSiteKey) -> Option<&Symbol> {
92        self.sites.get(key).map(|registered| &registered.owner)
93    }
94
95    /// Iterates over registered audio site rows.
96    pub fn registered_sites(&self) -> impl Iterator<Item = &RegisteredAudioSite> {
97        self.sites.values()
98    }
99
100    /// Iterates over registered audio site keys.
101    pub fn site_keys(&self) -> impl Iterator<Item = &AudioSiteKey> {
102        self.sites.keys()
103    }
104
105    /// Returns site keys whose device cards satisfy the requested output shape.
106    pub fn sites_by_capability(
107        &self,
108        min_channels_out: u16,
109        preferred_rates: &[u32],
110    ) -> Vec<AudioSiteKey> {
111        let mut keys = self
112            .sites
113            .values()
114            .filter(|registered| {
115                let card = registered.site.card();
116                card.channels_out >= min_channels_out
117                    && preferred_rates
118                        .iter()
119                        .any(|rate| card.sample_rates.contains(rate))
120            })
121            .map(|registered| registered.site.key().clone())
122            .collect::<Vec<_>>();
123        keys.sort_by_key(|key| key.0.to_string());
124        keys
125    }
126
127    /// Resolves `key` to a registered site, or returns the modeled fallback key.
128    ///
129    /// Missing native providers are treated as a placement miss rather than an
130    /// error when the modeled site is available.
131    pub fn resolve_or_modeled(
132        &self,
133        key: &AudioSiteKey,
134        modeled: &AudioSiteKey,
135    ) -> Result<AudioSiteKey> {
136        if self.sites.contains_key(key) {
137            Ok(key.clone())
138        } else if self.sites.contains_key(modeled) {
139            Ok(modeled.clone())
140        } else {
141            Err(Error::Eval(format!(
142                "no audio site registered for {:?} and no modeled fallback {:?}",
143                key, modeled
144            )))
145        }
146    }
147
148    /// Opens a placement request through its registered audio site after
149    /// checking authority and recording the declared device effects.
150    pub fn open_placement_checked(
151        &self,
152        cx: &mut Cx,
153        request: AudioPlacementRequest,
154    ) -> Result<HostOpenStream> {
155        if !self.sites.contains_key(&request.site_key) {
156            return Err(Error::Eval(format!(
157                "no audio site registered for {:?}",
158                request.site_key
159            )));
160        }
161        HostOpenPlan::new(
162            request.stream_request.backend().clone(),
163            request.stream_request.device().clone(),
164            request.stream_request.direction().effect_kinds(),
165            vec![stream_host_capability()],
166        )
167        .enforce(cx)?;
168        self.open_placement(request)
169    }
170
171    /// Opens a placement request through its registered audio site via the
172    /// site-level compatibility dispatch path.
173    ///
174    /// Runtime and public host opens should use [`Self::open_placement_checked`]
175    /// so the request's authority and device effects are handled first.
176    pub fn open_placement(&self, request: AudioPlacementRequest) -> Result<HostOpenStream> {
177        self.sites
178            .get(&request.site_key)
179            .ok_or_else(|| {
180                Error::Eval(format!(
181                    "no audio site registered for {:?}",
182                    request.site_key
183                ))
184            })?
185            .site
186            .open(request.stream_request)
187    }
188}
189
190fn local_audio_site_owner_symbol() -> Symbol {
191    Symbol::qualified("audio/provider", "local-modeled")
192}