Skip to main content

sim_lib_stream_host/
catalog.rs

1//! Shared host-device catalog for modeled stream placements.
2
3use sim_kernel::{
4    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 devices 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-device 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.
100    pub fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
101        for provider in &self.providers {
102            let records = provider.enumerate()?;
103            if records.iter().any(|record| &record.id == id) {
104                return provider.open(id);
105            }
106        }
107        Err(Error::Eval(format!("DeviceCatalog: no device '{id}'")))
108    }
109
110    /// Opens a cataloged MIDI device as a live MIDI evaluation site.
111    pub fn open_live(&self, id: &Symbol) -> Result<MidiLiveEvalSite> {
112        MidiLiveEvalSite::from_eval_site(id, self.open(id)?)
113    }
114
115    /// Enumerates MIDI device rows.
116    pub fn enumerate_midi(&self) -> Result<Vec<DeviceRecord>> {
117        self.enumerate_kind(DeviceKind::Midi)
118    }
119
120    /// Enumerates audio device rows.
121    pub fn enumerate_audio(&self) -> Result<Vec<DeviceRecord>> {
122        self.enumerate_kind(DeviceKind::Audio)
123    }
124
125    /// Returns safe audio backend candidate names from catalog records.
126    ///
127    /// Hardware records contribute their transport name. Modeled records keep
128    /// the deterministic `modeled` fallback as the final candidate.
129    pub fn audio_backend_names(&self) -> Result<Vec<String>> {
130        self.backend_names(DeviceKind::Audio)
131    }
132
133    /// Returns safe MIDI backend candidate names from catalog records.
134    ///
135    /// Hardware records contribute their transport name. Modeled records keep
136    /// the deterministic `modeled` fallback as the final candidate.
137    pub fn midi_backend_names(&self) -> Result<Vec<String>> {
138        self.backend_names(DeviceKind::Midi)
139    }
140
141    fn enumerate_kind(&self, kind: DeviceKind) -> Result<Vec<DeviceRecord>> {
142        Ok(self
143            .enumerate()?
144            .into_iter()
145            .filter(|record| record.kind == kind)
146            .collect())
147    }
148
149    fn backend_names(&self, kind: DeviceKind) -> Result<Vec<String>> {
150        let mut names = Vec::new();
151        let mut has_modeled = false;
152        for record in self
153            .enumerate()?
154            .into_iter()
155            .filter(|record| record.kind == kind)
156        {
157            match record.placement {
158                Placement::Modeled => has_modeled = true,
159                Placement::Hardware { transport } => push_unique(&mut names, transport.name),
160            }
161        }
162        if has_modeled || names.is_empty() {
163            push_unique(&mut names, "modeled");
164        }
165        Ok(names)
166    }
167}
168
169fn push_unique(names: &mut Vec<String>, name: impl ToString) {
170    let name = name.to_string();
171    if !names.iter().any(|existing| existing == &name) {
172        names.push(name);
173    }
174}
175
176/// Stable export symbol for an audio device owned by a loaded lib.
177pub fn audio_device_export_symbol(name: &str) -> Symbol {
178    Symbol::qualified("audio/device", name)
179}
180
181struct ProviderAudioSites {
182    records: Vec<DeviceRecord>,
183}
184
185impl ProviderAudioSites {
186    fn from_router(router: &AudioRouter) -> Self {
187        let mut records = router
188            .site_keys()
189            .filter_map(|key| router.site(key))
190            .map(|site| DeviceRecord::modeled_audio_from_card(site.card()))
191            .collect::<Vec<_>>();
192        records.sort_by_key(|record| record.id.to_string());
193        Self { records }
194    }
195
196    fn is_empty(&self) -> bool {
197        self.records.is_empty()
198    }
199}
200
201impl DeviceProvider for ProviderAudioSites {
202    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
203        Ok(self.records.clone())
204    }
205
206    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
207        let record = self
208            .records
209            .iter()
210            .find(|record| &record.id == id)
211            .ok_or_else(|| Error::Eval(format!("ProviderAudioSites: unknown id '{id}'")))?
212            .clone();
213        Ok(Box::new(ProviderAudioSite { record }))
214    }
215}
216
217struct RegistryAudioDevices {
218    records: Vec<DeviceRecord>,
219}
220
221impl RegistryAudioDevices {
222    fn from_registry(registry: &Registry) -> Self {
223        let mut records = registry
224            .libs()
225            .iter()
226            .flat_map(|lib| lib.exports.iter())
227            .filter_map(registry_audio_device_record)
228            .collect::<Vec<_>>();
229        records.sort_by_key(|record| record.id.to_string());
230        records.dedup_by(|left, right| left.id == right.id);
231        Self { records }
232    }
233
234    fn is_empty(&self) -> bool {
235        self.records.is_empty()
236    }
237}
238
239impl DeviceProvider for RegistryAudioDevices {
240    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
241        Ok(self.records.clone())
242    }
243
244    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
245        let record = self
246            .records
247            .iter()
248            .find(|record| &record.id == id)
249            .ok_or_else(|| Error::Eval(format!("RegistryAudioDevices: unknown id '{id}'")))?
250            .clone();
251        Ok(Box::new(ProviderAudioSite { record }))
252    }
253}
254
255fn registry_audio_device_record(record: &ExportRecord) -> Option<DeviceRecord> {
256    if record.kind != ExportKind::named(ExportKind::VALUE) {
257        return None;
258    }
259    if record.symbol.namespace.as_deref() != Some("audio/device") {
260        return None;
261    }
262    if matches!(record.state, ExportState::Invalid { .. }) {
263        return None;
264    }
265    Some(DeviceRecord {
266        id: record.symbol.clone(),
267        display_name: format!("{} audio device", record.symbol.name),
268        kind: DeviceKind::Audio,
269        direction: DeviceDirection::Duplex,
270        placement: Placement::Modeled,
271    })
272}
273
274struct ProviderAudioSite {
275    record: DeviceRecord,
276}
277
278impl StreamEvalSite for ProviderAudioSite {
279    fn placement(&self) -> &Placement {
280        &self.record.placement
281    }
282
283    fn device_record(&self) -> &DeviceRecord {
284        &self.record
285    }
286
287    fn close(self: Box<Self>) -> Result<()> {
288        Ok(())
289    }
290}