sim-lib-plugin-core 0.1.0-rc.1

Common plugin descriptors, state, and graph adapters.
Documentation
use std::sync::Arc;

use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Symbol};
use sim_lib_audio_graph_core::{
    BlockArena, NullEventSink, PrepareConfig, ProcessBlock, Processor, Transport,
};

use crate::{
    HostedPluginProcessor, ParameterDescriptor, PluginDescriptor, PluginDescriptorRecord,
    PluginFormat, PluginId, PluginInstance, PluginLoadSpec, PluginState, ProcessorPlugin,
    install_plugin_core_lib, plugin_core_symbols,
};

#[test]
fn descriptors_validate_and_normalize_parameters() {
    let parameter = ParameterDescriptor::new(7, "gain", "Gain", -60.0, 6.0, 0.0).unwrap();
    assert_eq!(parameter.plain_to_normalized(-60.0), 0.0);
    assert_eq!(parameter.plain_to_normalized(6.0), 1.0);
    assert_eq!(parameter.normalized_to_plain(0.5), -27.0);

    let descriptor =
        PluginDescriptor::audio_effect(PluginFormat::Clap, "org.sim.gain", "SIM Gain", 2)
            .unwrap()
            .with_parameter(parameter);
    assert_eq!(descriptor.id.format.as_str(), "clap");
    assert_eq!(descriptor.parameter(7).unwrap().stable_id, "gain");
    assert_eq!(descriptor.ports.len(), 4);
}

#[test]
fn citizen_plugin_descriptor_round_trips_and_fails_closed() {
    let parameter = ParameterDescriptor::new(7, "gain", "Gain", -60.0, 6.0, 0.0).unwrap();
    let descriptor =
        PluginDescriptor::audio_effect(PluginFormat::Clap, "org.sim.gain", "SIM Gain", 2)
            .unwrap()
            .with_parameter(parameter);

    let record = PluginDescriptorRecord::new(descriptor.clone());
    assert_eq!(record.descriptor().unwrap(), descriptor);

    let err = PluginDescriptorRecord::from_expr(sim_kernel::Expr::Map(Vec::new())).unwrap_err();
    assert!(format!("{err}").contains("field is missing"));
}

#[test]
fn plugin_state_round_trips_as_expr() {
    let mut state = PluginState::new();
    state.set_param(7, 0.25);
    state.insert_data("preset", sim_kernel::Expr::String("small".to_owned()));

    let decoded = PluginState::from_expr(&state.to_expr()).unwrap();
    assert_eq!(decoded, state);
}

#[test]
fn plugin_load_specs_validate_location_and_format() {
    assert!(PluginLoadSpec::new(PluginFormat::Clap, " ").is_err());

    let spec = PluginLoadSpec::new(PluginFormat::Clap, "fixture://gain").unwrap();
    assert_eq!(spec.format(), PluginFormat::Clap);
    assert_eq!(spec.location(), "fixture://gain");
    assert!(spec.require_format(PluginFormat::Clap).is_ok());
    assert!(spec.require_format(PluginFormat::Wasm).is_err());
}

#[test]
fn hosted_plugin_processor_delegates_to_instance() {
    let descriptor =
        PluginDescriptor::audio_effect(PluginFormat::Clap, "org.sim.double", "Double", 1).unwrap();
    let mut hosted = HostedPluginProcessor::new(TestPlugin::new(descriptor, 2.0));
    let output = process_mono(&mut hosted, &[0.25, -0.5, 1.0]);
    assert_eq!(round6(&output[0]), vec![0.5, -1.0, 2.0]);
}

#[test]
fn processor_plugin_exports_processor_as_plugin_instance() {
    let descriptor =
        PluginDescriptor::audio_effect(PluginFormat::Sim, "org.sim.triple", "Triple", 1).unwrap();
    let mut exported = ProcessorPlugin::new(descriptor, MultiplyProcessor::new(3.0));
    let output = process_plugin(&mut exported, &[0.25, -0.5, 1.0]);
    assert_eq!(round6(&output[0]), vec![0.75, -1.5, 3.0]);
}

#[test]
fn install_plugin_core_lib_registers_runtime_exports() {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    install_plugin_core_lib(&mut cx).expect("install");
    install_plugin_core_lib(&mut cx).expect("idempotent install");

    let manifest = &cx
        .registry()
        .lib(&Symbol::new("plugin-core"))
        .expect("registered")
        .manifest;
    for symbol in plugin_core_symbols() {
        assert!(
            manifest
                .exports
                .iter()
                .any(|export| *export.symbol() == symbol),
            "missing {symbol}"
        );
    }
}

#[derive(Clone, Debug)]
struct TestPlugin {
    descriptor: PluginDescriptor,
    multiplier: f32,
}

impl TestPlugin {
    fn new(descriptor: PluginDescriptor, multiplier: f32) -> Self {
        Self {
            descriptor,
            multiplier,
        }
    }
}

impl PluginInstance for TestPlugin {
    fn descriptor(&self) -> &PluginDescriptor {
        &self.descriptor
    }

    fn prepare(&mut self, _cfg: PrepareConfig) {}

    fn reset(&mut self) {}

    fn process(&mut self, block: &mut ProcessBlock<'_>) {
        MultiplyProcessor::new(self.multiplier).process(block);
    }
}

#[derive(Clone, Debug)]
struct MultiplyProcessor {
    multiplier: f32,
}

impl MultiplyProcessor {
    fn new(multiplier: f32) -> Self {
        Self { multiplier }
    }
}

impl Processor for MultiplyProcessor {
    fn prepare(&mut self, _cfg: PrepareConfig) {}

    fn reset(&mut self) {}

    fn process(&mut self, block: &mut ProcessBlock<'_>) {
        for frame in 0..block.frames as usize {
            block.out_audio[0][frame] = block.in_audio[0][frame] * self.multiplier;
        }
    }
}

fn process_plugin(plugin: &mut dyn PluginInstance, input: &[f32]) -> Vec<Vec<f32>> {
    plugin.prepare(PrepareConfig::new(48_000, input.len() as u32, 1, 1));
    process_with(|block| plugin.process(block), input)
}

fn process_mono<P: Processor>(processor: &mut P, input: &[f32]) -> Vec<Vec<f32>> {
    processor.prepare(PrepareConfig::new(48_000, input.len() as u32, 1, 1));
    process_with(|block| processor.process(block), input)
}

fn process_with<F: FnOnce(&mut ProcessBlock<'_>)>(process: F, input: &[f32]) -> Vec<Vec<f32>> {
    let mut output = vec![vec![0.0; input.len()]];
    let mut output_refs: Vec<&mut [f32]> = output.iter_mut().map(Vec::as_mut_slice).collect();
    let mut sink = NullEventSink;
    let mut scratch = BlockArena::with_f32_capacity(input.len());
    let mut block = ProcessBlock {
        frames: input.len() as u32,
        in_audio: &[input],
        out_audio: &mut output_refs,
        in_events: &[],
        out_events: &mut sink,
        transport: Transport::default(),
        scratch: &mut scratch,
    };
    process(&mut block);
    output
}

fn round6(values: &[f32]) -> Vec<f32> {
    values
        .iter()
        .map(|value| (value * 1_000_000.0).round() / 1_000_000.0)
        .collect()
}

#[test]
fn plugin_ids_reject_empty_stable_ids() {
    assert!(PluginId::new(PluginFormat::Clap, "").is_err());
}

#[test]
fn wasm_plugin_format_has_wire_name() {
    assert_eq!(PluginFormat::Wasm.as_str(), "wasm");
}