// Integration test for a plugin "worker". // // Here we test a plugin that requires // the LV2 Worker feature. We will use eg-sampler, // a plugin that uses a worker to load a sample // from disk for playback. // // Test outline: // // 1. Write a sample to disk, a small buffer filled with 1.0 values // 2. Load eg-sampler plugin // 3. Feed in single MIDI note to event buffer // 4. Call instance.run and verify the output buffer is empty // (no sample has been loaded yet, so the output is silence). // 5. Construct an LV2 Atom event instructing the sampler to // load the sample written to disk. Add this event to event buffer. // 6. Call instance.run and verify output buffer is still empty // (the worker has not yet loaded the sample, // so we still expect the output to be silence). // 7. Run the worker // 8. Call instance.run and verify output buffer is still empty // (the worker response swaps in the loaded sample, // but this does not happen until after audio // processing occurs - so the outputs remain zero still). // 9. Feed in MIDI note to event buffer and call instance.run // 10. Verify output buffer now contains the expected sample. // // We confirm that the worker feature is operating as expected // because the sampler is correctly playing back the sample // that the worker loaded from disk. // // Obviously this test does not reflect reality perfectly // since everything is running in one thread. In a real application // the instance.run method will usually be called in the realtime // thread while the worker will be run in a non-realtime thread. use livi::event::{LV2AtomEventBuilder, LV2AtomSequence}; use livi::{EmptyPortConnections, Features, Instance, World}; use std::ffi::CStr; use std::mem::size_of; use tempfile::NamedTempFile; const MIN_BLOCK_SIZE: usize = 1; const MAX_BLOCK_SIZE: usize = 256; const SAMPLE_RATE: f64 = 44100.0; const MAX_PATH_SIZE: usize = 256; // These structs define the message // that we will deliver to the plugin // in order to trigger the sample // to be loaded. We do not talk to // the worker directly but send // this message into the plugin // instance Atom event buffer. // The plugin then communicates // with the worker to load the sample. #[repr(C)] struct PatchProperty { key: u32, context: u32, value: lv2_sys::LV2_Atom_URID, } #[repr(C)] struct PathAtom { atom: lv2_sys::LV2_Atom, body: [u8; MAX_PATH_SIZE], } #[repr(C)] struct PatchValue { key: u32, context: u32, value: PathAtom, } #[repr(C)] struct SetSamplerMessage(lv2_sys::LV2_Atom_Object_Body, PatchValue, PatchProperty); // Some helper functions fn run_instance_with_input_sequence( instance: &mut Instance, features: &Features, input: LV2AtomSequence, ) -> [Vec; 1] { let mut output_events = LV2AtomSequence::new(features, 1024); let mut outputs = [vec![0.0; MAX_BLOCK_SIZE]]; let ports = EmptyPortConnections::new() .with_atom_sequence_inputs(std::iter::once(&input)) .with_atom_sequence_outputs(std::iter::once(&mut output_events)) .with_audio_outputs(outputs.iter_mut().map(|output| output.as_mut_slice())); unsafe { instance.run(MAX_BLOCK_SIZE, ports).unwrap() }; outputs } fn run_instance_with_single_midi_note_input( instance: &mut Instance, features: &Features, ) -> [Vec; 1] { let input = { let mut s = LV2AtomSequence::new(features, 1024); let play_note_data = [0x90, 0x40, 0x7f]; s.push_midi_event::<3>(1, features.midi_urid(), &play_note_data) .unwrap(); s }; run_instance_with_input_sequence(instance, features, input) } fn build_sampler_message(features: &Features, sample_filepath: &str) -> SetSamplerMessage { let eg_sample_urid = features .urid(CStr::from_bytes_with_nul(b"http://lv2plug.in/plugins/eg-sampler#sample\0").unwrap()); let urid_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_ATOM__URID).unwrap()); let patch_property_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_PATCH__property).unwrap()); let patch_value_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_PATCH__value).unwrap()); let patch_set_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_PATCH__Set).unwrap()); let path_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_ATOM__Path).unwrap()); let mut path = [0_u8; MAX_PATH_SIZE]; path[..sample_filepath.len()].copy_from_slice(sample_filepath.as_bytes()); SetSamplerMessage( lv2_sys::LV2_Atom_Object_Body { id: 0, otype: patch_set_urid, }, PatchValue { key: patch_value_urid, context: 0, value: PathAtom { atom: lv2_sys::LV2_Atom { size: MAX_PATH_SIZE as u32, type_: path_urid, }, body: path, }, }, PatchProperty { key: patch_property_urid, context: 0, value: lv2_sys::LV2_Atom_URID { atom: lv2_sys::LV2_Atom { size: size_of::() as u32, type_: urid_urid, }, body: eg_sample_urid, }, }, ) } fn assert_silence(buffers: [Vec; 1]) { for buffer in buffers { for sample in buffer { assert_eq!(sample, 0.0); } } } fn assert_not_silence(buffers: [Vec; 1]) { for buffer in buffers { for sample in buffer { assert_ne!(sample, 0.0); } } } #[test] fn test_sampler() { let cwd = std::env::current_dir().unwrap(); let mut out_file = NamedTempFile::new_in(cwd).unwrap(); let sample = wav::bit_depth::BitDepth::ThirtyTwoFloat(vec![1.0; MAX_BLOCK_SIZE]); let header = wav::Header::new(wav::header::WAV_FORMAT_PCM, 1, SAMPLE_RATE as u32, 32); wav::write(header, &sample, &mut out_file).unwrap(); let world = World::with_load_bundle("file:///usr/lib/lv2/eg-sampler.lv2/"); let plugin = world .plugin_by_uri("http://lv2plug.in/plugins/eg-sampler") .expect("Plugin not found."); let features = world.build_features(livi::FeaturesBuilder { min_block_length: MIN_BLOCK_SIZE, max_block_length: MAX_BLOCK_SIZE, }); let mut instance = unsafe { plugin .instantiate(features.clone(), SAMPLE_RATE) .expect("Could not instantiate plugin.") }; let outputs = run_instance_with_single_midi_note_input(&mut instance, &features); assert_silence(outputs); let message = build_sampler_message(&features, out_file.path().to_str().unwrap()); let object_urid = features.urid(CStr::from_bytes_with_nul(lv2_sys::LV2_ATOM__Object).unwrap()); let input = { let mut sequence = LV2AtomSequence::new(&features, 1024); let m = &message as *const SetSamplerMessage as *const u8; let slice: &[u8] = unsafe { std::slice::from_raw_parts(m, size_of::()) }; let event = LV2AtomEventBuilder::<512>::new(0, object_urid, slice).unwrap(); sequence.push_event(&event).unwrap(); sequence }; let outputs = run_instance_with_input_sequence(&mut instance, &features, input); assert_silence(outputs); // Run the worker manually to ensure it has run before inspecting the outputs. features.worker_manager().run_workers(); let outputs = run_instance_with_single_midi_note_input(&mut instance, &features); assert_silence(outputs); let outputs = run_instance_with_single_midi_note_input(&mut instance, &features); // There is now audio content // in the outputs, indicating // that the sample file was loaded // correctly by the worker. assert_not_silence(outputs); }