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

Modeled Windows ASIO stream-host adapter (in-process simulation; no ASIO SDK/driver I/O).
Documentation
use std::sync::Arc;

use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Symbol};
use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
use sim_lib_stream_core::{BufferPolicy, StreamMedia, StreamPacket};
use sim_lib_stream_host::{
    HostBackend, HostBackendCapability, HostBackendRegistry, HostCallbackCassette, HostDirection,
    HostStreamConfigRequest,
};

use crate::{
    AsioBackend, AsioBufferSwitchBridge, AsioDriver, AsioTiming, asio_backend_symbol,
    asio_clock_symbol, asio_sdk_build_requirements, install_stream_asio_lib,
};

#[derive(Debug)]
struct CopyGain {
    gain: f32,
    prepared: Option<PrepareConfig>,
}

impl Processor for CopyGain {
    fn prepare(&mut self, cfg: PrepareConfig) {
        self.prepared = Some(cfg);
    }

    fn reset(&mut self) {
        self.prepared = None;
    }

    fn process(&mut self, block: &mut ProcessBlock<'_>) {
        for (input, output) in block.in_audio.iter().zip(block.out_audio.iter_mut()) {
            for (source, target) in input.iter().zip(output.iter_mut()) {
                *target = *source * self.gain;
            }
        }
    }
}

#[test]
fn fake_backend_enumerates_sim_asio_driver_and_ports() {
    let backend = AsioBackend::fake();
    let inventory = backend.enumerate().unwrap();

    assert_eq!(inventory.backend(), &asio_backend_symbol());
    assert!(inventory.devices().iter().any(|device| {
        device.id() == &Symbol::new("asio/SIM-ASIO/driver")
            && device.direction() == HostDirection::Duplex
    }));
    assert!(
        inventory
            .ports()
            .iter()
            .any(|port| port.id() == &Symbol::new("asio/SIM-ASIO/driver/output_0"))
    );
}

#[test]
fn fake_backend_capabilities_include_duplex_and_offline() {
    let backend = AsioBackend::fake();
    let capabilities = backend.info().capabilities();

    assert!(capabilities.contains(&HostBackendCapability::AudioInput));
    assert!(capabilities.contains(&HostBackendCapability::AudioOutput));
    assert!(capabilities.contains(&HostBackendCapability::Duplex));
    assert!(capabilities.contains(&HostBackendCapability::Offline));
    assert!(capabilities.contains(&HostBackendCapability::Fake));
}

#[test]
fn registry_opens_sim_driver_by_backend_id() {
    let mut registry = HostBackendRegistry::new();
    registry.register(AsioBackend::fake()).unwrap();
    let opened = registry
        .open(HostStreamConfigRequest::new(
            asio_backend_symbol(),
            Symbol::new("asio/SIM-ASIO/driver"),
            StreamMedia::Pcm,
            HostDirection::Duplex,
            BufferPolicy::bounded(8).unwrap(),
        ))
        .unwrap();

    assert_eq!(opened.config().backend(), &asio_backend_symbol());
    assert_eq!(opened.config().clock().clock(), &asio_clock_symbol());
    assert_eq!(opened.config().clock().sample_rate_hz(), Some(48_000));
}

#[test]
fn custom_driver_timing_reaches_open_config() {
    let timing = AsioTiming::new(96_000, 64, 32, 96).unwrap();
    let backend = AsioBackend::new(vec![AsioDriver::new("Vendor", timing, 1, 2).unwrap()]);
    let opened = backend
        .open(HostStreamConfigRequest::new(
            asio_backend_symbol(),
            Symbol::new("asio/Vendor/driver"),
            StreamMedia::Pcm,
            HostDirection::Output,
            BufferPolicy::bounded(4).unwrap(),
        ))
        .unwrap();

    assert_eq!(opened.config().clock().sample_rate_hz(), Some(96_000));
    assert_eq!(opened.config().latency().output_frames(), 96);
}

#[test]
fn buffer_switch_bridge_drives_same_graph_processor_code() {
    let mut bridge = AsioBufferSwitchBridge::new(
        CopyGain {
            gain: 0.25,
            prepared: None,
        },
        AsioTiming::pro_audio_default(),
        2,
        2,
    )
    .unwrap();
    let left = [1.0, 0.5];
    let right = [0.0, -1.0];
    let output = bridge
        .process_planar_f32(Some(&[&left, &right]), 2)
        .unwrap();

    assert_eq!(output[0], vec![0.25, 0.125]);
    assert_eq!(output[1], vec![0.0, -0.25]);
    assert_eq!(bridge.sample_pos(), 2);
}

#[test]
fn callback_cassette_replays_fake_asio_queue_without_hardware() {
    let opened = AsioBackend::fake().open_sim_driver(4).unwrap();
    let mut cassette = HostCallbackCassette::new();
    cassette.record_packet(StreamPacket::Pcm(
        sim_lib_stream_core::PcmPacket::f32(1, 2, vec![0.0, 0.5]).unwrap(),
    ));

    cassette.replay(opened.queue()).unwrap();

    assert_eq!(opened.queue().drain(4).unwrap().len(), 1);
}

#[test]
fn sdk_requirements_document_optional_native_build() {
    let requirements = asio_sdk_build_requirements();

    assert!(requirements.iter().any(|item| item.contains("ASIO SDK")));
    assert!(requirements.iter().any(|item| item.contains("stream-asio")));
}

#[test]
fn install_stream_asio_lib_registers_runtime_exports() {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    sim_test_support::assert_lib_exports(
        &mut cx,
        install_stream_asio_lib,
        &Symbol::new("stream-asio"),
        &[Symbol::qualified("stream", "AsioBackend")],
    );
}

#[test]
#[ignore = "hardware smoke test requires an operator-provided Windows ASIO driver"]
fn asio_hardware_smoke_test_is_ignored_by_default() {
    let backend = AsioBackend::default();
    let _drivers = backend.list_drivers();
}