sim-lib-stream-jack 0.1.0

Modeled JACK stream-host adapter (in-process simulation; no real JACK server I/O).
Documentation
use std::sync::{Arc, Mutex};

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

use crate::{
    JackBackend, JackClient, JackGraphBridge, JackMidiEvent, JackTiming, JackTransportState,
    install_stream_jack_lib, jack_backend_symbol, jack_clock_symbol,
};

#[derive(Clone, Debug, Default)]
struct Recorder {
    state: Arc<Mutex<RecorderState>>,
}

#[derive(Clone, Debug, Default)]
struct RecorderState {
    prepared: Option<PrepareConfig>,
    last_sample_pos: u64,
    last_event_count: usize,
}

impl Processor for Recorder {
    fn prepare(&mut self, cfg: PrepareConfig) {
        self.state.lock().expect("recorder lock").prepared = Some(cfg);
    }

    fn reset(&mut self) {}

    fn process(&mut self, block: &mut ProcessBlock<'_>) {
        let mut state = self.state.lock().expect("recorder lock");
        state.last_sample_pos = block.transport.sample_pos;
        state.last_event_count = block.in_events.len();
        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_sim_client_audio_and_midi_ports() {
    let backend = JackBackend::fake();
    let inventory = backend.enumerate().unwrap();

    assert_eq!(inventory.backend(), &jack_backend_symbol());
    assert!(inventory.devices().iter().any(|device| {
        device.id() == &Symbol::new("jack/SIM/client")
            && device.direction() == HostDirection::Duplex
    }));
    assert!(
        inventory
            .ports()
            .iter()
            .any(|port| port.id() == &Symbol::new("jack/SIM/audio_in_0")
                && port.media() == StreamMedia::Pcm)
    );
    assert!(
        inventory
            .ports()
            .iter()
            .any(|port| port.id() == &Symbol::new("jack/SIM/midi_out_0")
                && port.media() == StreamMedia::Midi)
    );
}

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

    assert!(capabilities.contains(&HostBackendCapability::AudioInput));
    assert!(capabilities.contains(&HostBackendCapability::AudioOutput));
    assert!(capabilities.contains(&HostBackendCapability::MidiInput));
    assert!(capabilities.contains(&HostBackendCapability::MidiOutput));
    assert!(capabilities.contains(&HostBackendCapability::Duplex));
}

#[test]
fn backend_opens_sim_client_and_maps_jack_clock_metadata() {
    let opened = JackBackend::fake().open_sim_client(16).unwrap();

    assert_eq!(opened.config().backend(), &jack_backend_symbol());
    assert_eq!(opened.config().device(), &Symbol::new("jack/SIM/client"));
    assert_eq!(opened.config().clock().clock(), &jack_clock_symbol());
    assert_eq!(opened.config().clock().sample_rate_hz(), Some(48_000));
    assert_eq!(opened.config().latency().input_frames(), 128);
    assert_eq!(opened.config().latency().output_frames(), 128);
}

#[test]
fn registry_selects_jack_by_backend_id() {
    let mut registry = HostBackendRegistry::new();
    registry.register(JackBackend::fake()).unwrap();
    let opened = registry
        .open(HostStreamConfigRequest::new(
            jack_backend_symbol(),
            Symbol::new("jack/SIM/client"),
            StreamMedia::Pcm,
            HostDirection::Duplex,
            BufferPolicy::bounded(4).unwrap(),
        ))
        .unwrap();

    assert_eq!(opened.config().backend(), &jack_backend_symbol());
}

#[test]
fn custom_client_ports_and_timing_reach_inventory_and_open_config() {
    let timing = JackTiming::new(96_000, 64, 32, 96).unwrap();
    let backend = JackBackend::new(vec![JackClient::new("SIM", timing, 1, 2, 0, 1).unwrap()]);
    let inventory = backend.enumerate().unwrap();
    let opened = backend.open_sim_client(64).unwrap();

    assert_eq!(inventory.devices()[0].buffer().capacity(), 64);
    assert_eq!(opened.config().clock().sample_rate_hz(), Some(96_000));
    assert_eq!(opened.config().latency().output_frames(), 96);
    assert!(
        inventory
            .ports()
            .iter()
            .any(|port| port.id() == &Symbol::new("jack/SIM/midi_out_0"))
    );
}

#[test]
fn transport_snapshot_maps_to_graph_transport_and_stream_clock() {
    let transport = JackTransportState::rolling(2048, 132.0, 16.5).unwrap();
    let graph_transport = transport.to_graph_transport();
    let clock = JackTiming::pro_audio_default().frame_clock().unwrap();

    assert!(graph_transport.playing);
    assert_eq!(graph_transport.sample_pos, 2048);
    assert_eq!(graph_transport.tempo_bpm, 132.0);
    assert_eq!(clock.id(), &jack_clock_symbol());
    assert_eq!(
        clock.chart(),
        &ClockChart::Frames {
            frames_per_second: 48_000
        }
    );
}

#[test]
fn graph_bridge_drives_process_callback_with_transport_and_midi() {
    let recorder = Recorder::default();
    let state = Arc::clone(&recorder.state);
    let mut bridge =
        JackGraphBridge::new(recorder, PcmSpec::f32(2, 48_000).unwrap(), 2, 4).unwrap();
    let midi = [JackMidiEvent::short(1, &[0x90, 60, 100]).unwrap()];
    let output = bridge
        .process_interleaved_f32(
            Some(&[1.0, 0.0, 0.0, -1.0]),
            2,
            JackTransportState::rolling(4096, 120.0, 32.0).unwrap(),
            &midi,
        )
        .unwrap();
    let state = state.lock().expect("recorder state");

    assert_eq!(output, vec![1.0, 0.0, 0.0, -1.0]);
    assert_eq!(bridge.last_transport().sample_pos(), 4096);
    assert_eq!(state.last_sample_pos, 4096);
    assert_eq!(state.last_event_count, 1);
    assert_eq!(state.prepared, Some(PrepareConfig::new(48_000, 4, 2, 2)));
}

#[test]
fn install_stream_jack_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_jack_lib,
        &Symbol::new("stream-jack"),
        &[Symbol::qualified("stream", "JackBackend")],
    );
}

#[test]
#[ignore = "hardware smoke test requires an operator-provided JACK server"]
fn jack_hardware_smoke_test_is_ignored_by_default() {
    let backend = JackBackend::default();
    let _clients = backend.list_clients();
}