sim-lib-stream-host 0.1.3

Host-device stream backend substrate for SIM streams.
Documentation
//! Shared host-device catalog for stream placements.

use sim_kernel::{
    Cx, Error, Result, Symbol,
    library::{ExportKind, ExportRecord, ExportState, Registry},
};

use crate::audio_provider::ModeledAudioProvider;
use crate::eval_site::{DeviceProvider, StreamEvalSite};
use crate::midi_live_eval_site::MidiLiveEvalSite;
use crate::midi_provider::ModeledMidiProvider;
use crate::placement::{DeviceDirection, DeviceKind, DeviceRecord};
use crate::{AudioRouter, Placement};

/// Registry of stream-device providers.
pub struct DeviceCatalog {
    providers: Vec<Box<dyn DeviceProvider>>,
}

impl Default for DeviceCatalog {
    fn default() -> Self {
        Self::default_modeled()
    }
}

impl DeviceCatalog {
    /// Builds an empty device catalog.
    pub fn new() -> Self {
        Self {
            providers: Vec::new(),
        }
    }

    /// Builds a catalog with deterministic modeled MIDI and audio providers.
    pub fn default_modeled() -> Self {
        let mut catalog = Self::new();
        catalog.register(Box::new(ModeledMidiProvider::default()));
        catalog.register(Box::new(ModeledAudioProvider::default()));
        catalog
    }

    /// Builds a modeled catalog plus audio sites exported by loaded libs.
    pub fn with_registry_audio_devices(registry: &Registry) -> Self {
        let mut catalog = Self::default_modeled();
        catalog.register_registry_audio_devices(registry);
        catalog
    }

    /// Builds a catalog with modeled providers plus one caller-supplied native MIDI provider.
    #[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
    pub fn with_native_midi(provider: Box<dyn DeviceProvider>) -> Self {
        let mut catalog = Self::default_modeled();
        catalog.register(provider);
        catalog
    }

    /// Builds a catalog with modeled providers plus a caller-supplied ALSA MIDI provider.
    #[cfg(feature = "rtmidi-hardware")]
    pub fn with_alsa_midi(provider: Box<dyn DeviceProvider>) -> Self {
        Self::with_native_midi(provider)
    }

    /// Builds a catalog with modeled providers plus a caller-supplied BLE-MIDI provider.
    #[cfg(feature = "ble-midi-hardware")]
    pub fn with_ble_midi(provider: Box<dyn DeviceProvider>) -> Self {
        Self::with_native_midi(provider)
    }

    /// Registers a device provider.
    pub fn register(&mut self, provider: Box<dyn DeviceProvider>) {
        self.providers.push(provider);
    }

    /// Adds a snapshot of the audio sites currently registered by a provider.
    pub fn register_provider_sites(&mut self, router: &AudioRouter) {
        let provider = ProviderAudioSites::from_router(router);
        if !provider.is_empty() {
            self.register(Box::new(provider));
        }
    }

    /// Adds audio-site records currently owned by loaded registry libs.
    pub fn register_registry_audio_devices(&mut self, registry: &Registry) {
        let provider = RegistryAudioDevices::from_registry(registry);
        if !provider.is_empty() {
            self.register(Box::new(provider));
        }
    }

    /// Enumerates every registered provider.
    pub fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
        let mut records = Vec::new();
        for provider in &self.providers {
            records.extend(provider.enumerate()?);
        }
        Ok(records)
    }

    /// Opens a cataloged stream evaluation site by device id after checking
    /// authority and recording the declared device effects.
    pub fn open_checked(&self, cx: &mut Cx, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
        for record in self.enumerate()? {
            if &record.id == id {
                record.open_plan().enforce(cx)?;
                return self.open(id);
            }
        }
        Err(Error::Eval(format!("DeviceCatalog: no device '{id}'")))
    }

    /// Opens a cataloged stream evaluation site by device id through the
    /// provider-level compatibility dispatch path.
    ///
    /// Runtime and public host opens should use [`Self::open_checked`] so the
    /// catalog row's authority and device effects are handled first.
    pub fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
        for provider in &self.providers {
            let records = provider.enumerate()?;
            if records.iter().any(|record| &record.id == id) {
                return provider.open(id);
            }
        }
        Err(Error::Eval(format!("DeviceCatalog: no device '{id}'")))
    }

    /// Opens a cataloged MIDI device as a live MIDI evaluation site after
    /// checking authority and recording the declared device effects.
    pub fn open_live_checked(&self, cx: &mut Cx, id: &Symbol) -> Result<MidiLiveEvalSite> {
        MidiLiveEvalSite::from_eval_site(id, self.open_checked(cx, id)?)
    }

    /// Opens a cataloged MIDI device as a live MIDI evaluation site through the
    /// compatibility dispatch path.
    ///
    /// Runtime and public host opens should use [`Self::open_live_checked`] so
    /// the catalog row's authority and device effects are handled first.
    pub fn open_live(&self, id: &Symbol) -> Result<MidiLiveEvalSite> {
        MidiLiveEvalSite::from_eval_site(id, self.open(id)?)
    }

    /// Enumerates MIDI device rows.
    pub fn enumerate_midi(&self) -> Result<Vec<DeviceRecord>> {
        self.enumerate_kind(DeviceKind::Midi)
    }

    /// Enumerates audio device rows.
    pub fn enumerate_audio(&self) -> Result<Vec<DeviceRecord>> {
        self.enumerate_kind(DeviceKind::Audio)
    }

    /// Returns safe audio backend candidate names from catalog records.
    ///
    /// Hardware records contribute their transport name. Modeled records keep
    /// the deterministic `modeled` fallback as the final candidate.
    pub fn audio_backend_names(&self) -> Result<Vec<String>> {
        self.backend_names(DeviceKind::Audio, true)
    }

    /// Returns safe MIDI backend candidate names from catalog records.
    ///
    /// Hardware records contribute their transport name. Modeled records keep
    /// the deterministic `modeled` fallback as the final candidate.
    pub fn midi_backend_names(&self) -> Result<Vec<String>> {
        self.backend_names(DeviceKind::Midi, true)
    }

    /// Returns audio backend candidate names for real hardware only.
    pub fn audio_hardware_backend_names(&self) -> Result<Vec<String>> {
        self.backend_names(DeviceKind::Audio, false)
    }

    /// Returns MIDI backend candidate names for real hardware only.
    pub fn midi_hardware_backend_names(&self) -> Result<Vec<String>> {
        self.backend_names(DeviceKind::Midi, false)
    }

    fn enumerate_kind(&self, kind: DeviceKind) -> Result<Vec<DeviceRecord>> {
        Ok(self
            .enumerate()?
            .into_iter()
            .filter(|record| record.kind == kind)
            .collect())
    }

    fn backend_names(
        &self,
        kind: DeviceKind,
        include_modeled_fallback: bool,
    ) -> Result<Vec<String>> {
        let mut names = Vec::new();
        let mut has_modeled = false;
        for record in self
            .enumerate()?
            .into_iter()
            .filter(|record| record.kind == kind)
        {
            match record.placement {
                Placement::Modeled => has_modeled = true,
                Placement::Hardware { transport } => push_unique(&mut names, transport.name),
            }
        }
        if include_modeled_fallback && (has_modeled || names.is_empty()) {
            push_unique(&mut names, "modeled");
        }
        Ok(names)
    }
}

fn push_unique(names: &mut Vec<String>, name: impl ToString) {
    let name = name.to_string();
    if !names.iter().any(|existing| existing == &name) {
        names.push(name);
    }
}

/// Stable site export symbol for an audio site owned by a loaded lib.
pub fn audio_site_export_symbol(name: &str) -> Symbol {
    Symbol::qualified("audio/site", name)
}

/// Stable export symbol for an audio device owned by a loaded lib.
///
/// Prefer [`audio_site_export_symbol`] for placement-capable audio sites.
pub fn audio_device_export_symbol(name: &str) -> Symbol {
    Symbol::qualified("audio/device", name)
}

struct ProviderAudioSites {
    records: Vec<DeviceRecord>,
}

impl ProviderAudioSites {
    fn from_router(router: &AudioRouter) -> Self {
        let mut records = router
            .registered_sites()
            .map(|registered| {
                let card = registered.site.card();
                let placement = if card.hardware_required {
                    Placement::Hardware {
                        transport: registered.owner.clone(),
                    }
                } else {
                    Placement::Modeled
                };
                DeviceRecord::audio_from_card(card, placement)
            })
            .collect::<Vec<_>>();
        records.sort_by_key(|record| record.id.to_string());
        Self { records }
    }

    fn is_empty(&self) -> bool {
        self.records.is_empty()
    }
}

impl DeviceProvider for ProviderAudioSites {
    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
        Ok(self.records.clone())
    }

    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
        let record = self
            .records
            .iter()
            .find(|record| &record.id == id)
            .ok_or_else(|| Error::Eval(format!("ProviderAudioSites: unknown id '{id}'")))?
            .clone();
        Ok(Box::new(ProviderAudioSite { record }))
    }
}

struct RegistryAudioDevices {
    records: Vec<DeviceRecord>,
}

impl RegistryAudioDevices {
    fn from_registry(registry: &Registry) -> Self {
        let mut records = registry
            .libs()
            .iter()
            .flat_map(|lib| lib.exports.iter())
            .filter_map(registry_audio_site_record)
            .collect::<Vec<_>>();
        records.sort_by_key(|record| record.id.to_string());
        records.dedup_by(|left, right| left.id == right.id);
        Self { records }
    }

    fn is_empty(&self) -> bool {
        self.records.is_empty()
    }
}

impl DeviceProvider for RegistryAudioDevices {
    fn enumerate(&self) -> Result<Vec<DeviceRecord>> {
        Ok(self.records.clone())
    }

    fn open(&self, id: &Symbol) -> Result<Box<dyn StreamEvalSite>> {
        let record = self
            .records
            .iter()
            .find(|record| &record.id == id)
            .ok_or_else(|| Error::Eval(format!("RegistryAudioDevices: unknown id '{id}'")))?
            .clone();
        Ok(Box::new(ProviderAudioSite { record }))
    }
}

fn registry_audio_site_record(record: &ExportRecord) -> Option<DeviceRecord> {
    if record.kind != ExportKind::named(ExportKind::SITE) {
        return None;
    }
    if record.symbol.namespace.as_deref() != Some("audio/site") {
        return None;
    }
    if matches!(record.state, ExportState::Invalid { .. }) {
        return None;
    }
    Some(DeviceRecord {
        id: record.symbol.clone(),
        display_name: format!("{} audio site", record.symbol.name),
        kind: DeviceKind::Audio,
        direction: DeviceDirection::Duplex,
        placement: Placement::Hardware {
            transport: record.symbol.clone(),
        },
    })
}

struct ProviderAudioSite {
    record: DeviceRecord,
}

impl StreamEvalSite for ProviderAudioSite {
    fn placement(&self) -> &Placement {
        &self.record.placement
    }

    fn device_record(&self) -> &DeviceRecord {
        &self.record
    }

    fn close(self: Box<Self>) -> Result<()> {
        Ok(())
    }
}