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

Modeled macOS CoreAudio stream-host adapter (in-process simulation; no Apple framework 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, HostBackendRegistry, HostCallbackCassette, HostDirection, HostStreamConfigRequest,
};

use crate::{
    CoreAudioBackend, CoreAudioDevice, CoreAudioRenderBridge, CoreAudioTiming,
    coreaudio_backend_symbol, coreaudio_clock_symbol, install_stream_coreaudio_lib,
    macos_audio_backend_priority, macos_midi_backend_priority,
};

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

impl Processor for CopyInput {
    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;
            }
        }
    }
}

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

    assert_eq!(inventory.backend(), &coreaudio_backend_symbol());
    assert!(inventory.devices().iter().any(|device| {
        device.id() == &Symbol::new("coreaudio/default-output")
            && device.direction() == HostDirection::Output
    }));
    assert!(inventory.devices().iter().any(|device| {
        device.id() == &Symbol::new("coreaudio/default-input")
            && device.direction() == HostDirection::Input
    }));
}

#[test]
fn registry_opens_default_coreaudio_devices_by_backend_id() {
    let mut registry = HostBackendRegistry::new();
    registry.register(CoreAudioBackend::fake()).unwrap();
    let output = registry
        .open(HostStreamConfigRequest::new(
            coreaudio_backend_symbol(),
            Symbol::new("coreaudio/default-output"),
            StreamMedia::Pcm,
            HostDirection::Output,
            BufferPolicy::bounded(8).unwrap(),
        ))
        .unwrap();

    assert_eq!(output.config().clock().clock(), &coreaudio_clock_symbol());
    assert_eq!(output.config().clock().sample_rate_hz(), Some(48_000));
}

#[test]
fn custom_device_timing_reaches_open_config() {
    let timing = CoreAudioTiming::new(44_100, 64, 24, 32).unwrap();
    let backend = CoreAudioBackend::new(vec![
        CoreAudioDevice::output("coreaudio/vendor/out", "Vendor Out", 2, timing).unwrap(),
    ]);
    let opened = backend
        .open(HostStreamConfigRequest::new(
            coreaudio_backend_symbol(),
            Symbol::new("coreaudio/vendor/out"),
            StreamMedia::Pcm,
            HostDirection::Output,
            BufferPolicy::bounded(4).unwrap(),
        ))
        .unwrap();

    assert_eq!(opened.config().clock().sample_rate_hz(), Some(44_100));
    assert_eq!(opened.config().latency().output_frames(), 32);
}

#[test]
fn render_bridge_drives_same_graph_processor_code() {
    let mut bridge = CoreAudioRenderBridge::new(
        CopyInput { prepared: None },
        CoreAudioTiming::default_low_latency(),
        2,
        2,
    )
    .unwrap();
    let left = [1.0, 0.5];
    let right = [0.0, -1.0];
    let output = bridge.render_planar_f32(Some(&[&left, &right]), 2).unwrap();

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

#[test]
fn callback_cassette_replays_fake_coreaudio_queue_without_hardware() {
    let opened = CoreAudioBackend::fake().open_default_output(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 macos_priorities_keep_portable_audio_and_rtmidi_first() {
    let audio = macos_audio_backend_priority();
    let midi = macos_midi_backend_priority();

    assert_eq!(audio[0], Symbol::qualified("stream/host", "portaudio"));
    assert_eq!(audio[2], coreaudio_backend_symbol());
    assert_eq!(midi[0], Symbol::qualified("stream/host", "rtmidi"));
}

#[test]
fn install_stream_coreaudio_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_coreaudio_lib,
        &Symbol::new("stream-coreaudio"),
        &[Symbol::qualified("stream", "CoreAudioBackend")],
    );
}

#[test]
#[ignore = "hardware smoke test requires an operator-provided CoreAudio device"]
fn coreaudio_hardware_smoke_test_is_ignored_by_default() {
    let backend = CoreAudioBackend::default();
    let _devices = backend.list_devices();
}