sim-lib-stream-host 0.1.0-rc.1

Host-device stream backend substrate for STREAM 6.
Documentation
use std::sync::Arc;

use sim_kernel::{
    AbiVersion, Cx, DefaultFactory, EagerPolicy, Export, Lib, LibManifest, LibTarget, Linker,
    LoadCx, Result, Symbol, Version,
};

use crate::{
    DeviceCatalog, DeviceDirection, DeviceKind, Placement, StreamEvalSite,
    audio_device_export_symbol,
};

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
use crate::{DeviceProvider, DeviceRecord};

#[test]
fn catalog_default_modeled_enumerates_midi_and_audio() {
    let catalog = DeviceCatalog::default_modeled();
    let records = catalog.enumerate().unwrap();

    assert!(records.iter().any(|record| record.kind == DeviceKind::Midi));
    assert!(
        records
            .iter()
            .any(|record| record.kind == DeviceKind::Audio)
    );
    assert_eq!(catalog.enumerate_midi().unwrap().len(), 2);
    assert_eq!(catalog.enumerate_audio().unwrap().len(), 1);
}

#[test]
fn unload_retracts_backend_registry_records() {
    let mut cx = test_cx();
    let lib = ModeledBackendLib::new("alsa");
    let lib_id = cx.load_lib(&lib).unwrap();

    assert!(
        DeviceCatalog::with_registry_audio_devices(cx.registry())
            .enumerate_audio()
            .unwrap()
            .iter()
            .any(|record| record.id.to_string().contains("alsa"))
    );

    cx.unload_lib(lib_id).unwrap();

    assert!(
        DeviceCatalog::with_registry_audio_devices(cx.registry())
            .enumerate_audio()
            .unwrap()
            .iter()
            .all(|record| !record.id.to_string().contains("alsa"))
    );
}

#[test]
fn default_audio_catalog_has_no_retired_transport_targets() {
    let records = DeviceCatalog::default_modeled().enumerate_audio().unwrap();
    for transport in ["alsa", "pipewire", "portaudio", "jack", "coreaudio", "asio"] {
        assert!(
            records
                .iter()
                .all(|record| !record.id.to_string().contains(transport)),
            "{transport} appears as a live audio catalog target"
        );
    }
}

#[test]
fn catalog_open_modeled_midi_input_returns_modeled_placement() {
    let catalog = DeviceCatalog::default_modeled();
    let site = catalog.open(&Symbol::new("midi/model/in-0")).unwrap();

    assert_eq!(site.placement(), &Placement::Modeled);
    assert_eq!(site.device_record().kind, DeviceKind::Midi);
    assert_eq!(site.device_record().direction, DeviceDirection::Input);
    site.close().unwrap();
}

#[test]
fn catalog_open_unknown_id_returns_error() {
    let catalog = DeviceCatalog::default_modeled();
    let err = catalog
        .open(&Symbol::new("midi/no-such-device"))
        .err()
        .unwrap();

    assert!(format!("{err}").contains("DeviceCatalog: no device"));
}

#[test]
fn catalog_open_live_modeled_midi_input_yields_modeled_placement() {
    let catalog = DeviceCatalog::default_modeled();
    let mut live = catalog.open_live(&Symbol::new("midi/model/in-0")).unwrap();

    assert_eq!(live.placement(), &Placement::Modeled);
    assert_eq!(live.device_record().kind, DeviceKind::Midi);
    assert_eq!(live.device_record().direction, DeviceDirection::Input);
    assert_eq!(live.source_mut().tpq(), 480);
    assert!(live.sink_mut().is_none());
    Box::new(live).close().unwrap();
}

#[test]
fn catalog_open_live_modeled_midi_output_exposes_sink() {
    let catalog = DeviceCatalog::default_modeled();
    let mut live = catalog.open_live(&Symbol::new("midi/model/out-0")).unwrap();

    assert_eq!(live.placement(), &Placement::Modeled);
    assert_eq!(live.device_record().kind, DeviceKind::Midi);
    assert_eq!(live.device_record().direction, DeviceDirection::Output);
    assert!(live.sink_mut().is_some());
    Box::new(live).close().unwrap();
}

#[test]
fn catalog_open_live_non_midi_returns_error() {
    let catalog = DeviceCatalog::default_modeled();
    let err = catalog
        .open_live(&Symbol::new("audio/model/stereo-0"))
        .err()
        .unwrap();

    assert!(format!("{err}").contains("is not MIDI"));
}

#[cfg(feature = "rtmidi-hardware")]
#[test]
fn catalog_with_native_midi_keeps_hardware_registration_opt_in() {
    let default_records = DeviceCatalog::default_modeled().enumerate().unwrap();
    assert!(
        default_records
            .iter()
            .all(|record| record.placement == Placement::Modeled)
    );

    let catalog = DeviceCatalog::with_native_midi(Box::new(FixtureHardwareMidiProvider::new(
        "rtmidi/alsa/in-0",
        "ALSA Input 0",
        "alsa-seq",
    )));
    let records = catalog.enumerate().unwrap();
    assert!(records.iter().any(|record| {
        record.id == Symbol::new("rtmidi/alsa/in-0")
            && record.placement
                == Placement::Hardware {
                    transport: Symbol::new("alsa-seq"),
                }
    }));
}

#[cfg(feature = "ble-midi-hardware")]
#[test]
fn catalog_with_ble_midi_keeps_hardware_registration_opt_in() {
    let default_records = DeviceCatalog::default_modeled().enumerate().unwrap();
    assert!(
        default_records
            .iter()
            .all(|record| record.placement == Placement::Modeled)
    );

    let catalog = DeviceCatalog::with_ble_midi(Box::new(FixtureHardwareMidiProvider::new(
        "ble-midi/bluez-0",
        "BLE MIDI Device 0",
        "ble-midi",
    )));
    let records = catalog.enumerate().unwrap();
    assert!(records.iter().any(|record| {
        record.id == Symbol::new("ble-midi/bluez-0")
            && record.placement
                == Placement::Hardware {
                    transport: Symbol::new("ble-midi"),
                }
    }));
    let site = catalog.open(&Symbol::new("ble-midi/bluez-0")).unwrap();
    assert_eq!(site.device_record().direction, DeviceDirection::Duplex);
    site.close().unwrap();
}

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
struct FixtureHardwareMidiProvider {
    id: &'static str,
    name: &'static str,
    transport: &'static str,
}

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
impl FixtureHardwareMidiProvider {
    fn new(id: &'static str, name: &'static str, transport: &'static str) -> Self {
        Self {
            id,
            name,
            transport,
        }
    }
}

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
impl DeviceProvider for FixtureHardwareMidiProvider {
    fn enumerate(&self) -> sim_kernel::Result<Vec<DeviceRecord>> {
        Ok(vec![DeviceRecord {
            id: Symbol::new(self.id),
            display_name: self.name.to_owned(),
            kind: DeviceKind::Midi,
            direction: DeviceDirection::Duplex,
            placement: Placement::Hardware {
                transport: Symbol::new(self.transport),
            },
        }])
    }

    fn open(&self, id: &Symbol) -> sim_kernel::Result<Box<dyn StreamEvalSite>> {
        let record = self
            .enumerate()?
            .into_iter()
            .find(|record| &record.id == id)
            .ok_or_else(|| sim_kernel::Error::Eval(format!("unknown fixture port '{id}'")))?;
        Ok(Box::new(FixtureHardwareMidiSite { record }))
    }
}

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
struct FixtureHardwareMidiSite {
    record: DeviceRecord,
}

#[cfg(any(feature = "rtmidi-hardware", feature = "ble-midi-hardware"))]
impl StreamEvalSite for FixtureHardwareMidiSite {
    fn placement(&self) -> &Placement {
        &self.record.placement
    }

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

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

struct ModeledBackendLib {
    transport: &'static str,
}

impl ModeledBackendLib {
    fn new(transport: &'static str) -> Self {
        Self { transport }
    }

    fn lib_symbol(&self) -> Symbol {
        Symbol::new(format!("stream-{}", self.transport))
    }

    fn device_symbol(&self) -> Symbol {
        audio_device_export_symbol(self.transport)
    }
}

impl Lib for ModeledBackendLib {
    fn manifest(&self) -> LibManifest {
        LibManifest {
            id: self.lib_symbol(),
            version: Version("0.1.0".to_owned()),
            abi: AbiVersion { major: 0, minor: 1 },
            target: LibTarget::HostRegistered,
            requires: Vec::new(),
            capabilities: Vec::new(),
            exports: vec![Export::Value {
                symbol: self.device_symbol(),
            }],
        }
    }

    fn load(&self, cx: &mut LoadCx, linker: &mut Linker) -> Result<()> {
        linker.value(self.device_symbol(), cx.factory().bool(true)?)
    }
}

fn test_cx() -> Cx {
    Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
}