Skip to main content

sim_lib_stream_host/
catalog.rs

1//! Shared host-device catalog for stream placements.
2
3use sim_kernel::{
4    Cx, Error, Result, Symbol,
5    library::{ExportKind, ExportRecord, ExportState, Registry},
6};
7
8use crate::audio_provider::ModeledAudioProvider;
9use crate::eval_site::{DeviceProvider, StreamEvalSite};
10use crate::midi_live_eval_site::MidiLiveEvalSite;
11use crate::midi_provider::ModeledMidiProvider;
12use crate::placement::{DeviceDirection, DeviceKind, DeviceRecord};
13use crate::{AudioRouter, Placement};
14
15/// Registry of stream-device providers.
16pub struct DeviceCatalog {
17    providers: Vec<Box<dyn DeviceProvider>>,
18}
19
20impl Default for DeviceCatalog {
21    fn default() -> Self {
22        Self::default_modeled()
23    }
24}
25
26impl DeviceCatalog {
27    /// Builds an empty device catalog.
28    pub fn new() -> Self {
29        Self {
30            providers: Vec::new(),
31        }
32    }
33
34    /// Builds a catalog with deterministic modeled MIDI and audio providers.
35    pub fn default_modeled() -> Self {
36        let mut catalog = Self::new();
37        catalog.register(Box::new(ModeledMidiProvider::default()));
38        catalog.register(Box::new(ModeledAudioProvider::default()));
39        catalog
40    }
41
42    /// Builds a modeled catalog plus audio sites exported by loaded libs.
43    pub fn with_registry_audio_devices(registry: &Registry) -> Self {
44        let mut catalog = Self::default_modeled();
45        catalog.register_registry_audio_devices(registry);
46        catalog
47    }
48
49    /// Builds a catalog with modeled providers plus one caller-supplied native MIDI provider.
50    #[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
51    pub fn with_native_midi(provider: Box<dyn DeviceProvider>) -> Self {
52        let mut catalog = Self::default_modeled();
53        catalog.register(provider);
54        catalog
55    }
56
57    /// Builds a catalog with modeled providers plus a caller-supplied ALSA MIDI provider.
58    #[cfg(feature = "rtmidi-hardware")]
59    pub fn with_alsa_midi(provider: Box<dyn DeviceProvider>) -> Self {
60        Self::with_native_midi(provider)
61    }
62
63    /// Builds a catalog with modeled providers plus a caller-supplied BLE-MIDI provider.
64    #[cfg(feature = "ble-midi-hardware")]
65    pub fn with_ble_midi(provider: Box<dyn DeviceProvider>) -> Self {
66        Self::with_native_midi(provider)
67    }
68
69    /// Registers a device provider.
70    pub fn register(&mut self, provider: Box<dyn DeviceProvider>) {
71        self.providers.push(provider);
72    }
73
74    /// Adds a snapshot of the audio sites currently registered by a provider.
75    pub fn register_provider_sites(&mut self, router: &AudioRouter) {
76        let provider = ProviderAudioSites::from_router(router);
77        if !provider.is_empty() {
78            self.register(Box::new(provider));
79        }
80    }
81
82    /// Adds audio-site records currently owned by loaded registry libs.
83    pub fn register_registry_audio_devices(&mut self, registry: &Registry) {
84        let provider = RegistryAudioDevices::from_registry(registry);
85        if !provider.is_empty() {
86            self.register(Box::new(provider));
87        }
88    }
89
90    /// Enumerates every registered provider.
91    pub fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
92        let mut records = Vec::new();
93        for provider in &self.providers {
94            records.extend(provider.enumerate()?);
95        }
96        Ok(records)
97    }
98
99    /// Opens a cataloged stream evaluation site by device id after checking
100    /// authority and recording the declared device effects.
101    pub fn open_checked(&self, cx: &mut Cx, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
102        for record in self.enumerate()? {
103            if &record.id == id {
104                record.open_plan().enforce(cx)?;
105                return self.open(id);
106            }
107        }
108        Err(Error::Eval(format!("DeviceCatalog: no device '{id}'")))
109    }
110
111    /// Opens a cataloged stream evaluation site by device id through the
112    /// provider-level compatibility dispatch path.
113    ///
114    /// Runtime and public host opens should use [`Self::open_checked`] so the
115    /// catalog row's authority and device effects are handled first.
116    pub fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
117        for provider in &self.providers {
118            let records = provider.enumerate()?;
119            if records.iter().any(|record| &record.id == id) {
120                return provider.open(id);
121            }
122        }
123        Err(Error::Eval(format!("DeviceCatalog: no device '{id}'")))
124    }
125
126    /// Opens a cataloged MIDI device as a live MIDI evaluation site after
127    /// checking authority and recording the declared device effects.
128    pub fn open_live_checked(&self, cx: &mut Cx, id: &Symbol) -> Result<MidiLiveEvalSite> {
129        MidiLiveEvalSite::from_eval_site(id, self.open_checked(cx, id)?)
130    }
131
132    /// Opens a cataloged MIDI device as a live MIDI evaluation site through the
133    /// compatibility dispatch path.
134    ///
135    /// Runtime and public host opens should use [`Self::open_live_checked`] so
136    /// the catalog row's authority and device effects are handled first.
137    pub fn open_live(&self, id: &Symbol) -> Result<MidiLiveEvalSite> {
138        MidiLiveEvalSite::from_eval_site(id, self.open(id)?)
139    }
140
141    /// Enumerates MIDI device rows.
142    pub fn enumerate_midi(&self) -> Result<Vec<DeviceRecord>> {
143        self.enumerate_kind(DeviceKind::Midi)
144    }
145
146    /// Enumerates audio device rows.
147    pub fn enumerate_audio(&self) -> Result<Vec<DeviceRecord>> {
148        self.enumerate_kind(DeviceKind::Audio)
149    }
150
151    /// Returns safe audio backend candidate names from catalog records.
152    ///
153    /// Hardware records contribute their transport name. Modeled records keep
154    /// the deterministic `modeled` fallback as the final candidate.
155    pub fn audio_backend_names(&self) -> Result<Vec<String>> {
156        self.backend_names(DeviceKind::Audio, true)
157    }
158
159    /// Returns safe MIDI backend candidate names from catalog records.
160    ///
161    /// Hardware records contribute their transport name. Modeled records keep
162    /// the deterministic `modeled` fallback as the final candidate.
163    pub fn midi_backend_names(&self) -> Result<Vec<String>> {
164        self.backend_names(DeviceKind::Midi, true)
165    }
166
167    /// Returns audio backend candidate names for real hardware only.
168    pub fn audio_hardware_backend_names(&self) -> Result<Vec<String>> {
169        self.backend_names(DeviceKind::Audio, false)
170    }
171
172    /// Returns MIDI backend candidate names for real hardware only.
173    pub fn midi_hardware_backend_names(&self) -> Result<Vec<String>> {
174        self.backend_names(DeviceKind::Midi, false)
175    }
176
177    fn enumerate_kind(&self, kind: DeviceKind) -> Result<Vec<DeviceRecord>> {
178        Ok(self
179            .enumerate()?
180            .into_iter()
181            .filter(|record| record.kind == kind)
182            .collect())
183    }
184
185    fn backend_names(
186        &self,
187        kind: DeviceKind,
188        include_modeled_fallback: bool,
189    ) -> Result<Vec<String>> {
190        let mut names = Vec::new();
191        let mut has_modeled = false;
192        for record in self
193            .enumerate()?
194            .into_iter()
195            .filter(|record| record.kind == kind)
196        {
197            match record.placement {
198                Placement::Modeled => has_modeled = true,
199                Placement::Hardware { transport } => push_unique(&mut names, transport.name),
200            }
201        }
202        if include_modeled_fallback && (has_modeled || names.is_empty()) {
203            push_unique(&mut names, "modeled");
204        }
205        Ok(names)
206    }
207}
208
209fn push_unique(names: &mut Vec<String>, name: impl ToString) {
210    let name = name.to_string();
211    if !names.iter().any(|existing| existing == &name) {
212        names.push(name);
213    }
214}
215
216/// Stable site export symbol for an audio site owned by a loaded lib.
217pub fn audio_site_export_symbol(name: &str) -> Symbol {
218    Symbol::qualified("audio/site", name)
219}
220
221/// Stable export symbol for an audio device owned by a loaded lib.
222///
223/// Prefer [`audio_site_export_symbol`] for placement-capable audio sites.
224pub fn audio_device_export_symbol(name: &str) -> Symbol {
225    Symbol::qualified("audio/device", name)
226}
227
228struct ProviderAudioSites {
229    records: Vec<DeviceRecord>,
230}
231
232impl ProviderAudioSites {
233    fn from_router(router: &AudioRouter) -> Self {
234        let mut records = router
235            .registered_sites()
236            .map(|registered| {
237                let card = registered.site.card();
238                let placement = if card.hardware_required {
239                    Placement::Hardware {
240                        transport: registered.owner.clone(),
241                    }
242                } else {
243                    Placement::Modeled
244                };
245                DeviceRecord::audio_from_card(card, placement)
246            })
247            .collect::<Vec<_>>();
248        records.sort_by_key(|record| record.id.to_string());
249        Self { records }
250    }
251
252    fn is_empty(&self) -> bool {
253        self.records.is_empty()
254    }
255}
256
257impl DeviceProvider for ProviderAudioSites {
258    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
259        Ok(self.records.clone())
260    }
261
262    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
263        let record = self
264            .records
265            .iter()
266            .find(|record| &record.id == id)
267            .ok_or_else(|| Error::Eval(format!("ProviderAudioSites: unknown id '{id}'")))?
268            .clone();
269        Ok(Box::new(ProviderAudioSite { record }))
270    }
271}
272
273struct RegistryAudioDevices {
274    records: Vec<DeviceRecord>,
275}
276
277impl RegistryAudioDevices {
278    fn from_registry(registry: &Registry) -> Self {
279        let mut records = registry
280            .libs()
281            .iter()
282            .flat_map(|lib| lib.exports.iter())
283            .filter_map(registry_audio_site_record)
284            .collect::<Vec<_>>();
285        records.sort_by_key(|record| record.id.to_string());
286        records.dedup_by(|left, right| left.id == right.id);
287        Self { records }
288    }
289
290    fn is_empty(&self) -> bool {
291        self.records.is_empty()
292    }
293}
294
295impl DeviceProvider for RegistryAudioDevices {
296    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
297        Ok(self.records.clone())
298    }
299
300    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
301        let record = self
302            .records
303            .iter()
304            .find(|record| &record.id == id)
305            .ok_or_else(|| Error::Eval(format!("RegistryAudioDevices: unknown id '{id}'")))?
306            .clone();
307        Ok(Box::new(ProviderAudioSite { record }))
308    }
309}
310
311fn registry_audio_site_record(record: &ExportRecord) -> Option<DeviceRecord> {
312    if record.kind != ExportKind::named(ExportKind::SITE) {
313        return None;
314    }
315    if record.symbol.namespace.as_deref() != Some("audio/site") {
316        return None;
317    }
318    if matches!(record.state, ExportState::Invalid { .. }) {
319        return None;
320    }
321    Some(DeviceRecord {
322        id: record.symbol.clone(),
323        display_name: format!("{} audio site", record.symbol.name),
324        kind: DeviceKind::Audio,
325        direction: DeviceDirection::Duplex,
326        placement: Placement::Hardware {
327            transport: record.symbol.clone(),
328        },
329    })
330}
331
332struct ProviderAudioSite {
333    record: DeviceRecord,
334}
335
336impl StreamEvalSite for ProviderAudioSite {
337    fn placement(&self) -> &Placement {
338        &self.record.placement
339    }
340
341    fn device_record(&self) -> &DeviceRecord {
342        &self.record
343    }
344
345    fn close(self: Box<Self>) -> Result<()> {
346        Ok(())
347    }
348}