From 6bd241d1859eac07e6f7ea45594f9aa7cc5ea865 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Mon, 15 Dec 2025 19:50:30 +0400 Subject: [PATCH 001/114] add process-varying-sample-rates and process-random-block-sizes test cases --- src/plugin/instance/process.rs | 14 ++- src/tests/plugin.rs | 22 +++++ src/tests/plugin/processing.rs | 152 +++++++++++++++++++++++++++++++-- 3 files changed, 178 insertions(+), 10 deletions(-) diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 4f109ab..83e494f 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -29,6 +29,8 @@ pub struct ProcessData<'a> { pub input_events: Pin>>, /// The output events. pub output_events: Pin>>, + /// The length of the current block in samples. + pub block_size: u32, config: ProcessConfig, /// The current transport information. This is populated when constructing this object, and the @@ -79,6 +81,7 @@ pub struct OutOfPlaceAudioBuffers<'a> { // `*const *const f32` _input_channel_pointers: Vec>, _output_channel_pointers: Vec>, + clap_inputs: Vec, clap_outputs: Vec, @@ -146,9 +149,10 @@ impl<'a> ProcessData<'a> { // TODO: More transport info options. Missing fields, loop regions, flags, etc. pub fn new(buffers: &'a mut AudioBuffers<'a>, config: ProcessConfig) -> Self { ProcessData { - buffers, input_events: EventQueue::new_input(), output_events: EventQueue::new_output(), + block_size: buffers.len() as u32, + buffers, config, transport_info: clap_event_transport { @@ -186,12 +190,16 @@ impl<'a> ProcessData<'a> { /// contains raw pointers to this struct's data, so the closure is there to prevent dangling /// pointers. pub fn with_clap_process_data T>(&mut self, f: F) -> T { - let num_samples = self.buffers.len(); + assert!( + self.block_size as usize <= self.buffers.len(), + "internal error: invalid block size" + ); //TODO: should this be a result instead of a panic? + let (inputs, outputs) = self.buffers.io_buffers(); let process_data = clap_process { steady_time: self.sample_pos as i64, - frames_count: num_samples as u32, + frames_count: self.block_size as u32, transport: &self.transport_info, audio_inputs: if inputs.is_empty() { std::ptr::null() diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 7f46e6e..999a5e8 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -29,6 +29,10 @@ pub enum PluginTestCase { ProcessNoteOutOfPlaceBasic, #[strum(serialize = "process-note-inconsistent")] ProcessNoteInconsistent, + #[strum(serialize = "process-varying-sample-rates")] + ProcessVaryingSampleRates, + #[strum(serialize = "process-random-block-sizes")] + ProcessRandomBlockSizes, #[strum(serialize = "param-conversions")] ParamConversions, #[strum(serialize = "param-fuzz-basic")] @@ -69,6 +73,18 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses out-of-place audio processing.", ), + PluginTestCase::ProcessVaryingSampleRates => String::from( + "Processes random audio and random note events through the plugin with its \ + default parameter values while trying different sample rates ranging from 1kHz \ + to 768kHz, and tests whether the output does not contain any non-finite or \ + subnormal values. Uses out-of-place audio processing.", + ), + PluginTestCase::ProcessRandomBlockSizes => String::from( + "Processes random audio and random note events through the plugin with maximum \ + block size of 2048 while randomizing block sizes for each process call, and \ + tests whether the output does not contain any non-finite or subnormal values. \ + Uses out-of-place audio processing.", + ), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default \ parameter values and tests the output for consistency. Uses out-of-place audio \ @@ -163,6 +179,12 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessNoteInconsistent => { processing::test_process_note_inconsistent(library, plugin_id) } + PluginTestCase::ProcessVaryingSampleRates => { + processing::test_process_varying_sample_rates(library, plugin_id) + } + PluginTestCase::ProcessRandomBlockSizes => { + processing::test_process_random_block_sizes(library, plugin_id) + } PluginTestCase::ParamConversions => params::test_param_conversions(library, plugin_id), PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id), PluginTestCase::ParamSetWrongNamespace => { diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 2888e6f..5fe1b3f 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -1,9 +1,5 @@ //! Contains most of the boilerplate around testing audio processing. -use std::sync::atomic::Ordering; - -use anyhow::{Context, Result}; - use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; @@ -15,6 +11,9 @@ use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::rng::{new_prng, NoteGenerator}; use crate::tests::TestStatus; +use anyhow::{Context, Result}; +use rand::Rng; +use std::sync::atomic::Ordering; /// A helper to handle the boilerplate that comes with testing a plugin's audio processing behavior. pub struct ProcessingTest<'a> { @@ -107,7 +106,7 @@ impl<'a> ProcessingTest<'a> { })?; process_data.clear_events(); - process_data.advance_transport(buffer_size as u32); + process_data.advance_transport(process_data.block_size); // Restart processing as necesasry if plugin @@ -187,7 +186,7 @@ impl<'a> ProcessingTest<'a> { .context("Failed during processing")?; process_data.clear_events(); - process_data.advance_transport(buffer_size as u32); + process_data.advance_transport(process_data.block_size); plugin.stop_processing(); @@ -394,6 +393,141 @@ pub fn test_process_note_inconsistent( Ok(TestStatus::Success { details: None }) } +/// The test for `ProcessingTest::ProcessVaryingSampleRates`. +pub fn test_process_varying_sample_rates( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + const SAMPLE_RATES: &[f64] = &[ + 1000.0, 10000.0, 22050.0, 32000.0, 44100.0, 48000.0, 88200.0, 96000.0, 192000.0, 384000.0, + 768000.0, 1234.5678, 12345.678, 45678.901, 123456.78, + ]; + + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => Some( + note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + ), + None => None, + }; + + let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + + for &sample_rate in SAMPLE_RATES { + host.handle_callbacks_once(); + + const BUFFER_SIZE: usize = 512; + let (mut input_buffers, mut output_buffers) = + audio_ports_config.create_buffers(BUFFER_SIZE); + + ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? + .run( + 5, + ProcessConfig { + sample_rate, + ..ProcessConfig::default() + }, + |process_data| { + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + )?; + } + + process_data.buffers.randomize(&mut prng); + Ok(()) + }, + ) + .context(format!( + "Error while processing with {:.2}hz sample rate", + sample_rate + ))?; + + host.callback_error_check() + .context("An error occured during a host callback")?; + } + + Ok(TestStatus::Success { details: None }) +} + +/// The test for `ProcessingTest::ProcessRandomBlockSizes`. +pub fn test_process_random_block_sizes( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => Some( + note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + ), + None => None, + }; + + let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + + host.handle_callbacks_once(); + + const BUFFER_SIZE: usize = 2048; + let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); + ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run( + 20, + ProcessConfig::default(), + |process_data| { + process_data.block_size = prng.gen_range(1..=BUFFER_SIZE as u32); + process_data.buffers.randomize(&mut prng); + + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + )?; + } + + Ok(()) + }, + )?; + + host.callback_error_check() + .context("An error occured during a host callback")?; + + Ok(TestStatus::Success { details: None }) +} + /// The process for consistency. This verifies that the output buffer doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. @@ -413,7 +547,11 @@ fn check_out_of_place_output_consistency( } for (port_idx, channel_slices) in output_buffers.iter().enumerate() { for (channel_idx, channel_slice) in channel_slices.iter().enumerate() { - for (sample_idx, sample) in channel_slice.iter().enumerate() { + for (sample_idx, sample) in channel_slice + .iter() + .enumerate() + .take(process_data.block_size as usize) + { if !sample.is_finite() { anyhow::bail!( "The sample written to output port {port_idx}, channel {channel_idx}, and \ From 898be1680e7ed72d75b5cd87c4a0fa076c122790 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Mon, 15 Dec 2025 20:10:27 +0400 Subject: [PATCH 002/114] add process-varying-block-sizes test --- src/tests/plugin.rs | 36 ++++++++++----- src/tests/plugin/processing.rs | 82 +++++++++++++++++++++++++++++++--- src/tests/rng.rs | 4 ++ 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 999a5e8..aafe528 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -31,6 +31,8 @@ pub enum PluginTestCase { ProcessNoteInconsistent, #[strum(serialize = "process-varying-sample-rates")] ProcessVaryingSampleRates, + #[strum(serialize = "process-varying-block-sizes")] + ProcessVaryingBlockSizes, #[strum(serialize = "process-random-block-sizes")] ProcessRandomBlockSizes, #[strum(serialize = "param-conversions")] @@ -73,18 +75,6 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses out-of-place audio processing.", ), - PluginTestCase::ProcessVaryingSampleRates => String::from( - "Processes random audio and random note events through the plugin with its \ - default parameter values while trying different sample rates ranging from 1kHz \ - to 768kHz, and tests whether the output does not contain any non-finite or \ - subnormal values. Uses out-of-place audio processing.", - ), - PluginTestCase::ProcessRandomBlockSizes => String::from( - "Processes random audio and random note events through the plugin with maximum \ - block size of 2048 while randomizing block sizes for each process call, and \ - tests whether the output does not contain any non-finite or subnormal values. \ - Uses out-of-place audio processing.", - ), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default \ parameter values and tests the output for consistency. Uses out-of-place audio \ @@ -95,6 +85,25 @@ impl<'a> TestCase<'a> for PluginTestCase { plugin with its default parameter values and tests the output for consistency. \ Uses out-of-place audio processing.", ), + PluginTestCase::ProcessVaryingSampleRates => String::from( + "Processes random audio and random note events through the plugin with its \ + default parameter values while trying different sample rates ranging from 1kHz \ + to 768kHz, including fractional rates, and tests whether the output does not \ + contain any non-finite or subnormal values. Uses out-of-place audio processing.", + ), + PluginTestCase::ProcessVaryingBlockSizes => String::from( + "Processes random audio and random note events through the plugin with its \ + default parameter values while trying different maximum block sizes ranging from \ + 1 to 32768, including non-power-of-two ones, and tests whether the output does \ + not contain any non-finite or subnormal values. Uses out-of-place audio \ + processing.", + ), + PluginTestCase::ProcessRandomBlockSizes => String::from( + "Processes random audio and random note events through the plugin with maximum \ + block size of 2048 while randomizing block sizes for each process call, and \ + tests whether the output does not contain any non-finite or subnormal values. \ + Uses out-of-place audio processing.", + ), PluginTestCase::ParamConversions => String::from( "Asserts that value to string and string to value conversions are supported for \ ether all or none of the plugin's parameters, and that conversions between \ @@ -182,6 +191,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessVaryingSampleRates => { processing::test_process_varying_sample_rates(library, plugin_id) } + PluginTestCase::ProcessVaryingBlockSizes => { + processing::test_process_varying_block_sizes(library, plugin_id) + } PluginTestCase::ProcessRandomBlockSizes => { processing::test_process_random_block_sizes(library, plugin_id) } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 5fe1b3f..602d748 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -427,14 +427,13 @@ pub fn test_process_varying_sample_rates( None => None, }; - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + const BUFFER_SIZE: usize = 512; + let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); for &sample_rate in SAMPLE_RATES { host.handle_callbacks_once(); - const BUFFER_SIZE: usize = 512; - let (mut input_buffers, mut output_buffers) = - audio_ports_config.create_buffers(BUFFER_SIZE); + let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? .run( @@ -468,6 +467,71 @@ pub fn test_process_varying_sample_rates( Ok(TestStatus::Success { details: None }) } +/// The test for `ProcessingTest::ProcessVaryingBlockSizes`. +pub fn test_process_varying_block_sizes( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + const BLOCK_SIZES: &[u32] = &[ + 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 1536, 10, 1000, 10000, 2027, + ]; + + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => Some( + note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + ), + None => None, + }; + + for &buffer_size in BLOCK_SIZES { + host.handle_callbacks_once(); + + let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let (mut input_buffers, mut output_buffers) = + audio_ports_config.create_buffers(buffer_size as usize); + + ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? + .run(5, ProcessConfig::default(), |process_data| { + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + buffer_size, + )?; + } + + process_data.buffers.randomize(&mut prng); + Ok(()) + }) + .context(format!( + "Error while processing with buffer size of {}", + buffer_size + ))?; + + host.callback_error_check() + .context("An error occured during a host callback")?; + } + + Ok(TestStatus::Success { details: None }) +} + /// The test for `ProcessingTest::ProcessRandomBlockSizes`. pub fn test_process_random_block_sizes( library: &PluginLibrary, @@ -507,14 +571,18 @@ pub fn test_process_random_block_sizes( 20, ProcessConfig::default(), |process_data| { - process_data.block_size = prng.gen_range(1..=BUFFER_SIZE as u32); - process_data.buffers.randomize(&mut prng); + process_data.block_size = if prng.gen_bool(0.8) { + prng.gen_range(1..=BUFFER_SIZE as u32) + } else { + 1 + }; + process_data.buffers.randomize(&mut prng); if let Some(note_event_rng) = note_event_rng.as_mut() { note_event_rng.fill_event_queue( &mut prng, &process_data.input_events, - BUFFER_SIZE as u32, + process_data.block_size, )?; } diff --git a/src/tests/rng.rs b/src/tests/rng.rs index be1664c..6f83f0a 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -111,6 +111,10 @@ impl NoteGenerator { queue: &EventQueue, num_samples: u32, ) -> Result<()> { + if self.config.inputs.is_empty() { + return Ok(()); + } + // The range for the next event's timing relative to the `current_sample`. This will be // capped at 0, so there's a ~58% chance the next event occurs on the same time interval as // the previous event. From c249ca8fcb701a42f27881622e4329b89a4b134b Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 16 Dec 2025 16:28:26 +0400 Subject: [PATCH 003/114] refactor: - `ProcessingTest` to allow for less boilerplate and an option to turn off denormal checks - `AudioBuffers` to allow for future 64bit process tests and in-place tests - `EventQueue` --- src/plugin/ext/audio_ports.rs | 21 - src/plugin/ext/params.rs | 11 +- src/plugin/instance/audio_thread.rs | 12 +- src/plugin/instance/process.rs | 400 +++++++++-------- src/plugin/library.rs | 4 +- src/plugin/preset_discovery.rs | 6 +- src/tests/plugin/params.rs | 77 ++-- src/tests/plugin/processing.rs | 435 +++++++++---------- src/tests/plugin/state.rs | 51 +-- src/tests/plugin_library/preset_discovery.rs | 17 +- src/tests/rng.rs | 4 +- 11 files changed, 499 insertions(+), 539 deletions(-) diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 3fa4569..7e0368f 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -258,24 +258,3 @@ fn is_audio_port_type_consistent(info: &clap_audio_port_info) -> Result<()> { Ok(()) } } - -impl AudioPortConfig { - /// Create a pair of zero initialized `(input_buffers, output_buffers)` for this audio port - /// configuration. These can be bassed with - /// [`ProcessData`][super::audio_thread::process::ProcessData] to create a process data struct. - #[allow(clippy::type_complexity)] - pub fn create_buffers(&self, buffer_size: usize) -> (Vec>>, Vec>>) { - let input_buffers: Vec>> = self - .inputs - .iter() - .map(|port_config| vec![vec![0.0; buffer_size]; port_config.num_channels as usize]) - .collect(); - let output_buffers: Vec>> = self - .outputs - .iter() - .map(|port_config| vec![vec![0.0; buffer_size]; port_config.num_channels as usize]) - .collect(); - - (input_buffers, output_buffers) - } -} diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 1ee0ed1..cf825c2 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -1,7 +1,6 @@ //! Abstractions for interacting with the `params` extension. use anyhow::{Context, Result}; -use clap_sys::events::{clap_input_events, clap_output_events}; use clap_sys::ext::params::{ clap_param_info, clap_param_info_flags, clap_plugin_params, CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, @@ -327,11 +326,7 @@ impl Params<'_> { /// # Panics /// /// Panics if the plugin is active. - pub fn flush( - &self, - input_events: &Pin>>, - output_events: &Pin>>, - ) { + pub fn flush(&self, input_events: &Pin>, output_events: &Pin>) { // This may only be called on the audio thread when the plugin is active. This object is the // main thread interface for the parameters extension. assert_plugin_state_lt!(self, PluginStatus::Activated); @@ -341,8 +336,8 @@ impl Params<'_> { unsafe_clap_call! { params=>flush( plugin, - input_events.vtable(), - output_events.vtable(), + input_events.vtable_input(), + output_events.vtable_output(), ) }; } diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 3f48f7d..e578411 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -44,14 +44,10 @@ pub enum ProcessStatus { impl Drop for PluginAudioThread<'_> { fn drop(&mut self) { - match self - .state() - .status - .compare_exchange(PluginStatus::Processing, PluginStatus::Activated) - { - Ok(_) => self.stop_processing(), - Err(PluginStatus::Activated) => (), - Err(state) => panic!( + match self.state().status.load() { + PluginStatus::Processing => self.stop_processing(), + PluginStatus::Activated => (), + state => panic!( "The plugin was in an invalid state '{state:?}' when the audio thread got \ dropped, this is a clap-validator bug" ), diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 83e494f..2b97ae4 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -19,16 +19,17 @@ use rand_pcg::Pcg32; use std::ffi::c_void; use std::pin::Pin; +use crate::plugin::ext::audio_ports::AudioPortConfig; use crate::util::check_null_ptr; /// The input and output data for a call to `clap_plugin::process()`. pub struct ProcessData<'a> { /// The input and output audio buffers. - pub buffers: &'a mut AudioBuffers<'a>, + pub buffers: &'a mut AudioBuffers, /// The input events. - pub input_events: Pin>>, + pub input_events: Pin>, /// The output events. - pub output_events: Pin>>, + pub output_events: Pin>, /// The length of the current block in samples. pub block_size: u32, @@ -55,32 +56,20 @@ pub struct ProcessConfig { pub time_sig_denominator: u16, } -/// Audio buffers for [`ProcessData`]. CLAP allows hosts to do both in-place and out-of-place -/// processing, so we'll support and test both methods. -pub enum AudioBuffers<'a> { - /// Out-of-place processing with separate non-aliasing input and output buffers. - OutOfPlace(OutOfPlaceAudioBuffers<'a>), - // TODO: In-place processing, figure out a safe abstraction for this if the in-place pairs - // aren't symmetrical between the inputs and outputs (e.g. when it's not just - // input1<->output1, input2<->output2, etc.). -} - /// Audio buffers for out-of-place processing. This wrapper allocates and sets up the channel /// pointers. To avoid an unnecessary level of abstraction where the `Vec>`s need to be /// converted to a slice of slices, this data structure borrows the vectors directly. -// -// TODO: This only does f32 for now, we'll also want to test f64 and mixed configurations later. -pub struct OutOfPlaceAudioBuffers<'a> { +/// +#[derive(Clone)] +pub struct AudioBuffers { // These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be // mutable because reborrwing them from here is the only way to modify them without // reinitializing the pointers. - inputs: &'a mut [Vec>], - outputs: &'a mut [Vec>], + buffers: Vec, // These are point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a // `*const *const f32` - _input_channel_pointers: Vec>, - _output_channel_pointers: Vec>, + _pointers: Vec>, clap_inputs: Vec, clap_outputs: Vec, @@ -89,22 +78,33 @@ pub struct OutOfPlaceAudioBuffers<'a> { num_samples: usize, } +#[derive(Clone, Debug)] +pub enum AudioBuffer { + Float32 { + input: Option, + output: Option, + data: Vec>, + }, + + Float64 { + input: Option, + output: Option, + data: Vec>, + }, +} + // SAFETY: Sharing these pointers with other threads is safe as they refer to the borrowed input and // output slices. The pointers thus cannot be invalidated. -unsafe impl Send for OutOfPlaceAudioBuffers<'_> {} -unsafe impl Sync for OutOfPlaceAudioBuffers<'_> {} +unsafe impl Send for AudioBuffers {} +unsafe impl Sync for AudioBuffers {} /// An event queue that can be used as either an input queue or an output queue. This is always /// allocated through a `Pin>` so the pointers are stable. The `VTable` type /// argument should be either `clap_input_events` or `clap_output_events`. -// -// TODO: There's not much benefit in having this be generic over the VTable and it makes it more -// dififcult to reason about. Just split this up into two concrete structs. #[derive(Debug)] -pub struct EventQueue { - /// The vtable for this event queue. This will be either `clap_input_events` or - /// `clap_output_events`. - vtable: VTable, +pub struct EventQueue { + vtable_input: clap_input_events, + vtable_output: clap_output_events, /// The actual event queue. Since we're going for correctness over performance, this uses a very /// suboptimal memory layout by just using an `enum` instead of doing fancy bit packing. pub events: Mutex>, @@ -147,10 +147,10 @@ impl<'a> ProcessData<'a> { /// [`advance_transport()`][Self::advance_transport()] method. // // TODO: More transport info options. Missing fields, loop regions, flags, etc. - pub fn new(buffers: &'a mut AudioBuffers<'a>, config: ProcessConfig) -> Self { + pub fn new(buffers: &'a mut AudioBuffers, config: ProcessConfig) -> Self { ProcessData { - input_events: EventQueue::new_input(), - output_events: EventQueue::new_output(), + input_events: EventQueue::new(), + output_events: EventQueue::new(), block_size: buffers.len() as u32, buffers, @@ -195,7 +195,7 @@ impl<'a> ProcessData<'a> { "internal error: invalid block size" ); //TODO: should this be a result instead of a panic? - let (inputs, outputs) = self.buffers.io_buffers(); + let (inputs, outputs) = self.buffers.clap_buffers(); let process_data = clap_process { steady_time: self.sample_pos as i64, @@ -213,8 +213,8 @@ impl<'a> ProcessData<'a> { }, audio_inputs_count: inputs.len() as u32, audio_outputs_count: outputs.len() as u32, - in_events: &self.input_events.vtable, - out_events: &self.output_events.vtable, + in_events: self.input_events.vtable_input(), + out_events: self.output_events.vtable_output(), }; f(process_data) @@ -248,123 +248,118 @@ impl<'a> ProcessData<'a> { } } -impl AudioBuffers<'_> { - /// The number of samples in the buffer. - pub fn len(&self) -> usize { - match self { - AudioBuffers::OutOfPlace(buffers) => buffers.len(), - } - } - - /// Pointers for the inputs and the outputs. These can be used to construct the `clap_process` - /// data. - pub fn io_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { - match self { - AudioBuffers::OutOfPlace(buffers) => buffers.io_buffers(), - } - } - - /// Get a reference to the buffer's inputs. - pub fn inputs_ref(&self) -> &[Vec>] { - match self { - AudioBuffers::OutOfPlace(buffers) => buffers.inputs, - } - } - - /// Get a reference to the buffer's outputs. - pub fn outputs_ref(&self) -> &[Vec>] { - match self { - AudioBuffers::OutOfPlace(buffers) => buffers.outputs, - } - } - - /// Fill the input and output buffers with white noise. The values are distributed between `[-1, - /// 1]`, and denormals are snapped to zero. - pub fn randomize(&mut self, prng: &mut Pcg32) { - match self { - AudioBuffers::OutOfPlace(buffers) => buffers.randomize(prng), - } - } -} - -impl<'a> OutOfPlaceAudioBuffers<'a> { +impl AudioBuffers { /// Construct the out of place audio buffers. This allocates the channel pointers that are - /// handed to the plugin in the process function. The function will return an error if the - /// sample count doesn't match between all input and outputs vectors. - pub fn new(inputs: &'a mut [Vec>], outputs: &'a mut [Vec>]) -> Result { - // We need to make sure all inputs and outputs have the same number of channels. Since zero - // channel ports are technically legal and it's also possible to not have any inputs we - // can't just start with the first input. - let mut num_samples = None; - for channel_slices in inputs.iter().chain(outputs.iter()) { - for channel_slice in channel_slices { - match num_samples { - Some(num_samples) if channel_slice.len() != num_samples => anyhow::bail!( - "Inconsistent sample counts in audio buffers. Expected {}, found {}.", - num_samples, - channel_slice.len() - ), - Some(_) => (), - None => num_samples = Some(channel_slice.len()), - } - } - } - - let input_channel_pointers: Vec> = inputs - .iter() - .map(|channel_slices| { - channel_slices - .iter() - .map(|channel_slice| channel_slice.as_ptr()) - .collect() - }) - .collect(); - // These are always `*const` pointers in CLAP, even for output buffers - let output_channel_pointers: Vec> = outputs - .iter() - .map(|channel_slices| { - channel_slices - .iter() - .map(|channel_slice| channel_slice.as_ptr()) - .collect() - }) - .collect(); - - let clap_inputs: Vec = input_channel_pointers - .iter() - .map(|channel_pointers| clap_audio_buffer { - data32: channel_pointers.as_ptr(), + /// handed to the plugin in the process function. + pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: usize) -> Result { + assert!(num_samples > 0); + + let mut buffers = vec![]; + let mut pointers = vec![]; + let mut clap_inputs = vec![]; + let mut clap_outputs = vec![]; + + for (port, index) in config.inputs.iter().zip(0u32..) { + let bus_data: Vec> = + vec![vec![0.0f32; num_samples]; port.num_channels as usize]; + let bus_ptrs: Vec<*const ()> = bus_data + .iter() + .map(|channel| channel.as_ptr() as *const _) + .collect(); + + clap_inputs.push(clap_audio_buffer { + data32: bus_ptrs.as_ptr() as *const *const f32, data64: std::ptr::null(), - channel_count: channel_pointers.len() as u32, + channel_count: port.num_channels, // TODO: Do some interesting tests with these two fields latency: 0, constant_mask: 0, - }) - .collect(); - let clap_outputs: Vec = output_channel_pointers - .iter() - .map(|channel_pointers| clap_audio_buffer { - data32: channel_pointers.as_ptr(), + }); + buffers.push(AudioBuffer::Float32 { + input: Some(index), + output: None, + data: bus_data, + }); + pointers.push(bus_ptrs); + } + + for (port, index) in config.outputs.iter().zip(0u32..) { + let bus_data: Vec> = + vec![vec![0.0f32; num_samples]; port.num_channels as usize]; + let bus_ptrs: Vec<*const ()> = bus_data + .iter() + .map(|channel| channel.as_ptr() as *const _) + .collect(); + + clap_outputs.push(clap_audio_buffer { + data32: bus_ptrs.as_ptr() as *const *const f32, data64: std::ptr::null(), - channel_count: channel_pointers.len() as u32, + channel_count: port.num_channels, + // TODO: Do some interesting tests with these two fields latency: 0, constant_mask: 0, - }) - .collect(); + }); + buffers.push(AudioBuffer::Float32 { + input: None, + output: Some(index), + data: bus_data, + }); + pointers.push(bus_ptrs); + } Ok(Self { - inputs, - outputs, - _input_channel_pointers: input_channel_pointers, - _output_channel_pointers: output_channel_pointers, + buffers, + _pointers: pointers, clap_inputs, clap_outputs, - - // This cannot default to 0, because 0 isn't a valid buffer size in CLAP - num_samples: num_samples.unwrap_or(512), + num_samples, }) } + // pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: usize) -> Result { + // assert!(num_samples > 0); + + // let mut buffers = vec![]; + // let mut pointers = vec![]; + // let mut clap_inputs = vec![]; + // let mut clap_outputs = vec![]; + + // for (port, index) in config.inputs.iter().zip(0u32..) { + // let mut bus_data: Vec> = + // vec![vec![0.0f32; num_samples]; port.num_channels as usize]; + // let mut bus_ptrs: Vec<*const ()> = bus_data + // .iter() + // .map(|channel| channel.as_ptr() as *const _) + // .collect(); + // let bus_ptr = bus_ptrs.as_ptr(); + + // pointers.push(bus_ptrs); + // buffers.push(AudioBuffer::Float32 { + // input: Some(index), + // output: port.in_place_pair_idx.map(|x| x as u32), + // data: bus_data, + // }); + // clap_inputs.push(clap_audio_buffer { + // data32: bus_ptrs.as_ptr() as *const *const f32, + // data64: std::ptr::null(), + // channel_count: port.num_channels, + // // TODO: Do some interesting tests with these two fields + // latency: 0, + // constant_mask: 0, + // }); + // } + + // for (port, index) in config.outputs.iter().zip(0u32..) {} + + // Ok(Self { + // buffers, + // _pointers: pointers, + // clap_inputs, + // clap_outputs, + // num_samples, + // }) + // } + /// The number of samples in the buffer. pub fn len(&self) -> usize { self.num_samples @@ -372,62 +367,123 @@ impl<'a> OutOfPlaceAudioBuffers<'a> { /// Pointers for the inputs and the outputs. These can be used to construct the `clap_process` /// data. - pub fn io_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { + pub fn clap_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { (&self.clap_inputs, &mut self.clap_outputs) } + pub fn buffers(&self) -> &[AudioBuffer] { + &self.buffers + } + + pub fn fill_f32(&mut self, mut next: impl FnMut() -> f32) { + for bus in &mut self.buffers { + match bus { + AudioBuffer::Float32 { data, .. } => { + for channel in data { + for sample in channel { + *sample = next(); + } + } + } + _ => {} + } + } + } + + pub fn fill_f64(&mut self, mut next: impl FnMut() -> f64) { + for bus in &mut self.buffers { + match bus { + AudioBuffer::Float64 { data, .. } => { + for channel in data { + for sample in channel { + *sample = next(); + } + } + } + _ => {} + } + } + } + /// Fill the input and output buffers with white noise. The values are distributed between `[-1, /// 1]`, and denormals are snapped to zero. pub fn randomize(&mut self, prng: &mut Pcg32) { - randomize_audio_buffers(prng, self.inputs); - randomize_audio_buffers(prng, self.outputs); + self.fill_f32(|| { + let y = prng.gen_range(-1.0..=1.0f32); + if y.is_subnormal() { + 0.0 + } else { + y + } + }); + + self.fill_f64(|| { + let y = prng.gen_range(-1.0..=1.0f64); + if y.is_subnormal() { + 0.0 + } else { + y + } + }); + } +} + +impl AudioBuffer { + pub fn is_input(&self) -> bool { + matches!( + self, + AudioBuffer::Float32 { input: Some(_), .. } + | AudioBuffer::Float64 { input: Some(_), .. } + ) + } + + pub fn is_output(&self) -> bool { + matches!( + self, + AudioBuffer::Float32 { + output: Some(_), + .. + } | AudioBuffer::Float64 { + output: Some(_), + .. + } + ) } } -impl EventQueue { +impl EventQueue { /// Construct a new event queue. This can be used as both an input and an output queue. - pub fn new_input() -> Pin> { - let mut queue = Box::pin(EventQueue { - vtable: clap_input_events { + pub fn new() -> Pin> { + let mut queue = Box::pin(Self { + vtable_input: clap_input_events { // This is set to point to this object below ctx: std::ptr::null_mut(), size: Some(Self::size), get: Some(Self::get), }, - // Using a mutex here is obviously a terrible idea in a real host, but we're not a real - // host - events: Mutex::new(Vec::new()), - }); - - queue.vtable.ctx = &*queue as *const Self as *mut c_void; - - queue - } -} -impl EventQueue { - /// Construct a new output event queue. - pub fn new_output() -> Pin> { - let mut queue = Box::pin(EventQueue { - vtable: clap_output_events { + vtable_output: clap_output_events { // This is set to point to this object below ctx: std::ptr::null_mut(), try_push: Some(Self::try_push), }, + // Using a mutex here is obviously a terrible idea in a real host, but we're not a real // host events: Mutex::new(Vec::new()), }); - queue.vtable.ctx = &*queue as *const Self as *mut c_void; - + queue.vtable_input.ctx = &*queue as *const Self as *mut c_void; + queue.vtable_output.ctx = &*queue as *const Self as *mut c_void; queue } -} -impl EventQueue { - pub fn vtable(self: &Pin>) -> *const VTable { - &self.vtable + pub fn vtable_input(self: &Pin>) -> *const clap_input_events { + &self.vtable_input + } + + pub fn vtable_output(self: &Pin>) -> *const clap_output_events { + &self.vtable_output } unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { @@ -517,17 +573,3 @@ impl Event { } } } - -/// Set each sample in the buffers to a random value in `[-1, 1]`. Denormals are snapped to zero. -fn randomize_audio_buffers(prng: &mut Pcg32, buffers: &mut [Vec>]) { - for channel_slices in buffers { - for channel_slice in channel_slices { - for sample in channel_slice { - *sample = prng.gen_range(-1.0..=1.0); - if sample.is_subnormal() { - *sample = 0.0; - } - } - } - } -} diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 7fb113c..054e566 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -231,7 +231,7 @@ impl PluginLibrary { /// IDs supported by this plugin library can be found by calling /// [`metadata()`][Self::metadata()]. The returned plugin has not yet been initialized, and /// `destroy()` will be called automatically when the object is dropped. - pub fn create_plugin(&self, id: &str, host: Rc) -> Result { + pub fn create_plugin(&self, id: &str, host: Rc) -> Result> { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); let plugin_factory = unsafe_clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } @@ -248,7 +248,7 @@ impl PluginLibrary { } /// Returns the plugin's preset discovery factory, if it has one. - pub fn preset_discovery_factory(&self) -> Result { + pub fn preset_discovery_factory(&self) -> Result> { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); let preset_discovery_factory = unsafe_clap_call! { diff --git a/src/plugin/preset_discovery.rs b/src/plugin/preset_discovery.rs index 48407a4..b5103e0 100644 --- a/src/plugin/preset_discovery.rs +++ b/src/plugin/preset_discovery.rs @@ -15,8 +15,8 @@ mod indexer; mod metadata_receiver; mod provider; -pub use self::indexer::{FileType, Flags, IndexerResults, Location, LocationValue, Soundpack}; -pub use self::metadata_receiver::{PluginAbi, Preset, PresetFile, PresetFlags}; +pub use self::indexer::{Flags, Location, LocationValue, Soundpack}; +pub use self::metadata_receiver::{PluginAbi, Preset, PresetFile}; pub use self::provider::Provider; /// A `Send+Sync` wrapper around `*const clap_preset_discovery_factory`. @@ -134,7 +134,7 @@ impl<'lib> PresetDiscoveryFactory<'lib> { /// [`metadata()`][Self::metadata()]. /// /// Returns an error if the provider's CLAP version is not supported. - pub fn create_provider(&self, metadata: &ProviderMetadata) -> Result { + pub fn create_provider(&self, metadata: &ProviderMetadata) -> Result> { if !clap_version_is_compatible(metadata.clap_version()) { anyhow::bail!( "The preset provider with ID '{}' has an unsupported CLAP version {:?}.", diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 3725c48..f859867 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -7,15 +7,15 @@ use rand::Rng; use serde::Serialize; use std::collections::BTreeMap; -use super::processing::ProcessingTest; use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::params::Params; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{Event, ProcessConfig}; +use crate::plugin::instance::process::{AudioBuffers, Event}; use crate::plugin::library::PluginLibrary; +use crate::tests::plugin::ProcessingTest; use crate::tests::rng::{new_prng, NoteGenerator, ParamFuzzer}; use crate::tests::{TestCase, TestStatus}; @@ -215,7 +215,8 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result let audio_ports_config = audio_ports .map(|ports| ports.config()) .transpose() - .context("Could not fetch the plugin's audio port config")?; + .context("Could not fetch the plugin's audio port config")? + .unwrap_or_default(); let note_ports_config = note_ports .map(|ports| ports.config()) .transpose() @@ -236,41 +237,35 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result // to a file if the test fails let mut current_events: Option>; let mut previous_events: Option> = None; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE)?; - let (mut input_buffers, mut output_buffers) = audio_ports_config - .unwrap_or_default() - .create_buffers(BUFFER_SIZE); for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); let mut have_set_parameters = false; - let run_result = - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run( - FUZZ_RUNS_PER_PERMUTATION, - ProcessConfig::default(), - |process_data| { - if !have_set_parameters { - *process_data.input_events.events.lock() = - current_events.clone().unwrap(); - have_set_parameters = true; - } - - // Audio and MIDI/note events are randomized in accordance to what the plugin - // supports - if let Some(note_event_rng) = note_event_rng.as_mut() { - // This includes a sort if `random_param_set_events` also contained a queue - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - )?; - } - process_data.buffers.randomize(&mut prng); - - Ok(()) - }, - ); + let run_result = ProcessingTest::new(&plugin, &mut audio_buffers).run( + FUZZ_RUNS_PER_PERMUTATION, + |process_data| { + if !have_set_parameters { + *process_data.input_events.events.lock() = current_events.clone().unwrap(); + have_set_parameters = true; + } + + // Audio and MIDI/note events are randomized in accordance to what the plugin + // supports + if let Some(note_event_rng) = note_event_rng.as_mut() { + // This includes a sort if `random_param_set_events` also contained a queue + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + )?; + } + process_data.buffers.randomize(&mut prng); + + Ok(()) + }, + ); // If the run failed we'll want to write the parameter values to a file first if run_result.is_err() { @@ -374,6 +369,7 @@ pub fn test_param_set_wrong_namespace( let param_fuzzer = ParamFuzzer::new(¶m_infos); let mut random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + for event in random_param_set_events.iter_mut() { match event { Event::ParamValue(event) => event.header.space_id = INCORRECT_NAMESPACE_ID, @@ -381,15 +377,12 @@ pub fn test_param_set_wrong_namespace( } } - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run_once( - ProcessConfig::default(), - move |process_data| { - *process_data.input_events.events.lock() = random_param_set_events; - - Ok(()) - }, - )?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run_once(|process_data| { + process_data.buffers.randomize(&mut prng); + *process_data.input_events.events.lock() = random_param_set_events; + Ok(()) + })?; // We'll check that the plugin has these sames values after reloading the state. These values // are rounded to the tenth decimal to provide some leeway in the serialization and diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 602d748..909f4b2 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -4,9 +4,7 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{ - AudioBuffers, OutOfPlaceAudioBuffers, ProcessConfig, ProcessData, -}; +use crate::plugin::instance::process::{AudioBuffer, AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::rng::{new_prng, NoteGenerator}; @@ -16,53 +14,65 @@ use rand::Rng; use std::sync::atomic::Ordering; /// A helper to handle the boilerplate that comes with testing a plugin's audio processing behavior. +/// Run the standard audio processing test for a still **deactivated** plugin. This calls the +/// process function `num_iters` times, and checks the output for consistency each time. +/// +/// The `Preprocess` closure is called before each processing cycle to allow the process data to be +/// modified for the next process cycle. +/// +/// Main-thread callbacks that were made to the plugin while the audio thread was active are +/// handled implicitly. pub struct ProcessingTest<'a> { plugin: &'a Plugin<'a>, - audio_buffers: AudioBuffers<'a>, + buffers: &'a mut AudioBuffers, + config: ProcessConfig, + check_denormals: bool, } impl<'a> ProcessingTest<'a> { - /// Construct a new processing test using out-of-place processing. This allocates the CLAP audio - /// buffer structs needed for the test. Returns an error if the the inner vectors don't all have - /// the same length. - pub fn new_out_of_place( - plugin: &'a Plugin<'a>, - input_buffers: &'a mut [Vec>], - output_buffers: &'a mut [Vec>], - ) -> Result { - Ok(Self { + pub fn new(plugin: &'a Plugin<'a>, buffers: &'a mut AudioBuffers) -> Self { + Self { plugin, - audio_buffers: AudioBuffers::OutOfPlace(OutOfPlaceAudioBuffers::new( - input_buffers, - output_buffers, - )?), - }) + buffers, + config: ProcessConfig::default(), + check_denormals: true, + } + } + + #[allow(unused)] //TODO: use this for future denormal tests + pub fn allow_denormals(self) -> Self { + Self { + check_denormals: false, + ..self + } + } + + pub fn with_sample_rate(self, sample_rate: f64) -> Self { + Self { + config: ProcessConfig { + sample_rate, + ..self.config + }, + ..self + } } - /// Run the standard audio processing test for a still **deactivated** plugin. This calls the - /// process function `num_iters` times, and checks the output for consistency each time. - /// - /// The `Preprocess` closure is called before each processing cycle to allow the process data to be - /// modified for the next process cycle. - /// - /// Main-thread callbacks that were made to the plugin while the audio thread was active are - /// handled implicitly. - pub fn run( - &'a mut self, - num_iters: usize, - process_config: ProcessConfig, - mut preprocess: Preprocess, - ) -> Result<()> + pub fn run(self, num_iters: usize, mut preprocess: Preprocess) -> Result<()> where Preprocess: FnMut(&mut ProcessData) -> Result<()> + Send, { + // Handle callbacks the plugin may have made during init or these queries. The + // `ProcessingTest::run*` functions will implicitly handle all outstanding callbacks before they + // return. + self.plugin.host().handle_callbacks_once(); + self.plugin .state .requested_restart .store(false, Ordering::SeqCst); - let buffer_size = self.audio_buffers.len(); - let mut process_data = ProcessData::new(&mut self.audio_buffers, process_config); + let buffer_size = self.buffers.len(); + let mut process_data = ProcessData::new(self.buffers, self.config); // If the plugin requests a restart in the middle of processing, then the plugin will be // stopped, deactivated, reactivated, and started again. Because of that, we need to keep @@ -70,7 +80,7 @@ impl<'a> ProcessingTest<'a> { let mut iters_done = 0; while iters_done < num_iters { self.plugin - .activate(process_config.sample_rate, 1, buffer_size)?; + .activate(self.config.sample_rate, 1, buffer_size)?; self.plugin.on_audio_thread(|plugin| -> Result<()> { plugin.start_processing()?; @@ -84,19 +94,17 @@ impl<'a> ProcessingTest<'a> { // We'll check that the plugin hasn't modified the input buffers after the // test - let original_input_buffers = process_data.buffers.inputs_ref().to_owned(); + let original_buffers = process_data.buffers.clone(); plugin .process(&mut process_data) .context("Error during audio processing")?; - // When we add in-place processing this will need some slightly different checks - match process_data.buffers { - AudioBuffers::OutOfPlace(_) => check_out_of_place_output_consistency( - &process_data, - &original_input_buffers, - ), - } + check_process_call_consistency( + &process_data, + original_buffers, + self.check_denormals, + ) .with_context(|| { format!( "Failed during processing cycle {} out of {}", @@ -139,66 +147,15 @@ impl<'a> ProcessingTest<'a> { Ok(()) } - /// Run the standard audio processing test for a still **deactivated** plugin. This is identical - /// to the [`run()`][Self::run()] function, except that it does exactly one processing cycle and - /// thus non-copy values can be moved into the closure. - /// - /// Main-thread callbacks that were made to the plugin while the audio thread was active are - /// handled implicitly. - pub fn run_once( - &'a mut self, - process_config: ProcessConfig, - preprocess: Preprocess, - ) -> Result<()> + pub fn run_once(self, preprocess: Preprocess) -> Result<()> where Preprocess: FnOnce(&mut ProcessData) -> Result<()> + Send, { - self.plugin - .state - .requested_restart - .store(false, Ordering::SeqCst); - - let buffer_size = self.audio_buffers.len(); - let mut process_data = ProcessData::new(&mut self.audio_buffers, process_config); - - self.plugin - .activate(process_config.sample_rate, 1, buffer_size)?; - - self.plugin.on_audio_thread(|plugin| -> Result<()> { - plugin.start_processing()?; - - preprocess(&mut process_data)?; - - // We'll check that the plugin hasn't modified the input buffers after the - // test - let original_input_buffers = process_data.buffers.inputs_ref().to_owned(); - - plugin - .process(&mut process_data) - .context("Error during audio processing")?; - - // When we add in-place processing this will need some slightly different checks - match process_data.buffers { - AudioBuffers::OutOfPlace(_) => { - check_out_of_place_output_consistency(&process_data, &original_input_buffers) - } - } - .context("Failed during processing")?; - - process_data.clear_events(); - process_data.advance_transport(process_data.block_size); - - plugin.stop_processing(); - - Ok(()) - })?; - - self.plugin.deactivate(); - - // Handle callbacks the plugin may have made during deactivate - self.plugin.host().handle_callbacks_once(); - - Ok(()) + let mut preprocess = Some(preprocess); + self.run(1, |data| match preprocess.take() { + Some(preprocess) => preprocess(data), + None => unreachable!(), + }) } } @@ -228,21 +185,12 @@ pub fn test_process_audio_out_of_place_basic( }) } }; - // Handle callbacks the plugin may have made during init or these queries. The - // `ProcessingTest::run*` functions will implicitly handle all outstanding callbacks before they - // return. - host.handle_callbacks_once(); - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(512); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run( - 5, - ProcessConfig::default(), - |process_data| { - process_data.buffers.randomize(&mut prng); - - Ok(()) - }, - )?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + process_data.buffers.randomize(&mut prng); + Ok(()) + })?; // The `Host` contains built-in thread safety checks host.callback_error_check() @@ -294,28 +242,20 @@ pub fn test_process_note_out_of_place_basic( )), }); } - host.handle_callbacks_once(); // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(note_ports_config); - - const BUFFER_SIZE: usize = 512; - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run( - 5, - ProcessConfig::default(), - |process_data| { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - )?; - process_data.buffers.randomize(&mut prng); - - Ok(()) - }, - )?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + process_data.buffers.randomize(&mut prng); + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + Ok(()) + })?; host.callback_error_check() .context("An error occured during a host callback")?; @@ -369,24 +309,18 @@ pub fn test_process_note_inconsistent( // This RNG (Random Note Generator) allows generates mismatching events let mut note_event_rng = NoteGenerator::new(note_port_config).with_inconsistent_events(); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; // TODO: Use in-place processing for this test - const BUFFER_SIZE: usize = 512; - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run( - 5, - ProcessConfig::default(), - |process_data| { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - )?; - process_data.buffers.randomize(&mut prng); - - Ok(()) - }, - )?; + ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + process_data.buffers.randomize(&mut prng); + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + Ok(()) + })?; host.callback_error_check() .context("An error occured during a host callback")?; @@ -427,34 +361,25 @@ pub fn test_process_varying_sample_rates( None => None, }; - const BUFFER_SIZE: usize = 512; - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); - + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; for &sample_rate in SAMPLE_RATES { - host.handle_callbacks_once(); - let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run( - 5, - ProcessConfig { - sample_rate, - ..ProcessConfig::default() - }, - |process_data| { - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - )?; - } + ProcessingTest::new(&plugin, &mut audio_buffers) + .with_sample_rate(sample_rate) + .run(5, |process_data| { + process_data.buffers.randomize(&mut prng); - process_data.buffers.randomize(&mut prng); - Ok(()) - }, - ) + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + } + + Ok(()) + }) .context(format!( "Error while processing with {:.2}hz sample rate", sample_rate @@ -501,23 +426,22 @@ pub fn test_process_varying_block_sizes( }; for &buffer_size in BLOCK_SIZES { - host.handle_callbacks_once(); - let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); - let (mut input_buffers, mut output_buffers) = - audio_ports_config.create_buffers(buffer_size as usize); + let mut audio_buffers = + AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize)?; + + ProcessingTest::new(&plugin, &mut audio_buffers) + .run(5, |process_data| { + process_data.buffers.randomize(&mut prng); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run(5, ProcessConfig::default(), |process_data| { if let Some(note_event_rng) = note_event_rng.as_mut() { note_event_rng.fill_event_queue( &mut prng, &process_data.input_events, - buffer_size, + process_data.block_size, )?; } - process_data.buffers.randomize(&mut prng); Ok(()) }) .context(format!( @@ -537,6 +461,8 @@ pub fn test_process_random_block_sizes( library: &PluginLibrary, plugin_id: &str, ) -> Result { + const MAX_BUFFER_SIZE: u32 = 2048; + let mut prng = new_prng(); let host = Host::new(); @@ -562,33 +488,28 @@ pub fn test_process_random_block_sizes( }; let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut audio_buffers = + AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize)?; - host.handle_callbacks_once(); + ProcessingTest::new(&plugin, &mut audio_buffers).run(20, |process_data| { + process_data.block_size = if prng.gen_bool(0.8) { + prng.gen_range(2..=MAX_BUFFER_SIZE) + } else { + 1 + }; - const BUFFER_SIZE: usize = 2048; - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(BUFFER_SIZE); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run( - 20, - ProcessConfig::default(), - |process_data| { - process_data.block_size = if prng.gen_bool(0.8) { - prng.gen_range(1..=BUFFER_SIZE as u32) - } else { - 1 - }; + process_data.buffers.randomize(&mut prng); - process_data.buffers.randomize(&mut prng); - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - )?; - } + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + } - Ok(()) - }, - )?; + Ok(()) + })?; host.callback_error_check() .context("An error occured during a host callback")?; @@ -599,37 +520,83 @@ pub fn test_process_random_block_sizes( /// The process for consistency. This verifies that the output buffer doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. -fn check_out_of_place_output_consistency( +fn check_process_call_consistency( process_data: &ProcessData, - original_input_buffers: &[Vec>], + original_buffers: AudioBuffers, + check_denormals: bool, ) -> Result<()> { - // The input buffer must not be overwritten during out of place processing, and the outputs - // should not contain any non-finite or denormal values - let num_samples = process_data.buffers.len() as u32; - let input_buffers = process_data.buffers.inputs_ref(); - let output_buffers = process_data.buffers.outputs_ref(); - if input_buffers != original_input_buffers { - anyhow::bail!( - "The plugin has overwritten the input buffers during out-of-place processing." - ); - } - for (port_idx, channel_slices) in output_buffers.iter().enumerate() { - for (channel_idx, channel_slice) in channel_slices.iter().enumerate() { - for (sample_idx, sample) in channel_slice - .iter() - .enumerate() - .take(process_data.block_size as usize) - { - if !sample.is_finite() { - anyhow::bail!( - "The sample written to output port {port_idx}, channel {channel_idx}, and \ - sample index {sample_idx} is {sample:?}." - ); - } else if sample.is_subnormal() { - anyhow::bail!( - "The sample written to output port {port_idx}, channel {channel_idx}, and \ - sample index {sample_idx} is subnormal ({sample:?})." - ); + let block_size = process_data.block_size as usize; + + for (buffer, before) in process_data + .buffers + .buffers() + .iter() + .zip(original_buffers.buffers()) + { + // Input buffers must not be overwritten during out of place processing + if buffer.is_input() && !buffer.is_output() { + let matches = match (buffer, before) { + ( + AudioBuffer::Float32 { data: after, .. }, + AudioBuffer::Float32 { data: before, .. }, + ) => after == before, + + ( + AudioBuffer::Float64 { data: after, .. }, + AudioBuffer::Float64 { data: before, .. }, + ) => after == before, + + _ => unreachable!(), + }; + + if !matches { + anyhow::bail!( + "The plugin has overwritten the input buffers during out-of-place processing." + ); + } + } + + // Output buffers must not contain any non-finite or denormal values + if buffer.is_output() { + match buffer { + AudioBuffer::Float32 { data, output, .. } => { + let port_idx = output.unwrap(); + for (channel_idx, channel) in data.iter().enumerate() { + for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { + if !sample.is_finite() { + anyhow::bail!( + "The sample written to output port {port_idx}, channel \ + {channel_idx}, and sample index {sample_idx} is {sample:?}." + ); + } else if sample.is_subnormal() && check_denormals { + anyhow::bail!( + "The sample written to output port {port_idx}, channel \ + {channel_idx}, and sample index {sample_idx} is subnormal \ + ({sample:?})." + ); + } + } + } + } + + AudioBuffer::Float64 { data, output, .. } => { + let port_idx = output.unwrap(); + for (channel_idx, channel) in data.iter().enumerate() { + for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { + if !sample.is_finite() { + anyhow::bail!( + "The sample written to output port {port_idx}, channel \ + {channel_idx}, and sample index {sample_idx} is {sample:?}." + ); + } else if sample.is_subnormal() && check_denormals { + anyhow::bail!( + "The sample written to output port {port_idx}, channel \ + {channel_idx}, and sample index {sample_idx} is subnormal \ + ({sample:?})." + ); + } + } + } } } } @@ -649,10 +616,10 @@ fn check_out_of_place_output_consistency( last_event_time = event_time; } - if last_event_time >= num_samples { + if last_event_time >= block_size as u32 { anyhow::bail!( "The plugin output an event for sample {last_event_time} but the audio buffer only \ - contains {num_samples} samples." + contains {block_size} samples." ) } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 8410ea5..c050f05 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -5,19 +5,18 @@ use clap_sys::id::clap_id; use std::collections::BTreeMap; use std::io::Write; +use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::params::{ParamInfo, Params}; use crate::plugin::ext::state::State; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{Event, EventQueue, ProcessConfig}; +use crate::plugin::instance::process::{AudioBuffers, Event, EventQueue}; use crate::plugin::library::PluginLibrary; +use crate::tests::plugin::ProcessingTest; use crate::tests::rng::{new_prng, ParamFuzzer}; use crate::tests::{TestCase, TestStatus}; -use super::processing::ProcessingTest; -use super::PluginTestCase; - /// The file name we'll use to dump the expected state when a test fails. const EXPECTED_STATE_FILE_NAME: &str = "state-expected"; /// The file name we'll use to dump the actual state when a test fails. @@ -137,13 +136,11 @@ pub fn test_state_reproducibility_null_cookies( } } - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(512); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run_once(ProcessConfig::default(), move |process_data| { - *process_data.input_events.events.lock() = random_param_set_events; - - Ok(()) - })?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { + *process_data.input_events.events.lock() = random_param_set_events; + Ok(()) + })?; // We'll check that the plugin has these sames values after reloading the state. These // values are rounded to the tenth decimal to provide some leeway in the serialization and @@ -305,9 +302,9 @@ pub fn test_state_reproducibility_flush( let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); - let input_events = EventQueue::new_input(); + let input_events = EventQueue::new(); + let output_events = EventQueue::new(); *input_events.events.lock() = random_param_set_events.clone(); - let output_events = EventQueue::new_output(); params.flush(&input_events, &output_events); host.handle_callbacks_once(); @@ -400,16 +397,12 @@ pub fn test_state_reproducibility_flush( } } - // In theprevious pass we used flush, and here we use the process funciton - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(512); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)?.run_once( - ProcessConfig::default(), - move |process_data| { - *process_data.input_events.events.lock() = new_random_param_set_events; - - Ok(()) - }, - )?; + // In the previous pass we used flush, and here we use the process funciton + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { + *process_data.input_events.events.lock() = new_random_param_set_events; + Ok(()) + })?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -495,7 +488,6 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> }) } }; - host.handle_callbacks_once(); let param_infos = params .info() @@ -503,13 +495,12 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let param_fuzzer = ParamFuzzer::new(¶m_infos); let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); - let (mut input_buffers, mut output_buffers) = audio_ports_config.create_buffers(512); - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run_once(ProcessConfig::default(), move |process_data| { - *process_data.input_events.events.lock() = random_param_set_events; - Ok(()) - })?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { + *process_data.input_events.events.lock() = random_param_set_events; + Ok(()) + })?; let expected_param_values: BTreeMap = param_infos .keys() diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 10a01d1..8ac842d 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -9,7 +9,7 @@ use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::preset_load::PresetLoad; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::ProcessConfig; +use crate::plugin::instance::process::AudioBuffers; use crate::plugin::library::PluginLibrary; use crate::plugin::preset_discovery::{LocationValue, PluginAbi, Preset, PresetFile}; use crate::tests::plugin::ProcessingTest; @@ -17,9 +17,6 @@ use crate::tests::TestStatus; // TODO: Test for duplicate locations and soundpacks in declared data across all providers -/// The fixed buffer size to use for these tests. -const BUFFER_SIZE: usize = 512; - /// The test for `PluginLibraryTestCase::PresetDiscoveryCrawl`. Makes sure that all of a plugin's /// reported preset locations can be crawled successfully. If `load_presets` is enabled, then the /// crawled presets are also loaded. @@ -143,10 +140,10 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result let audio_ports_config = audio_ports .map(|ports| ports.config()) .transpose() - .context("Could not fetch the plugin's audio port config")?; - let (mut input_buffers, mut output_buffers) = audio_ports_config - .unwrap_or_default() - .create_buffers(BUFFER_SIZE); + .context("Could not fetch the plugin's audio port config")? + .unwrap_or_default(); + + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; for LoadablePreset { location, @@ -181,8 +178,8 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // We'll process a single buffer of silent audio just to make sure everything's // settled in - ProcessingTest::new_out_of_place(&plugin, &mut input_buffers, &mut output_buffers)? - .run_once(ProcessConfig::default(), |_| Ok(())) + ProcessingTest::new(&plugin, &mut audio_buffers) + .run_once(move |_| Ok(())) .with_context(|| { format!( "Error while processing an audio buffer after loading a preset for \ diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 6f83f0a..9b21128 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -105,10 +105,10 @@ impl NoteGenerator { /// /// Returns an error if generating random events failed. This can happen if the plugin doesn't /// support any note event types. - pub fn fill_event_queue( + pub fn fill_event_queue( &mut self, prng: &mut Pcg32, - queue: &EventQueue, + queue: &EventQueue, num_samples: u32, ) -> Result<()> { if self.config.inputs.is_empty() { From 8d431e33ec6aa3132b32fde1651ada5b763eb1a2 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 17 Dec 2025 00:20:57 +0400 Subject: [PATCH 004/114] add in-place audio process test --- src/plugin/ext/audio_ports.rs | 2 +- src/plugin/instance/process.rs | 319 ++++++++++++------- src/tests/plugin.rs | 10 + src/tests/plugin/params.rs | 4 +- src/tests/plugin/processing.rs | 89 ++++-- src/tests/plugin/state.rs | 6 +- src/tests/plugin_library/preset_discovery.rs | 2 +- 7 files changed, 286 insertions(+), 146 deletions(-) diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 7e0368f..eb7fa13 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -157,7 +157,7 @@ impl AudioPorts<'_> { if output_pair_stable_id == input_stable_id => { config.inputs[*input_port_idx].in_place_pair_idx = Some(*pair_output_port_idx); - config.inputs[*pair_output_port_idx].in_place_pair_idx = Some(*input_port_idx); + config.outputs[*pair_output_port_idx].in_place_pair_idx = Some(*input_port_idx); } Some((output_stable_id, (pair_output_port_idx, output_pair_stable_id))) => { anyhow::bail!( diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 2b97ae4..17c2d0e 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -17,7 +17,9 @@ use parking_lot::Mutex; use rand::Rng; use rand_pcg::Pcg32; use std::ffi::c_void; +use std::fmt::Debug; use std::pin::Pin; +use std::ptr::null; use crate::plugin::ext::audio_ports::AudioPortConfig; use crate::util::check_null_ptr; @@ -56,11 +58,9 @@ pub struct ProcessConfig { pub time_sig_denominator: u16, } -/// Audio buffers for out-of-place processing. This wrapper allocates and sets up the channel -/// pointers. To avoid an unnecessary level of abstraction where the `Vec>`s need to be -/// converted to a slice of slices, this data structure borrows the vectors directly. -/// -#[derive(Clone)] +/// Audio buffers for audio processing. These contain both input and output buffers, that can be either in-place +/// or out-of-place, single or double precision. +#[derive(Clone, Debug)] pub struct AudioBuffers { // These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be // mutable because reborrwing them from here is the only way to modify them without @@ -78,7 +78,7 @@ pub struct AudioBuffers { num_samples: usize, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum AudioBuffer { Float32 { input: Option, @@ -86,6 +86,7 @@ pub enum AudioBuffer { data: Vec>, }, + #[allow(unused)] //TODO: use for future 64 bit processing tests Float64 { input: Option, output: Option, @@ -192,8 +193,9 @@ impl<'a> ProcessData<'a> { pub fn with_clap_process_data T>(&mut self, f: F) -> T { assert!( self.block_size as usize <= self.buffers.len(), - "internal error: invalid block size" - ); //TODO: should this be a result instead of a panic? + "Process block size is larger than the maximum allowed buffer size. This is a \ + clap-validator bug." + ); let (inputs, outputs) = self.buffers.clap_buffers(); @@ -249,117 +251,169 @@ impl<'a> ProcessData<'a> { } impl AudioBuffers { - /// Construct the out of place audio buffers. This allocates the channel pointers that are - /// handed to the plugin in the process function. - pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: usize) -> Result { - assert!(num_samples > 0); + /// Construct the audio buffers from the given buffer configurations. The number of samples must + /// be greater than zero and all channel vectors must have the same length. + pub fn new(buffers: Vec, num_samples: usize) -> Self { + assert!( + num_samples > 0, + "Number of samples must be greater than zero." + ); - let mut buffers = vec![]; let mut pointers = vec![]; let mut clap_inputs = vec![]; let mut clap_outputs = vec![]; - for (port, index) in config.inputs.iter().zip(0u32..) { - let bus_data: Vec> = - vec![vec![0.0f32; num_samples]; port.num_channels as usize]; - let bus_ptrs: Vec<*const ()> = bus_data + for buffer in buffers.iter() { + let pointer_list = match buffer { + AudioBuffer::Float32 { data, .. } => { + assert!( + data.iter().all(|x| x.len() == num_samples), + "Channel buffer length does not match" + ); + + data.iter() + .map(|x| x.as_ptr() as *const ()) + .collect::>() + } + AudioBuffer::Float64 { data, .. } => { + assert!( + data.iter().all(|x| x.len() == num_samples), + "Channel buffer length does not match" + ); + + data.iter() + .map(|x| x.as_ptr() as *const ()) + .collect::>() + } + }; + + if let Some(input) = buffer.input() { + if clap_inputs.len() <= input as usize { + clap_inputs.resize(input as usize + 1, None); + } + + clap_inputs[input as usize] = Some(clap_audio_buffer { + data32: if buffer.is_64bit() { + null() + } else { + pointer_list.as_ptr() as *const _ + }, + + data64: if buffer.is_64bit() { + pointer_list.as_ptr() as *const _ + } else { + null() + }, + + channel_count: pointer_list.len() as u32, + latency: 0, //TODO: do some interesting tests with these 2 fields + constant_mask: 0, + }); + } + + if let Some(output) = buffer.output() { + if clap_outputs.len() <= output as usize { + clap_outputs.resize(output as usize + 1, None); + } + + clap_outputs[output as usize] = Some(clap_audio_buffer { + data32: if buffer.is_64bit() { + null() + } else { + pointer_list.as_ptr() as *const _ + }, + + data64: if buffer.is_64bit() { + pointer_list.as_ptr() as *const _ + } else { + null() + }, + + channel_count: pointer_list.len() as u32, + latency: 0, //TODO: do some interesting tests with these 2 fields + constant_mask: 0, + }); + } + + pointers.push(pointer_list); + } + + Self { + buffers, + _pointers: pointers, + clap_inputs: clap_inputs + .into_iter() + .collect::>>() + .expect("Missing an input bus"), + clap_outputs: clap_outputs + .into_iter() + .collect::>>() + .expect("Missing an output bus"), + num_samples, + } + } + + /// Construct the out of place audio buffers. This allocates the channel pointers that are + /// handed to the plugin in the process function. + pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: usize) -> Self { + Self::new( + config + .inputs .iter() - .map(|channel| channel.as_ptr() as *const _) - .collect(); - - clap_inputs.push(clap_audio_buffer { - data32: bus_ptrs.as_ptr() as *const *const f32, - data64: std::ptr::null(), - channel_count: port.num_channels, - // TODO: Do some interesting tests with these two fields - latency: 0, - constant_mask: 0, - }); - buffers.push(AudioBuffer::Float32 { - input: Some(index), - output: None, - data: bus_data, - }); - pointers.push(bus_ptrs); + .zip(0u32..) + .map(|(port, index)| AudioBuffer::Float32 { + input: Some(index), + output: None, + data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], + }) + .chain(config.outputs.iter().zip(0u32..).map(|(port, index)| { + AudioBuffer::Float32 { + input: None, + output: Some(index), + data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], + } + })) + .collect(), + num_samples, + ) + } + + /// Construct the in place audio buffers. This allocates the channel pointers that are handed to + /// the plugin in the process function. + pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: usize) -> Self { + let mut buffers = vec![]; + + for (port, index) in config.inputs.iter().zip(0u32..) { + let in_place = port + .in_place_pair_idx + .filter(|output| config.outputs[*output].num_channels == port.num_channels) //TODO: is this guaranteed or do we have to handle a case with different in/out channel counts + .map(|output| output as u32); + + if in_place.is_none() { + buffers.push(AudioBuffer::Float32 { + input: Some(index), + output: None, + data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], + }); + } } for (port, index) in config.outputs.iter().zip(0u32..) { - let bus_data: Vec> = - vec![vec![0.0f32; num_samples]; port.num_channels as usize]; - let bus_ptrs: Vec<*const ()> = bus_data - .iter() - .map(|channel| channel.as_ptr() as *const _) - .collect(); - - clap_outputs.push(clap_audio_buffer { - data32: bus_ptrs.as_ptr() as *const *const f32, - data64: std::ptr::null(), - channel_count: port.num_channels, - // TODO: Do some interesting tests with these two fields - latency: 0, - constant_mask: 0, - }); + let in_place = port + .in_place_pair_idx + .filter(|input| config.inputs[*input].num_channels == port.num_channels) //TODO: is this guaranteed or do we have to handle a case with different in/out channel counts + .map(|input| input as u32); + buffers.push(AudioBuffer::Float32 { - input: None, + input: in_place, output: Some(index), - data: bus_data, + data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], }); - pointers.push(bus_ptrs); } - Ok(Self { - buffers, - _pointers: pointers, - clap_inputs, - clap_outputs, - num_samples, - }) + Self::new(buffers, num_samples) } - // pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: usize) -> Result { - // assert!(num_samples > 0); - - // let mut buffers = vec![]; - // let mut pointers = vec![]; - // let mut clap_inputs = vec![]; - // let mut clap_outputs = vec![]; - - // for (port, index) in config.inputs.iter().zip(0u32..) { - // let mut bus_data: Vec> = - // vec![vec![0.0f32; num_samples]; port.num_channels as usize]; - // let mut bus_ptrs: Vec<*const ()> = bus_data - // .iter() - // .map(|channel| channel.as_ptr() as *const _) - // .collect(); - // let bus_ptr = bus_ptrs.as_ptr(); - - // pointers.push(bus_ptrs); - // buffers.push(AudioBuffer::Float32 { - // input: Some(index), - // output: port.in_place_pair_idx.map(|x| x as u32), - // data: bus_data, - // }); - // clap_inputs.push(clap_audio_buffer { - // data32: bus_ptrs.as_ptr() as *const *const f32, - // data64: std::ptr::null(), - // channel_count: port.num_channels, - // // TODO: Do some interesting tests with these two fields - // latency: 0, - // constant_mask: 0, - // }); - // } - - // for (port, index) in config.outputs.iter().zip(0u32..) {} - - // Ok(Self { - // buffers, - // _pointers: pointers, - // clap_inputs, - // clap_outputs, - // num_samples, - // }) - // } - /// The number of samples in the buffer. pub fn len(&self) -> usize { self.num_samples @@ -371,10 +425,12 @@ impl AudioBuffers { (&self.clap_inputs, &mut self.clap_outputs) } + /// Pointers to the internal audio buffers pub fn buffers(&self) -> &[AudioBuffer] { &self.buffers } + /// Fill the single precision input and output buffers with arbitrary values. pub fn fill_f32(&mut self, mut next: impl FnMut() -> f32) { for bus in &mut self.buffers { match bus { @@ -390,6 +446,7 @@ impl AudioBuffers { } } + /// Fill the double precision input and output buffers with arbitrary values. pub fn fill_f64(&mut self, mut next: impl FnMut() -> f64) { for bus in &mut self.buffers { match bus { @@ -429,25 +486,45 @@ impl AudioBuffers { } impl AudioBuffer { - pub fn is_input(&self) -> bool { - matches!( - self, - AudioBuffer::Float32 { input: Some(_), .. } - | AudioBuffer::Float64 { input: Some(_), .. } - ) + /// Get the index of the input bus for this buffer. + pub fn input(&self) -> Option { + match self { + AudioBuffer::Float32 { input, .. } => *input, + AudioBuffer::Float64 { input, .. } => *input, + } } - pub fn is_output(&self) -> bool { - matches!( - self, - AudioBuffer::Float32 { - output: Some(_), - .. - } | AudioBuffer::Float64 { - output: Some(_), - .. - } - ) + /// Get the index of the output bus for this buffer. + pub fn output(&self) -> Option { + match self { + AudioBuffer::Float32 { output, .. } => *output, + AudioBuffer::Float64 { output, .. } => *output, + } + } + + /// Check whether this is a double precision buffer.. + pub fn is_64bit(&self) -> bool { + match self { + AudioBuffer::Float32 { .. } => false, + AudioBuffer::Float64 { .. } => true, + } + } +} + +impl Debug for AudioBuffer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Float32 { input, output, .. } => f + .debug_struct("Float32") + .field("input", input) + .field("output", output) + .finish_non_exhaustive(), + Self::Float64 { input, output, .. } => f + .debug_struct("Float64") + .field("input", input) + .field("output", output) + .finish_non_exhaustive(), + } } } @@ -478,10 +555,12 @@ impl EventQueue { queue } + /// Get the vtable pointer for input events. pub fn vtable_input(self: &Pin>) -> *const clap_input_events { &self.vtable_input } + /// Get the vtable pointer for output events. pub fn vtable_output(self: &Pin>) -> *const clap_output_events { &self.vtable_output } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index aafe528..adcedd7 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -25,6 +25,8 @@ pub enum PluginTestCase { FeaturesDuplicates, #[strum(serialize = "process-audio-out-of-place-basic")] ProcessAudioOutOfPlaceBasic, + #[strum(serialize = "process-audio-in-place-basic")] + ProcessAudioInPlaceBasic, #[strum(serialize = "process-note-out-of-place-basic")] ProcessNoteOutOfPlaceBasic, #[strum(serialize = "process-note-inconsistent")] @@ -75,6 +77,11 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses out-of-place audio processing.", ), + PluginTestCase::ProcessAudioInPlaceBasic => String::from( + "Processes random audio through the plugin with its default parameter values and \ + tests whether the output does not contain any non-finite or subnormal values. \ + Uses in-place audio processing for buses that support it.", + ), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default \ parameter values and tests the output for consistency. Uses out-of-place audio \ @@ -182,6 +189,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessAudioOutOfPlaceBasic => { processing::test_process_audio_out_of_place_basic(library, plugin_id) } + PluginTestCase::ProcessAudioInPlaceBasic => { + processing::test_process_audio_in_place_basic(library, plugin_id) + } PluginTestCase::ProcessNoteOutOfPlaceBasic => { processing::test_process_note_out_of_place_basic(library, plugin_id) } diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index f859867..430b406 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -237,7 +237,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result // to a file if the test fails let mut current_events: Option>; let mut previous_events: Option> = None; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); @@ -377,7 +377,7 @@ pub fn test_param_set_wrong_namespace( } } - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); ProcessingTest::new(&plugin, &mut audio_buffers).run_once(|process_data| { process_data.buffers.randomize(&mut prng); *process_data.input_events.events.lock() = random_param_set_events; diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 909f4b2..8f18b00 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -57,6 +57,14 @@ impl<'a> ProcessingTest<'a> { } } + /// Run the standard audio processing test for a still **deactivated** plugin. This calls the + /// process function `num_iters` times, and checks the output for consistency each time. + /// + /// The `Preprocess` closure is called before each processing cycle to allow the process data to be + /// modified for the next process cycle. + /// + /// Main-thread callbacks that were made to the plugin while the audio thread was active are + /// handled implicitly. pub fn run(self, num_iters: usize, mut preprocess: Preprocess) -> Result<()> where Preprocess: FnMut(&mut ProcessData) -> Result<()> + Send, @@ -147,6 +155,12 @@ impl<'a> ProcessingTest<'a> { Ok(()) } + /// Run the standard audio processing test for a still **deactivated** plugin. This is identical + /// to the [`run()`][Self::run()] function, except that it does exactly one processing cycle and + /// thus non-copy values can be moved into the closure. + /// + /// Main-thread callbacks that were made to the plugin while the audio thread was active are + /// handled implicitly. pub fn run_once(self, preprocess: Preprocess) -> Result<()> where Preprocess: FnOnce(&mut ProcessData) -> Result<()> + Send, @@ -186,7 +200,46 @@ pub fn test_process_audio_out_of_place_basic( } }; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); + ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + process_data.buffers.randomize(&mut prng); + Ok(()) + })?; + + // The `Host` contains built-in thread safety checks + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) +} + +/// The test for `ProcessingTest::ProcessAudioInPlaceBasic`. +pub fn test_process_audio_in_place_basic( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + + let mut audio_buffers = AudioBuffers::new_in_place_f32(&audio_ports_config, 512); ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { process_data.buffers.randomize(&mut prng); Ok(()) @@ -246,7 +299,7 @@ pub fn test_process_note_out_of_place_basic( // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(note_ports_config); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( @@ -309,7 +362,7 @@ pub fn test_process_note_inconsistent( // This RNG (Random Note Generator) allows generates mismatching events let mut note_event_rng = NoteGenerator::new(note_port_config).with_inconsistent_events(); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); // TODO: Use in-place processing for this test ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { @@ -361,7 +414,7 @@ pub fn test_process_varying_sample_rates( None => None, }; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); @@ -428,7 +481,7 @@ pub fn test_process_varying_block_sizes( for &buffer_size in BLOCK_SIZES { let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); let mut audio_buffers = - AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize)?; + AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); ProcessingTest::new(&plugin, &mut audio_buffers) .run(5, |process_data| { @@ -489,7 +542,7 @@ pub fn test_process_random_block_sizes( let mut note_event_rng = note_ports_config.map(NoteGenerator::new); let mut audio_buffers = - AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize)?; + AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); ProcessingTest::new(&plugin, &mut audio_buffers).run(20, |process_data| { process_data.block_size = if prng.gen_bool(0.8) { @@ -534,7 +587,7 @@ fn check_process_call_consistency( .zip(original_buffers.buffers()) { // Input buffers must not be overwritten during out of place processing - if buffer.is_input() && !buffer.is_output() { + if buffer.input().is_some() && buffer.output().is_none() { let matches = match (buffer, before) { ( AudioBuffer::Float32 { data: after, .. }, @@ -557,10 +610,9 @@ fn check_process_call_consistency( } // Output buffers must not contain any non-finite or denormal values - if buffer.is_output() { + if let Some(port_idx) = buffer.output() { match buffer { - AudioBuffer::Float32 { data, output, .. } => { - let port_idx = output.unwrap(); + AudioBuffer::Float32 { data, .. } => { for (channel_idx, channel) in data.iter().enumerate() { for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { if !sample.is_finite() { @@ -579,8 +631,7 @@ fn check_process_call_consistency( } } - AudioBuffer::Float64 { data, output, .. } => { - let port_idx = output.unwrap(); + AudioBuffer::Float64 { data, .. } => { for (channel_idx, channel) in data.iter().enumerate() { for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { if !sample.is_finite() { @@ -613,14 +664,14 @@ fn check_process_call_consistency( ) } - last_event_time = event_time; - } + if event_time >= block_size as u32 { + anyhow::bail!( + "The plugin output an event for sample {event_time} but the audio buffer only \ + contains {block_size} samples." + ) + } - if last_event_time >= block_size as u32 { - anyhow::bail!( - "The plugin output an event for sample {last_event_time} but the audio buffer only \ - contains {block_size} samples." - ) + last_event_time = event_time; } Ok(()) diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index c050f05..7da5013 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -136,7 +136,7 @@ pub fn test_state_reproducibility_null_cookies( } } - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { *process_data.input_events.events.lock() = random_param_set_events; Ok(()) @@ -398,7 +398,7 @@ pub fn test_state_reproducibility_flush( } // In the previous pass we used flush, and here we use the process funciton - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { *process_data.input_events.events.lock() = new_random_param_set_events; Ok(()) @@ -496,7 +496,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { *process_data.input_events.events.lock() = random_param_set_events; Ok(()) diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 8ac842d..3b8425e 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -143,7 +143,7 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result .context("Could not fetch the plugin's audio port config")? .unwrap_or_default(); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512)?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for LoadablePreset { location, From a56cd1c845805bf88f0bb99e46e592e34b0f614b Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 17 Dec 2025 00:58:27 +0400 Subject: [PATCH 005/114] add methods-non-null test that checks for a common? mistake of exporting null methods in clap_plugin/extensions --- src/tests/plugin.rs | 7 +++ src/tests/plugin/descriptor.rs | 107 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index adcedd7..207a151 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -19,6 +19,8 @@ pub use processing::ProcessingTest; pub enum PluginTestCase { #[strum(serialize = "descriptor-consistency")] DescriptorConsistency, + #[strum(serialize = "methods-non-null")] + MethodsNonNull, #[strum(serialize = "features-categories")] FeaturesCategories, #[strum(serialize = "features-duplicates")] @@ -62,6 +64,10 @@ impl<'a> TestCase<'a> for PluginTestCase { fn description(&self) -> String { match self { + PluginTestCase::MethodsNonNull => String::from( + "Asserts that all methods of the 'clap_plugin' object and some known extension \ + are non-null.", + ), PluginTestCase::DescriptorConsistency => String::from( "The plugin descriptor returned from the plugin factory and the plugin descriptor \ stored on the 'clap_plugin object should be equivalent.", @@ -177,6 +183,7 @@ impl<'a> TestCase<'a> for PluginTestCase { fn run_in_process(&self, (library, plugin_id): Self::TestArgs) -> TestResult { let status = match self { + PluginTestCase::MethodsNonNull => descriptor::test_methods_non_null(library, plugin_id), PluginTestCase::DescriptorConsistency => { descriptor::test_consistency(library, plugin_id) } diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 3153a3a..6830a5a 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -1,15 +1,34 @@ //! Tests surrounding plugin features. use anyhow::{Context, Result}; +use clap_sys::ext::audio_ports::{clap_plugin_audio_ports, CLAP_EXT_AUDIO_PORTS}; +use clap_sys::ext::audio_ports_config::{ + clap_plugin_audio_ports_config, CLAP_EXT_AUDIO_PORTS_CONFIG, +}; +use clap_sys::ext::gui::{clap_plugin_gui, CLAP_EXT_GUI}; +use clap_sys::ext::latency::{clap_plugin_latency, CLAP_EXT_LATENCY}; +use clap_sys::ext::note_name::{clap_plugin_note_name, CLAP_EXT_NOTE_NAME}; +use clap_sys::ext::note_ports::{clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS}; +use clap_sys::ext::params::{clap_plugin_params, CLAP_EXT_PARAMS}; +use clap_sys::ext::posix_fd_support::{clap_plugin_posix_fd_support, CLAP_EXT_POSIX_FD_SUPPORT}; +use clap_sys::ext::render::{clap_plugin_render, CLAP_EXT_RENDER}; +use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE}; +use clap_sys::ext::tail::{clap_plugin_tail, CLAP_EXT_TAIL}; +use clap_sys::ext::thread_pool::{clap_plugin_thread_pool, CLAP_EXT_THREAD_POOL}; +use clap_sys::ext::timer_support::{clap_plugin_timer_support, CLAP_EXT_TIMER_SUPPORT}; +use clap_sys::ext::voice_info::{clap_plugin_voice_info, CLAP_EXT_VOICE_INFO}; use clap_sys::plugin_features::{ CLAP_PLUGIN_FEATURE_ANALYZER, CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, CLAP_PLUGIN_FEATURE_INSTRUMENT, CLAP_PLUGIN_FEATURE_NOTE_DETECTOR, CLAP_PLUGIN_FEATURE_NOTE_EFFECT, }; use std::collections::HashSet; +use std::ffi::CStr; use crate::plugin::host::Host; +use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::TestStatus; +use crate::util::unsafe_clap_call; /// Verifies that the descriptor stored in the factory and the descriptor stored on the plugin /// object are equivalent. @@ -44,6 +63,94 @@ pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result Result { + unsafe fn check_extension(plugin: &Plugin<'_>, extension: &CStr) -> Result<()> { + let extension_ptr = unsafe_clap_call! { plugin.as_ptr()=>get_extension(plugin.as_ptr(), extension.as_ptr()) }; + if extension_ptr.is_null() { + return Ok(()); + } + + let methods = std::slice::from_raw_parts( + extension_ptr as *const *const (), + std::mem::size_of::() / std::mem::size_of::<*const ()>(), + ); + + for &method in methods.iter() { + if method.is_null() { + anyhow::bail!( + "Extension '{}' has a method that is null.", + extension.to_string_lossy() + ); + } + } + + Ok(()) + } + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host) + .context("Could not create the plugin instance")?; + + // Check `clap_plugin` methods. + // SAFETY: `plugin.as_ptr()` is guaranteed to be a valid pointer as long as `plugin` is alive. + unsafe { + let plugin = plugin.as_ptr(); + + anyhow::ensure!((*plugin).init.is_some(), "clap_plugin::init is null"); + anyhow::ensure!((*plugin).destroy.is_some(), "clap_plugin::destroy is null"); + anyhow::ensure!((*plugin).process.is_some(), "clap_plugin::process is null"); + anyhow::ensure!((*plugin).reset.is_some(), "clap_plugin::reset is null"); + anyhow::ensure!( + (*plugin).get_extension.is_some(), + "clap_plugin::get_extension is null" + ); + anyhow::ensure!( + (*plugin).on_main_thread.is_some(), + "clap_plugin::on_main_thread is null" + ); + anyhow::ensure!( + (*plugin).activate.is_some(), + "clap_plugin::activate is null" + ); + anyhow::ensure!( + (*plugin).deactivate.is_some(), + "clap_plugin::deactivate is null" + ); + anyhow::ensure!( + (*plugin).start_processing.is_some(), + "clap_plugin::start_processing is null" + ); + anyhow::ensure!( + (*plugin).stop_processing.is_some(), + "clap_plugin::stop_processing is null" + ); + } + + plugin.init().context("Error during initialization")?; + + // Check known extensions. + unsafe { + check_extension::(&plugin, CLAP_EXT_AUDIO_PORTS)?; + check_extension::(&plugin, CLAP_EXT_AUDIO_PORTS_CONFIG)?; + check_extension::(&plugin, CLAP_EXT_GUI)?; + check_extension::(&plugin, CLAP_EXT_NOTE_NAME)?; + check_extension::(&plugin, CLAP_EXT_NOTE_PORTS)?; + check_extension::(&plugin, CLAP_EXT_PARAMS)?; + check_extension::(&plugin, CLAP_EXT_STATE)?; + check_extension::(&plugin, CLAP_EXT_LATENCY)?; + check_extension::(&plugin, CLAP_EXT_TAIL)?; + check_extension::(&plugin, CLAP_EXT_POSIX_FD_SUPPORT)?; + check_extension::(&plugin, CLAP_EXT_TIMER_SUPPORT)?; + check_extension::(&plugin, CLAP_EXT_THREAD_POOL)?; + check_extension::(&plugin, CLAP_EXT_RENDER)?; + check_extension::(&plugin, CLAP_EXT_VOICE_INFO)?; + } + + Ok(TestStatus::Success { details: None }) +} + /// Check whether the plugin's categories are consistent. Currently this just makes sure that the /// plugin has one of the four main plugin category features. pub fn test_features_categories(library: &PluginLibrary, plugin_id: &str) -> Result { From 001a7dcffa2237794f1b72d2d5381b41380d1519 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 17 Dec 2025 05:13:44 +0400 Subject: [PATCH 006/114] detect when output is left uninitialized --- src/plugin/ext/note_ports.rs | 4 +- src/plugin/ext/params.rs | 6 +- src/plugin/instance/process.rs | 152 ++++++++++++++++++++++----------- src/tests/plugin/processing.rs | 71 +++++++++++---- 4 files changed, 164 insertions(+), 69 deletions(-) diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 214fa66..181c20e 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap_sys::ext::note_ports::{ - clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS, + CLAP_EXT_NOTE_PORTS, clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, }; use std::collections::HashSet; use std::ffi::CStr; @@ -112,7 +112,7 @@ impl NotePorts<'_> { for i in 0..num_outputs { let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { note_ports=>get(plugin, i, true, &mut info) }; + let success = unsafe_clap_call! { note_ports=>get(plugin, i, false, &mut info) }; if !success { anyhow::bail!( "Plugin returned an error when querying output note port {i} ({num_outputs} \ diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index cf825c2..42769c5 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -2,18 +2,18 @@ use anyhow::{Context, Result}; use clap_sys::ext::params::{ - clap_param_info, clap_param_info_flags, clap_plugin_params, CLAP_EXT_PARAMS, - CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, + CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, CLAP_PARAM_IS_AUTOMATABLE_PER_KEY, CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID, CLAP_PARAM_IS_AUTOMATABLE_PER_PORT, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_HIDDEN, CLAP_PARAM_IS_MODULATABLE, CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL, CLAP_PARAM_IS_MODULATABLE_PER_KEY, CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID, CLAP_PARAM_IS_MODULATABLE_PER_PORT, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, + clap_param_info, clap_param_info_flags, clap_plugin_params, }; use clap_sys::id::clap_id; use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::collections::BTreeMap; -use std::ffi::{c_void, CStr, CString}; +use std::ffi::{CStr, CString, c_void}; use std::ops::RangeInclusive; use std::pin::Pin; use std::ptr::NonNull; diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 17c2d0e..466a635 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -81,19 +81,38 @@ pub struct AudioBuffers { #[derive(Clone)] pub enum AudioBuffer { Float32 { - input: Option, - output: Option, + input: Option, + output: Option, data: Vec>, }, #[allow(unused)] //TODO: use for future 64 bit processing tests Float64 { - input: Option, - output: Option, + input: Option, + output: Option, data: Vec>, }, } +pub trait AudioBufferFill { + fn fill_input_f32(&mut self, bus: usize, channel: usize, slice: &mut [f32]); + fn fill_input_f64(&mut self, bus: usize, channel: usize, slice: &mut [f64]); + fn fill_output_f32(&mut self, bus: usize, channel: usize, slice: &mut [f32]) { + self.fill_input_f32(bus, channel, slice); + } + fn fill_output_f64(&mut self, bus: usize, channel: usize, slice: &mut [f64]) { + self.fill_input_f64(bus, channel, slice); + } + fn fill_inplace_f32(&mut self, input: usize, output: usize, channel: usize, slice: &mut [f32]) { + let _ = output; + self.fill_input_f32(input, channel, slice); + } + fn fill_inplace_f64(&mut self, input: usize, output: usize, channel: usize, slice: &mut [f64]) { + let _ = output; + self.fill_input_f64(input, channel, slice); + } +} + // SAFETY: Sharing these pointers with other threads is safe as they refer to the borrowed input and // output slices. The pointers thus cannot be invalidated. unsafe impl Send for AudioBuffers {} @@ -360,13 +379,13 @@ impl AudioBuffers { config .inputs .iter() - .zip(0u32..) - .map(|(port, index)| AudioBuffer::Float32 { + .enumerate() + .map(|(index, port)| AudioBuffer::Float32 { input: Some(index), output: None, data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], }) - .chain(config.outputs.iter().zip(0u32..).map(|(port, index)| { + .chain(config.outputs.iter().enumerate().map(|(index, port)| { AudioBuffer::Float32 { input: None, output: Some(index), @@ -383,11 +402,10 @@ impl AudioBuffers { pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: usize) -> Self { let mut buffers = vec![]; - for (port, index) in config.inputs.iter().zip(0u32..) { + for (index, port) in config.inputs.iter().enumerate() { let in_place = port .in_place_pair_idx - .filter(|output| config.outputs[*output].num_channels == port.num_channels) //TODO: is this guaranteed or do we have to handle a case with different in/out channel counts - .map(|output| output as u32); + .filter(|output| config.outputs[*output].num_channels == port.num_channels); if in_place.is_none() { buffers.push(AudioBuffer::Float32 { @@ -398,11 +416,10 @@ impl AudioBuffers { } } - for (port, index) in config.outputs.iter().zip(0u32..) { + for (index, port) in config.outputs.iter().enumerate() { let in_place = port .in_place_pair_idx - .filter(|input| config.inputs[*input].num_channels == port.num_channels) //TODO: is this guaranteed or do we have to handle a case with different in/out channel counts - .map(|input| input as u32); + .filter(|input| config.inputs[*input].num_channels == port.num_channels); buffers.push(AudioBuffer::Float32 { input: in_place, @@ -430,34 +447,50 @@ impl AudioBuffers { &self.buffers } - /// Fill the single precision input and output buffers with arbitrary values. - pub fn fill_f32(&mut self, mut next: impl FnMut() -> f32) { + /// Fill the input and output buffers with arbitrary values. + pub fn fill(&mut self, mut fill: impl AudioBufferFill) { for bus in &mut self.buffers { match bus { - AudioBuffer::Float32 { data, .. } => { - for channel in data { - for sample in channel { - *sample = next(); + AudioBuffer::Float32 { + input, + output, + data, + } => { + for (channel_idx, channel) in data.iter_mut().enumerate() { + match (*input, *output) { + (Some(input), Some(output)) => { + fill.fill_inplace_f32(input, output, channel_idx, channel); + } + (Some(input), None) => { + fill.fill_input_f32(input, channel_idx, channel); + } + (None, Some(output)) => { + fill.fill_output_f32(output, channel_idx, channel); + } + (None, None) => {} } } } - _ => {} - } - } - } - - /// Fill the double precision input and output buffers with arbitrary values. - pub fn fill_f64(&mut self, mut next: impl FnMut() -> f64) { - for bus in &mut self.buffers { - match bus { - AudioBuffer::Float64 { data, .. } => { - for channel in data { - for sample in channel { - *sample = next(); + AudioBuffer::Float64 { + input, + output, + data, + } => { + for (channel_idx, channel) in data.iter_mut().enumerate() { + match (*input, *output) { + (Some(input), Some(output)) => { + fill.fill_inplace_f64(input, output, channel_idx, channel); + } + (Some(input), None) => { + fill.fill_input_f64(input, channel_idx, channel); + } + (None, Some(output)) => { + fill.fill_output_f64(output, channel_idx, channel); + } + (None, None) => {} } } } - _ => {} } } } @@ -465,29 +498,50 @@ impl AudioBuffers { /// Fill the input and output buffers with white noise. The values are distributed between `[-1, /// 1]`, and denormals are snapped to zero. pub fn randomize(&mut self, prng: &mut Pcg32) { - self.fill_f32(|| { - let y = prng.gen_range(-1.0..=1.0f32); - if y.is_subnormal() { - 0.0 - } else { - y + struct Randomizer<'a>(&'a mut Pcg32); + + impl AudioBufferFill for Randomizer<'_> { + fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { + for sample in slice.iter_mut() { + let y = self.0.gen_range(-1.0..=1.0f32); + *sample = if y.is_subnormal() { 0.0 } else { y }; + } } - }); - self.fill_f64(|| { - let y = prng.gen_range(-1.0..=1.0f64); - if y.is_subnormal() { - 0.0 - } else { - y + fn fill_input_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { + for sample in slice.iter_mut() { + let y = self.0.gen_range(-1.0..=1.0f64); + *sample = if y.is_subnormal() { 0.0 } else { y }; + } } - }); + + // fill with random NaN values so we can detect if a plugin left the output uninitialized + fn fill_output_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { + for sample in slice.iter_mut() { + let y: u32 = self.0.gen(); + let y = f32::from_bits(y | 0x7F800001); + assert!(y.is_nan()); + *sample = y; + } + } + + fn fill_output_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { + for sample in slice.iter_mut() { + let y: u64 = self.0.gen(); + let y = f64::from_bits(y | 0x7FF0000000000001); + assert!(y.is_nan()); + *sample = y; + } + } + } + + self.fill(Randomizer(prng)); } } impl AudioBuffer { /// Get the index of the input bus for this buffer. - pub fn input(&self) -> Option { + pub fn input(&self) -> Option { match self { AudioBuffer::Float32 { input, .. } => *input, AudioBuffer::Float64 { input, .. } => *input, @@ -495,7 +549,7 @@ impl AudioBuffer { } /// Get the index of the output bus for this buffer. - pub fn output(&self) -> Option { + pub fn output(&self) -> Option { match self { AudioBuffer::Float32 { output, .. } => *output, AudioBuffer::Float64 { output, .. } => *output, diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 8f18b00..7aa2d70 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -173,7 +173,7 @@ impl<'a> ProcessingTest<'a> { } } -/// The test for `ProcessingTest::ProcessAudioOutOfPlaceBasic`. +/// The test for `PluginTestCase::ProcessAudioOutOfPlaceBasic`. pub fn test_process_audio_out_of_place_basic( library: &PluginLibrary, plugin_id: &str, @@ -196,7 +196,7 @@ pub fn test_process_audio_out_of_place_basic( "The plugin does not implement the '{}' extension.", AudioPorts::EXTENSION_ID.to_str().unwrap(), )), - }) + }); } }; @@ -212,7 +212,7 @@ pub fn test_process_audio_out_of_place_basic( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessAudioInPlaceBasic`. +/// The test for `PluginTestCase::ProcessAudioInPlaceBasic`. pub fn test_process_audio_in_place_basic( library: &PluginLibrary, plugin_id: &str, @@ -235,7 +235,7 @@ pub fn test_process_audio_in_place_basic( "The plugin does not implement the '{}' extension.", AudioPorts::EXTENSION_ID.to_str().unwrap(), )), - }) + }); } }; @@ -251,7 +251,7 @@ pub fn test_process_audio_in_place_basic( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessNoteOutOfPlaceBasic`. This test is very similar to +/// The test for `PluginTestCase::ProcessNoteOutOfPlaceBasic`. This test is very similar to /// `ProcessAudioOutOfPlaceBasic`, but it requires the `note-ports` extension, sends notes and/or /// MIDI to the plugin, and doesn't require the `audio-ports` extension. pub fn test_process_note_out_of_place_basic( @@ -283,7 +283,7 @@ pub fn test_process_note_out_of_place_basic( "The plugin does not implement the '{}' extension.", NotePorts::EXTENSION_ID.to_str().unwrap(), )), - }) + }); } }; if note_ports_config.inputs.is_empty() { @@ -315,7 +315,7 @@ pub fn test_process_note_out_of_place_basic( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessNoteInconsistent`. This is the same test as +/// The test for `PluginTestCase::ProcessNoteInconsistent`. This is the same test as /// `ProcessAudioOutOfPlaceBasic`, but without requiring matched note on/off pairs and similar /// invariants pub fn test_process_note_inconsistent( @@ -346,7 +346,7 @@ pub fn test_process_note_inconsistent( "The plugin does not implement the '{}' extension.", NotePorts::EXTENSION_ID.to_str().unwrap(), )), - }) + }); } }; if note_port_config.inputs.is_empty() { @@ -380,7 +380,7 @@ pub fn test_process_note_inconsistent( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessVaryingSampleRates`. +/// The test for `PluginTestCase::ProcessVaryingSampleRates`. pub fn test_process_varying_sample_rates( library: &PluginLibrary, plugin_id: &str, @@ -445,13 +445,13 @@ pub fn test_process_varying_sample_rates( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessVaryingBlockSizes`. +/// The test for `PluginTestCase::ProcessVaryingBlockSizes`. pub fn test_process_varying_block_sizes( library: &PluginLibrary, plugin_id: &str, ) -> Result { const BLOCK_SIZES: &[u32] = &[ - 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 1536, 10, 1000, 10000, 2027, + 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 1536, 10, 17, 1000, 10000, 2027, ]; let mut prng = new_prng(); @@ -509,7 +509,7 @@ pub fn test_process_varying_block_sizes( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ProcessRandomBlockSizes`. +/// The test for `PluginTestCase::ProcessRandomBlockSizes`. pub fn test_process_random_block_sizes( library: &PluginLibrary, plugin_id: &str, @@ -586,8 +586,8 @@ fn check_process_call_consistency( .iter() .zip(original_buffers.buffers()) { - // Input buffers must not be overwritten during out of place processing - if buffer.input().is_some() && buffer.output().is_none() { + // Input-only buffers must not be overwritten during out of place processing + if let (Some(index), None) = (buffer.input(), buffer.output()) { let matches = match (buffer, before) { ( AudioBuffer::Float32 { data: after, .. }, @@ -604,7 +604,48 @@ fn check_process_call_consistency( if !matches { anyhow::bail!( - "The plugin has overwritten the input buffers during out-of-place processing." + "The plugin has overwritten an input buffer (index {index}) during \ + out-of-place processing." + ); + } + } + + // Output-only buffers must not be left "untouched" during out of place processing + if let (Some(index), None) = (buffer.output(), buffer.input()) { + let matches = match (buffer, before) { + ( + AudioBuffer::Float32 { data: after, .. }, + AudioBuffer::Float32 { data: before, .. }, + ) => after.iter().zip(before.iter()).all(|(after, before)| { + after + .iter() + .zip(before.iter()) + .take(block_size) + .all(|(after, before)| { + after.is_nan() && (after.to_bits() == before.to_bits()) + }) + }), + + ( + AudioBuffer::Float64 { data: after, .. }, + AudioBuffer::Float64 { data: before, .. }, + ) => after.iter().zip(before.iter()).all(|(after, before)| { + after + .iter() + .zip(before.iter()) + .take(block_size) + .all(|(after, before)| { + after.is_nan() && (after.to_bits() == before.to_bits()) + }) + }), + + _ => unreachable!(), + }; + + if matches { + anyhow::bail!( + "The plugin has left an output buffer (index {index}) untouched during \ + out-of-place processing." ); } } From fd98238e1df9ec68e0af3a822c9f96b09be990b0 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 17 Dec 2025 21:01:35 +0400 Subject: [PATCH 007/114] refactor `ProcessingTest` and `AudioBuffers` further; add `process-audio-constant-mask` test --- Cargo.lock | 1 + Cargo.toml | 1 + src/plugin/instance.rs | 1 + src/plugin/instance/process.rs | 91 +++++++- src/tests/plugin.rs | 11 + src/tests/plugin/params.rs | 2 +- src/tests/plugin/processing.rs | 370 ++++++++++++++++++++------------- 7 files changed, 328 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3aa7d3..c300581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,7 @@ dependencies = [ "colored", "core-foundation", "crossbeam", + "either", "libloading", "log", "log-panics", diff --git a/Cargo.toml b/Cargo.toml index 45ce2c7..49a4209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/free-audio/clap-validator" [dependencies] anyhow = "1.0.58" +either = "1.9.0" chrono = { version = "0.4.23", features = ["serde"] } # All the claps! clap = { version = "4.1.8", features = ["derive", "wrap_help"] } diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 466d169..5cd7749 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -265,6 +265,7 @@ impl<'lib> Plugin<'lib> { // Apparently 0 is invalid here assert!(min_buffer_size >= 1); + assert!(max_buffer_size >= min_buffer_size); let plugin = self.as_ptr(); if unsafe_clap_call! { diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 466a635..4028cd6 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -13,6 +13,7 @@ use clap_sys::events::{ }; use clap_sys::fixedpoint::{CLAP_BEATTIME_FACTOR, CLAP_SECTIME_FACTOR}; use clap_sys::process::clap_process; +use either::Either; use parking_lot::Mutex; use rand::Rng; use rand_pcg::Pcg32; @@ -495,12 +496,12 @@ impl AudioBuffers { } } - /// Fill the input and output buffers with white noise. The values are distributed between `[-1, - /// 1]`, and denormals are snapped to zero. + /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). + /// Output buffers are filled with random NaN values to detect if they have been written to. pub fn randomize(&mut self, prng: &mut Pcg32) { - struct Randomizer<'a>(&'a mut Pcg32); + struct Randomize<'a>(&'a mut Pcg32); - impl AudioBufferFill for Randomizer<'_> { + impl AudioBufferFill for Randomize<'_> { fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { for sample in slice.iter_mut() { let y = self.0.gen_range(-1.0..=1.0f32); @@ -535,7 +536,34 @@ impl AudioBuffers { } } - self.fill(Randomizer(prng)); + self.fill(Randomize(prng)); + } + + pub fn silence_all_inputs(&mut self) { + struct Silence; + + impl AudioBufferFill for Silence { + fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { + slice.fill(0.0); + } + + fn fill_input_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { + slice.fill(0.0); + } + + fn fill_output_f32(&mut self, _bus: usize, _channel: usize, _slice: &mut [f32]) {} + fn fill_output_f64(&mut self, _bus: usize, _channel: usize, _slice: &mut [f64]) {} + } + + self.fill(Silence); + + for input in &mut self.clap_inputs { + input.constant_mask = 1u64.unbounded_shl(input.channel_count).wrapping_sub(1); + } + } + + pub fn output_constant_mask(&self, bus: usize) -> u64 { + self.clap_outputs[bus].constant_mask } } @@ -556,13 +584,64 @@ impl AudioBuffer { } } - /// Check whether this is a double precision buffer.. + /// Check whether this is a double precision buffer. pub fn is_64bit(&self) -> bool { match self { AudioBuffer::Float32 { .. } => false, AudioBuffer::Float64 { .. } => true, } } + + pub fn is_same(&self, other: &Self) -> bool { + match (self, other) { + (AudioBuffer::Float32 { data: this, .. }, AudioBuffer::Float32 { data: other, .. }) => { + for (this, other) in this.iter().zip(other.iter()) { + for (this, other) in this.iter().zip(other.iter()) { + if this.to_bits() != other.to_bits() { + return false; + } + } + } + + true + } + + (AudioBuffer::Float64 { data: this, .. }, AudioBuffer::Float64 { data: other, .. }) => { + for (this, other) in this.iter().zip(other.iter()) { + for (this, other) in this.iter().zip(other.iter()) { + if this.to_bits() != other.to_bits() { + return false; + } + } + } + + true + } + + _ => false, + } + } + + pub fn len(&self) -> usize { + match self { + AudioBuffer::Float32 { data, .. } => data.first().map_or(0, |x| x.len()), + AudioBuffer::Float64 { data, .. } => data.first().map_or(0, |x| x.len()), + } + } + + pub fn channels(&self) -> usize { + match self { + AudioBuffer::Float32 { data, .. } => data.len(), + AudioBuffer::Float64 { data, .. } => data.len(), + } + } + + pub fn get(&self, channel: usize, sample: usize) -> Either { + match self { + AudioBuffer::Float32 { data, .. } => Either::Right(data[channel][sample]), + AudioBuffer::Float64 { data, .. } => Either::Left(data[channel][sample]), + } + } } impl Debug for AudioBuffer { diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 207a151..8f741ed 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -29,6 +29,8 @@ pub enum PluginTestCase { ProcessAudioOutOfPlaceBasic, #[strum(serialize = "process-audio-in-place-basic")] ProcessAudioInPlaceBasic, + #[strum(serialize = "process-audio-constant-mask")] + ProcessAudioConstantMask, #[strum(serialize = "process-note-out-of-place-basic")] ProcessNoteOutOfPlaceBasic, #[strum(serialize = "process-note-inconsistent")] @@ -88,6 +90,12 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses in-place audio processing for buses that support it.", ), + PluginTestCase::ProcessAudioConstantMask => String::from( + "Processes random audio through the plugin with its default parameter values \ + while setting the constant mask on silent blocks, and tests whether the output \ + does not contain any non-finite or subnormal values and that the plugin sets the \ + constant mask correctly. Uses out-of-place audio processing.", + ), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default \ parameter values and tests the output for consistency. Uses out-of-place audio \ @@ -199,6 +207,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessAudioInPlaceBasic => { processing::test_process_audio_in_place_basic(library, plugin_id) } + PluginTestCase::ProcessAudioConstantMask => { + processing::test_process_audio_constant_mask(library, plugin_id) + } PluginTestCase::ProcessNoteOutOfPlaceBasic => { processing::test_process_note_out_of_place_basic(library, plugin_id) } diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 430b406..6b775ca 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -243,7 +243,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); let mut have_set_parameters = false; - let run_result = ProcessingTest::new(&plugin, &mut audio_buffers).run( + let run_result = ProcessingTest::new(&plugin, &mut audio_buffers).run_simple( FUZZ_RUNS_PER_PERMUTATION, |process_data| { if !have_set_parameters { diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 7aa2d70..0267e1d 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -4,7 +4,7 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{AudioBuffer, AudioBuffers, ProcessConfig, ProcessData}; +use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::rng::{new_prng, NoteGenerator}; @@ -26,7 +26,18 @@ pub struct ProcessingTest<'a> { plugin: &'a Plugin<'a>, buffers: &'a mut AudioBuffers, config: ProcessConfig, - check_denormals: bool, +} + +pub enum ProcessingCallback<'a, 'b> { + Prepare { + process_data: &'a mut ProcessData<'b>, + iteration: usize, + }, + + Validate { + process_data: &'a ProcessData<'b>, + iteration: usize, + }, } impl<'a> ProcessingTest<'a> { @@ -35,15 +46,6 @@ impl<'a> ProcessingTest<'a> { plugin, buffers, config: ProcessConfig::default(), - check_denormals: true, - } - } - - #[allow(unused)] //TODO: use this for future denormal tests - pub fn allow_denormals(self) -> Self { - Self { - check_denormals: false, - ..self } } @@ -57,17 +59,9 @@ impl<'a> ProcessingTest<'a> { } } - /// Run the standard audio processing test for a still **deactivated** plugin. This calls the - /// process function `num_iters` times, and checks the output for consistency each time. - /// - /// The `Preprocess` closure is called before each processing cycle to allow the process data to be - /// modified for the next process cycle. - /// - /// Main-thread callbacks that were made to the plugin while the audio thread was active are - /// handled implicitly. - pub fn run(self, num_iters: usize, mut preprocess: Preprocess) -> Result<()> + pub fn run(self, mut callback: Callback) -> Result<()> where - Preprocess: FnMut(&mut ProcessData) -> Result<()> + Send, + Callback: FnMut(ProcessingCallback) -> Result + Send, { // Handle callbacks the plugin may have made during init or these queries. The // `ProcessingTest::run*` functions will implicitly handle all outstanding callbacks before they @@ -86,7 +80,9 @@ impl<'a> ProcessingTest<'a> { // stopped, deactivated, reactivated, and started again. Because of that, we need to keep // track of the number of processed iterations manually instead of using a for loop. let mut iters_done = 0; - while iters_done < num_iters { + let mut running = true; + + while running { self.plugin .activate(self.config.sample_rate, 1, buffer_size)?; @@ -95,36 +91,28 @@ impl<'a> ProcessingTest<'a> { // This test can be repeated a couple of times // NOTE: We intentionally do not disable denormals here - 'processing: while iters_done < num_iters { - iters_done += 1; - - preprocess(&mut process_data)?; - - // We'll check that the plugin hasn't modified the input buffers after the - // test - let original_buffers = process_data.buffers.clone(); - - plugin - .process(&mut process_data) - .context("Error during audio processing")?; - - check_process_call_consistency( - &process_data, - original_buffers, - self.check_denormals, - ) - .with_context(|| { - format!( - "Failed during processing cycle {} out of {}", - iters_done + 1, - num_iters - ) + 'processing: while running { + running &= callback(ProcessingCallback::Prepare { + process_data: &mut process_data, + iteration: iters_done, + }) + .with_context(|| format!("Failed to prepare cycle #{}", iters_done + 1))?; + + plugin.process(&mut process_data).with_context(|| { + format!("Error during processing cycle #{}", iters_done + 1) })?; + running &= callback(ProcessingCallback::Validate { + process_data: &process_data, + iteration: iters_done, + }) + .with_context(|| format!("Failed to validate cycle #{}", iters_done + 1))?; + process_data.clear_events(); process_data.advance_transport(process_data.block_size); + iters_done += 1; - // Restart processing as necesasry + // Restart processing as necessary if plugin .state() .requested_restart @@ -132,10 +120,9 @@ impl<'a> ProcessingTest<'a> { .is_ok() { log::trace!( - "Restarting the plugin during processing cycle {} out of {} after a \ - call to 'clap_host::request_restart()'", - iters_done + 1, - num_iters + "Restarting the plugin during processing cycle #{} after a call to \ + 'clap_host::request_restart()'", + iters_done, ); break 'processing; } @@ -155,6 +142,35 @@ impl<'a> ProcessingTest<'a> { Ok(()) } + /// Run the standard audio processing test for a still **deactivated** plugin. This calls the + /// process function `num_iters` times, and checks the output for consistency each time. + /// + /// The `Preprocess` closure is called before each processing cycle to allow the process data to be + /// modified for the next process cycle. + /// + /// Main-thread callbacks that were made to the plugin while the audio thread was active are + /// handled implicitly. + pub fn run_simple(self, num_iters: usize, mut preprocess: Callback) -> Result<()> + where + Callback: FnMut(&mut ProcessData) -> Result<()> + Send, + { + let mut original_buffers = self.buffers.clone(); + self.run(|callback| match callback { + ProcessingCallback::Prepare { process_data, .. } => { + preprocess(process_data)?; + original_buffers.clone_from(&process_data.buffers); + Ok(true) + } + ProcessingCallback::Validate { + process_data, + iteration, + } => { + check_process_call_consistency(process_data, &original_buffers, true)?; + Ok(iteration < num_iters) + } + }) + } + /// Run the standard audio processing test for a still **deactivated** plugin. This is identical /// to the [`run()`][Self::run()] function, except that it does exactly one processing cycle and /// thus non-copy values can be moved into the closure. @@ -166,9 +182,9 @@ impl<'a> ProcessingTest<'a> { Preprocess: FnOnce(&mut ProcessData) -> Result<()> + Send, { let mut preprocess = Some(preprocess); - self.run(1, |data| match preprocess.take() { + self.run_simple(1, |data| match preprocess.take() { Some(preprocess) => preprocess(data), - None => unreachable!(), + None => Ok(()), }) } } @@ -201,7 +217,7 @@ pub fn test_process_audio_out_of_place_basic( }; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); Ok(()) })?; @@ -239,8 +255,20 @@ pub fn test_process_audio_in_place_basic( } }; + if audio_ports_config + .inputs + .iter() + .all(|x| x.in_place_pair_idx.is_none()) + { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not have any in-place audio port pairs.", + )), + }); + } + let mut audio_buffers = AudioBuffers::new_in_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); Ok(()) })?; @@ -300,7 +328,7 @@ pub fn test_process_note_out_of_place_basic( // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(note_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( &mut prng, @@ -365,7 +393,7 @@ pub fn test_process_note_inconsistent( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); // TODO: Use in-place processing for this test - ProcessingTest::new(&plugin, &mut audio_buffers).run(5, |process_data| { + ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( &mut prng, @@ -420,7 +448,7 @@ pub fn test_process_varying_sample_rates( ProcessingTest::new(&plugin, &mut audio_buffers) .with_sample_rate(sample_rate) - .run(5, |process_data| { + .run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); if let Some(note_event_rng) = note_event_rng.as_mut() { @@ -484,7 +512,7 @@ pub fn test_process_varying_block_sizes( AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); ProcessingTest::new(&plugin, &mut audio_buffers) - .run(5, |process_data| { + .run_simple(5, |process_data| { process_data.buffers.randomize(&mut prng); if let Some(note_event_rng) = note_event_rng.as_mut() { @@ -544,7 +572,7 @@ pub fn test_process_random_block_sizes( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); - ProcessingTest::new(&plugin, &mut audio_buffers).run(20, |process_data| { + ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(20, |process_data| { process_data.block_size = if prng.gen_bool(0.8) { prng.gen_range(2..=MAX_BUFFER_SIZE) } else { @@ -570,12 +598,108 @@ pub fn test_process_random_block_sizes( Ok(TestStatus::Success { details: None }) } -/// The process for consistency. This verifies that the output buffer doesn't contain any NaN, +/// The test for `PluginTestCase::ProcessVaryingBlockSizes`. +pub fn test_process_audio_constant_mask( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); + let mut original_buffers = audio_buffers.clone(); + + let mut has_received_constant_output = false; + let mut has_received_constant_flag = false; + + ProcessingTest::new(&plugin, &mut audio_buffers).run(|callback| match callback { + ProcessingCallback::Prepare { + process_data, + iteration, + } => { + process_data.buffers.randomize(&mut prng); + + if iteration != 1 { + process_data.buffers.silence_all_inputs(); + } + + original_buffers.clone_from(&process_data.buffers); + Ok(true) + } + ProcessingCallback::Validate { + process_data, + iteration, + } => { + check_process_call_consistency(process_data, &original_buffers, true)?; + + for buffer in process_data.buffers.buffers() { + let Some(output) = buffer.output() else { + continue; + }; + + for channel in 0..buffer.channels() { + let is_constant = (0..buffer.len()) + .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); + + let marked_constant = process_data.buffers.output_constant_mask(output) + & (1u64.unbounded_shl(channel as u32)) + != 0; + + if marked_constant && !is_constant { + anyhow::bail!( + "The plugin has marked output port {output}, channel {channel} as \ + constant, but it contains non-constant data." + ); + } + + has_received_constant_flag |= marked_constant; + has_received_constant_output |= is_constant; + } + } + + Ok(iteration < 20) + } + })?; + + host.callback_error_check() + .context("An error occured during a host callback")?; + + if !has_received_constant_flag && has_received_constant_output { + return Ok(TestStatus::Warning { + details: Some(format!( + "The plugin does not seem to set the constant mask during processing.", + )), + }); + } + + Ok(TestStatus::Success { details: None }) +} + +/// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. fn check_process_call_consistency( process_data: &ProcessData, - original_buffers: AudioBuffers, + original_buffers: &AudioBuffers, check_denormals: bool, ) -> Result<()> { let block_size = process_data.block_size as usize; @@ -588,21 +712,7 @@ fn check_process_call_consistency( { // Input-only buffers must not be overwritten during out of place processing if let (Some(index), None) = (buffer.input(), buffer.output()) { - let matches = match (buffer, before) { - ( - AudioBuffer::Float32 { data: after, .. }, - AudioBuffer::Float32 { data: before, .. }, - ) => after == before, - - ( - AudioBuffer::Float64 { data: after, .. }, - AudioBuffer::Float64 { data: before, .. }, - ) => after == before, - - _ => unreachable!(), - }; - - if !matches { + if !buffer.is_same(before) { anyhow::bail!( "The plugin has overwritten an input buffer (index {index}) during \ out-of-place processing." @@ -612,37 +722,15 @@ fn check_process_call_consistency( // Output-only buffers must not be left "untouched" during out of place processing if let (Some(index), None) = (buffer.output(), buffer.input()) { - let matches = match (buffer, before) { - ( - AudioBuffer::Float32 { data: after, .. }, - AudioBuffer::Float32 { data: before, .. }, - ) => after.iter().zip(before.iter()).all(|(after, before)| { - after - .iter() - .zip(before.iter()) - .take(block_size) - .all(|(after, before)| { - after.is_nan() && (after.to_bits() == before.to_bits()) - }) - }), - - ( - AudioBuffer::Float64 { data: after, .. }, - AudioBuffer::Float64 { data: before, .. }, - ) => after.iter().zip(before.iter()).all(|(after, before)| { - after - .iter() - .zip(before.iter()) - .take(block_size) - .all(|(after, before)| { - after.is_nan() && (after.to_bits() == before.to_bits()) - }) - }), - - _ => unreachable!(), - }; - - if matches { + let is_all_nans = (0..buffer.channels()).all(|channel| { + (0..block_size).all(|sample| { + buffer + .get(channel, sample) + .either(|x| x.is_nan(), |x| x.is_nan()) + }) + }); + + if is_all_nans && buffer.is_same(before) { anyhow::bail!( "The plugin has left an output buffer (index {index}) untouched during \ out-of-place processing." @@ -652,43 +740,41 @@ fn check_process_call_consistency( // Output buffers must not contain any non-finite or denormal values if let Some(port_idx) = buffer.output() { - match buffer { - AudioBuffer::Float32 { data, .. } => { - for (channel_idx, channel) in data.iter().enumerate() { - for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { - if !sample.is_finite() { - anyhow::bail!( - "The sample written to output port {port_idx}, channel \ - {channel_idx}, and sample index {sample_idx} is {sample:?}." - ); - } else if sample.is_subnormal() && check_denormals { - anyhow::bail!( - "The sample written to output port {port_idx}, channel \ - {channel_idx}, and sample index {sample_idx} is subnormal \ - ({sample:?})." - ); - } - } + let maybe_non_finite = (0..buffer.channels()) + .flat_map(|channel| (0..block_size).map(move |sample| (channel, sample))) + .find_map(|(channel, sample)| { + let x = buffer.get(channel, sample); + if x.either(|x| !x.is_finite(), |x| !x.is_finite()) { + Some((x, channel, sample)) + } else { + None } - } + }); - AudioBuffer::Float64 { data, .. } => { - for (channel_idx, channel) in data.iter().enumerate() { - for (sample_idx, sample) in channel.iter().enumerate().take(block_size) { - if !sample.is_finite() { - anyhow::bail!( - "The sample written to output port {port_idx}, channel \ - {channel_idx}, and sample index {sample_idx} is {sample:?}." - ); - } else if sample.is_subnormal() && check_denormals { - anyhow::bail!( - "The sample written to output port {port_idx}, channel \ - {channel_idx}, and sample index {sample_idx} is subnormal \ - ({sample:?})." - ); - } + if let Some((sample, channel_idx, sample_idx)) = maybe_non_finite { + anyhow::bail!( + "The sample written to output port {port_idx}, channel {channel_idx}, and \ + sample index {sample_idx} is {sample}." + ); + } + + if check_denormals { + let maybe_denormal = (0..buffer.channels()) + .flat_map(|channel| (0..block_size).map(move |sample| (channel, sample))) + .find_map(|(channel, sample)| { + let x = buffer.get(channel, sample); + if x.either(|x| x.is_subnormal(), |x| x.is_subnormal()) { + Some((x, channel, sample)) + } else { + None } - } + }); + + if let Some((sample, channel_idx, sample_idx)) = maybe_denormal { + anyhow::bail!( + "The sample written to output port {port_idx}, channel {channel_idx}, and \ + sample index {sample_idx} is subnormal ({sample})." + ); } } } From 73c5eb6d82df9c7e7b4de872aa43ee9c3b4b659b Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 18 Dec 2025 00:42:50 +0400 Subject: [PATCH 008/114] add 'state-random-garbage' test; relax other state tests --- Cargo.lock | 143 ++++++++++++++++++--------------- src/tests/plugin.rs | 9 +++ src/tests/plugin/processing.rs | 2 + src/tests/plugin/state.rs | 82 +++++++++++++++++-- 4 files changed, 167 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c300581..1dcfe69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -72,7 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -95,9 +95,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" @@ -206,7 +206,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.111", ] [[package]] @@ -223,20 +223,19 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ - "is-terminal", "lazy_static", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -244,9 +243,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam" @@ -367,23 +366,12 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "errno" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "cc", "libc", + "windows-sys 0.61.2", ] [[package]] @@ -400,13 +388,13 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -458,7 +446,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -469,7 +457,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix 0.38.6", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -489,15 +477,15 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" @@ -644,18 +632,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.32" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -770,7 +758,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -779,18 +767,18 @@ version = "0.38.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.5", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -815,33 +803,45 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.111", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", + "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -885,9 +885,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "strsim" @@ -927,9 +927,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -946,7 +946,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix 0.38.6", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -975,7 +975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.23", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1048,9 +1048,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" @@ -1076,9 +1076,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" @@ -1101,7 +1101,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -1123,7 +1123,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1174,6 +1174,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -1183,6 +1189,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.1" diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 8f741ed..deebce0 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -57,6 +57,8 @@ pub enum PluginTestCase { StateReproducibilityFlush, #[strum(serialize = "state-buffered-streams")] StateBufferedStreams, + #[strum(serialize = "state-random-garbage")] + StateRandomGarbage, } impl<'a> TestCase<'a> for PluginTestCase { @@ -171,6 +173,10 @@ impl<'a> TestCase<'a> for PluginTestCase { when reloading and resaving the state.", PluginTestCase::StateReproducibilityBasic ), + PluginTestCase::StateRandomGarbage => String::from( + "Loads a megabyte of random bytes via 'clap_plugin_state::load()' and asserts \ + that the plugin doesn't crash.", + ), } } @@ -243,6 +249,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::StateBufferedStreams => { state::test_state_buffered_streams(library, plugin_id) } + PluginTestCase::StateRandomGarbage => { + state::test_state_random_garbage(library, plugin_id) + } }; self.create_result(status) diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 0267e1d..1c01040 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -680,6 +680,8 @@ pub fn test_process_audio_constant_mask( } })?; + drop(plugin); + host.callback_error_check() .context("An error occured during a host callback")?; diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 7da5013..8077a16 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use clap_sys::id::clap_id; +use rand::Rng; use std::collections::BTreeMap; use std::io::Write; @@ -201,7 +202,8 @@ pub fn test_state_reproducibility_null_cookies( .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if actual_param_values != expected_param_values { + + if !compare_params_lenient(&actual_param_values, &expected_param_values) { let param_infos = params .info() .context("Failure while fetching the plugin's parameters")?; @@ -408,7 +410,8 @@ pub fn test_state_reproducibility_flush( .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if actual_param_values != expected_param_values { + + if !compare_params_lenient(&actual_param_values, &expected_param_values) { let param_infos = params .info() .context("Failure while fetching the plugin's parameters")?; @@ -510,10 +513,10 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> // This state file is saved without buffered writes. It's expected that the plugin // implementsq this correctly, so we can check if it handles buffered streams correctly by // treating this as the ground truth. - let expected_stae = state.save()?; + let expected_state = state.save()?; host.handle_callbacks_once(); - (expected_stae, expected_param_values) + (expected_state, expected_param_values) }; // Now we'll recreate the plugin instance, load the state using buffered reads, check the @@ -559,7 +562,8 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if actual_param_values != expected_param_values { + + if !compare_params_lenient(&actual_param_values, &expected_param_values) { let param_infos = params .info() .context("Failure while fetching the plugin's parameters")?; @@ -606,6 +610,50 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> } } +/// The test for `PluginTestCase::StateRandomGarbage`. +pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + + plugin.init().context("Error during initialization")?; + + let state = match plugin.get_extension::() { + Some(state) => state, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + State::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + + host.handle_callbacks_once(); + + let mut random_data = vec![0u8; 1024 * 1024]; + prng.fill(&mut random_data[..]); + + let result = state.load(&random_data); + + host.handle_callbacks_once(); + host.callback_error_check() + .context("An error occured during a host callback")?; + + match result { + Err(_) => Ok(TestStatus::Success { details: None }), + Ok(_) => Ok(TestStatus::Warning { + details: Some(String::from( + "The plugin loaded random bytes successfully, which is unexpected, but the plugin \ + did not crash.", + )), + }), + } +} /// Build a string containing all different values between two sets of values. /// /// # Panics @@ -634,3 +682,27 @@ fn format_mismatching_values( .collect::>() .join(", ") } + +fn compare_params_lenient( + actual: &BTreeMap, + expected: &BTreeMap, +) -> bool { + const EPSILON: f64 = 1e-6; + + if actual.len() != expected.len() { + return false; + } + + for (param_id, expected_value) in expected { + let actual_value = match actual.get(param_id) { + Some(value) => value, + None => return false, + }; + + if (actual_value - expected_value).abs() > EPSILON { + return false; + } + } + + true +} From 24ccdfdcdd657c7c4d952adbe24601e876c9a177 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 18 Dec 2025 14:01:44 +0400 Subject: [PATCH 009/114] refactor stuff; print per-test run time info --- src/commands/validate.rs | 8 +- src/plugin/instance/audio_thread.rs | 8 ++ src/tests.rs | 33 +++--- src/tests/plugin.rs | 18 ++- src/tests/plugin/processing.rs | 169 ++++++++++++--------------- src/tests/plugin/state.rs | 7 +- src/tests/plugin_library.rs | 18 ++- src/tests/plugin_library/scanning.rs | 5 +- src/validator.rs | 22 +++- 9 files changed, 142 insertions(+), 146 deletions(-) diff --git a/src/commands/validate.rs b/src/commands/validate.rs index 9b71675..da880bd 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -64,7 +64,13 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { - println_wrapped!(wrapper, " - {}: {}", $test.name, $test.description); + println_wrapped!( + wrapper, + " - {} {}: {}", + $test.name, + format!("({}ms)", $test.duration.as_millis()).black().bold(), + $test.description + ); let status_text = match $test.status { TestStatus::Success { .. } => "PASSED".green(), diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index e578411..922b287 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -143,6 +143,14 @@ impl<'a> PluginAudioThread<'a> { } } + /// Reset the internal state of the plugin. + pub fn reset(&self) { + assert_plugin_state_eq!(self, PluginStatus::Activated); + + let plugin = self.as_ptr(); + unsafe_clap_call! { plugin=>reset(plugin) }; + } + /// Stop processing audio. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. diff --git a/src/tests.rs b/src/tests.rs index 6f31ad7..40450a6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -19,6 +19,7 @@ use std::fs; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::str::FromStr; +use std::time::Duration; use strum::IntoEnumIterator; use crate::{util, Verbosity}; @@ -40,6 +41,8 @@ pub struct TestResult { pub description: String, /// The outcome of the test. pub status: TestStatus, + /// How much time it took + pub duration: Duration, } /// The result of running a test. Skipped and failed test may optionally include an explanation for @@ -95,7 +98,7 @@ pub trait TestCase<'a>: Display + FromStr + IntoEnumIterator + Sized + 'static { /// /// In the event that this is called for a plugin ID that does not exist within the plugin /// library, then the test will also be marked as failed. - fn run_in_process(&self, args: Self::TestArgs) -> TestResult; + fn run_in_process(&self, args: Self::TestArgs) -> Result; /// Run a test case for a plugin in another process, returning the result. If the test cuases the /// plugin to segfault, then the result will have a status of `TestStatus::Crashed`. If @@ -113,7 +116,7 @@ pub trait TestCase<'a>: Display + FromStr + IntoEnumIterator + Sized + 'static { args: Self::TestArgs, verbosity: Verbosity, hide_output: bool, - ) -> Result { + ) -> Result { // The idea here is that we'll invoke the same clap-validator binary with a special hidden command // that runs a single test. This is the reason why test cases must be convertible to and // from strings. If everything goes correctly, then the child process will write the results @@ -149,13 +152,10 @@ pub trait TestCase<'a>: Display + FromStr + IntoEnumIterator + Sized + 'static { // spawn succeeds then this can never fail: .wait() .context("Error while waiting on clap-validator to finish running the test")?; + if !exit_status.success() { - return Ok(TestResult { - name: self.to_string(), - description: self.description(), - status: TestStatus::Crashed { - details: exit_status.to_string(), - }, + return Ok(TestStatus::Crashed { + details: exit_status.to_string(), }); } @@ -196,18 +196,13 @@ pub trait TestCase<'a>: Display + FromStr + IntoEnumIterator + Sized + 'static { Ok((path, file)) } +} - /// Create a [`TestResult`] for this test case. The test status is wrapped in an anyhow - /// [`Result`] to make writing test cases more ergonomic using the question mark operator. `Err` - /// values are converted to [`TestStatus::Failed`] statuses containing the full error backtrace. - fn create_result(&self, status: Result) -> TestResult { - TestResult { - name: self.to_string(), - description: self.description(), - status: status.unwrap_or_else(|err| TestStatus::Failed { - details: Some(format!("{err:#}")), - }), - } +impl From> for TestStatus { + fn from(status: Result) -> Self { + status.unwrap_or_else(|err| TestStatus::Failed { + details: Some(format!("{err:#}")), + }) } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index deebce0..f5b69e0 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -1,11 +1,11 @@ //! Tests for individual plugin instances. +use super::TestCase; +use crate::{plugin::library::PluginLibrary, tests::TestStatus}; +use anyhow::Result; use clap::ValueEnum; use std::process::Command; -use super::{TestCase, TestResult}; -use crate::plugin::library::PluginLibrary; - mod descriptor; mod params; mod processing; @@ -174,8 +174,8 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::StateReproducibilityBasic ), PluginTestCase::StateRandomGarbage => String::from( - "Loads a megabyte of random bytes via 'clap_plugin_state::load()' and asserts \ - that the plugin doesn't crash.", + "Loads 10 chunks of random bytes via 'clap_plugin_state::load()' and asserts that \ + the plugin doesn't crash.", ), } } @@ -195,8 +195,8 @@ impl<'a> TestCase<'a> for PluginTestCase { .arg(test_name); } - fn run_in_process(&self, (library, plugin_id): Self::TestArgs) -> TestResult { - let status = match self { + fn run_in_process(&self, (library, plugin_id): Self::TestArgs) -> Result { + match self { PluginTestCase::MethodsNonNull => descriptor::test_methods_non_null(library, plugin_id), PluginTestCase::DescriptorConsistency => { descriptor::test_consistency(library, plugin_id) @@ -252,8 +252,6 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::StateRandomGarbage => { state::test_state_random_garbage(library, plugin_id) } - }; - - self.create_result(status) + } } } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 1c01040..b19e49a 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -4,6 +4,7 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; +use crate::plugin::instance::audio_thread::PluginAudioThread; use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; @@ -28,18 +29,6 @@ pub struct ProcessingTest<'a> { config: ProcessConfig, } -pub enum ProcessingCallback<'a, 'b> { - Prepare { - process_data: &'a mut ProcessData<'b>, - iteration: usize, - }, - - Validate { - process_data: &'a ProcessData<'b>, - iteration: usize, - }, -} - impl<'a> ProcessingTest<'a> { pub fn new(plugin: &'a Plugin<'a>, buffers: &'a mut AudioBuffers) -> Self { Self { @@ -61,7 +50,7 @@ impl<'a> ProcessingTest<'a> { pub fn run(self, mut callback: Callback) -> Result<()> where - Callback: FnMut(ProcessingCallback) -> Result + Send, + Callback: FnMut(&PluginAudioThread, &mut ProcessData) -> Result + Send, { // Handle callbacks the plugin may have made during init or these queries. The // `ProcessingTest::run*` functions will implicitly handle all outstanding callbacks before they @@ -75,13 +64,7 @@ impl<'a> ProcessingTest<'a> { let buffer_size = self.buffers.len(); let mut process_data = ProcessData::new(self.buffers, self.config); - - // If the plugin requests a restart in the middle of processing, then the plugin will be - // stopped, deactivated, reactivated, and started again. Because of that, we need to keep - // track of the number of processed iterations manually instead of using a for loop. - let mut iters_done = 0; let mut running = true; - while running { self.plugin .activate(self.config.sample_rate, 1, buffer_size)?; @@ -92,25 +75,10 @@ impl<'a> ProcessingTest<'a> { // This test can be repeated a couple of times // NOTE: We intentionally do not disable denormals here 'processing: while running { - running &= callback(ProcessingCallback::Prepare { - process_data: &mut process_data, - iteration: iters_done, - }) - .with_context(|| format!("Failed to prepare cycle #{}", iters_done + 1))?; - - plugin.process(&mut process_data).with_context(|| { - format!("Error during processing cycle #{}", iters_done + 1) - })?; - - running &= callback(ProcessingCallback::Validate { - process_data: &process_data, - iteration: iters_done, - }) - .with_context(|| format!("Failed to validate cycle #{}", iters_done + 1))?; + running &= callback(&plugin, &mut process_data)?; process_data.clear_events(); process_data.advance_transport(process_data.block_size); - iters_done += 1; // Restart processing as necessary if plugin @@ -120,9 +88,8 @@ impl<'a> ProcessingTest<'a> { .is_ok() { log::trace!( - "Restarting the plugin during processing cycle #{} after a call to \ + "Restarting the plugin during processing cycle after a call to \ 'clap_host::request_restart()'", - iters_done, ); break 'processing; } @@ -155,19 +122,34 @@ impl<'a> ProcessingTest<'a> { Callback: FnMut(&mut ProcessData) -> Result<()> + Send, { let mut original_buffers = self.buffers.clone(); - self.run(|callback| match callback { - ProcessingCallback::Prepare { process_data, .. } => { - preprocess(process_data)?; - original_buffers.clone_from(&process_data.buffers); - Ok(true) - } - ProcessingCallback::Validate { - process_data, - iteration, - } => { - check_process_call_consistency(process_data, &original_buffers, true)?; - Ok(iteration < num_iters) - } + let mut curr_iter = 0; + + self.run(|plugin, process| { + curr_iter += 1; + + preprocess(process).with_context(|| { + format!( + "Failed to preprocess cycle {} out of {}", + curr_iter, num_iters + ) + })?; + + original_buffers.clone_from(&process.buffers); + + plugin.process(process).with_context(|| { + format!("Failed to process cycle {} out of {}", curr_iter, num_iters) + })?; + + check_process_call_consistency(process, &original_buffers, true).with_context( + || { + format!( + "Failed to validate cycle {} out of {}", + curr_iter, num_iters + ) + }, + )?; + + Ok(curr_iter < num_iters) }) } @@ -479,7 +461,7 @@ pub fn test_process_varying_block_sizes( plugin_id: &str, ) -> Result { const BLOCK_SIZES: &[u32] = &[ - 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 1536, 10, 17, 1000, 10000, 2027, + 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 32768, 1536, 10, 17, 2027, ]; let mut prng = new_prng(); @@ -510,9 +492,10 @@ pub fn test_process_varying_block_sizes( let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); + let num_iters = (32768 / buffer_size).min(5); ProcessingTest::new(&plugin, &mut audio_buffers) - .run_simple(5, |process_data| { + .run_simple(num_iters as usize, |process_data| { process_data.buffers.randomize(&mut prng); if let Some(note_event_rng) = note_event_rng.as_mut() { @@ -627,60 +610,56 @@ pub fn test_process_audio_constant_mask( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); let mut original_buffers = audio_buffers.clone(); + let mut curr_iter = 0; let mut has_received_constant_output = false; let mut has_received_constant_flag = false; - ProcessingTest::new(&plugin, &mut audio_buffers).run(|callback| match callback { - ProcessingCallback::Prepare { - process_data, - iteration, - } => { - process_data.buffers.randomize(&mut prng); + ProcessingTest::new(&plugin, &mut audio_buffers).run(|plugin, process| { + process.buffers.randomize(&mut prng); - if iteration != 1 { - process_data.buffers.silence_all_inputs(); - } - - original_buffers.clone_from(&process_data.buffers); - Ok(true) + if curr_iter != 1 { + process.buffers.silence_all_inputs(); } - ProcessingCallback::Validate { - process_data, - iteration, - } => { - check_process_call_consistency(process_data, &original_buffers, true)?; - - for buffer in process_data.buffers.buffers() { - let Some(output) = buffer.output() else { - continue; - }; - - for channel in 0..buffer.channels() { - let is_constant = (0..buffer.len()) - .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); - - let marked_constant = process_data.buffers.output_constant_mask(output) - & (1u64.unbounded_shl(channel as u32)) - != 0; - - if marked_constant && !is_constant { - anyhow::bail!( - "The plugin has marked output port {output}, channel {channel} as \ - constant, but it contains non-constant data." - ); - } - has_received_constant_flag |= marked_constant; - has_received_constant_output |= is_constant; + original_buffers.clone_from(&process.buffers); + curr_iter += 1; + + plugin + .process(process) + .with_context(|| format!("Failed to process cycle {} out of 20", curr_iter))?; + + check_process_call_consistency(process, &original_buffers, true) + .with_context(|| format!("Failed to validate cycle {} out of 20", curr_iter))?; + + for buffer in process.buffers.buffers() { + let Some(output) = buffer.output() else { + continue; + }; + + for channel in 0..buffer.channels() { + let is_constant = (0..buffer.len()) + .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); + + let marked_constant = process.buffers.output_constant_mask(output) + & (1u64.unbounded_shl(channel as u32)) + != 0; + + if marked_constant && !is_constant { + anyhow::bail!( + "Failed to validate cycle {curr_iter} out of 20: The plugin has marked \ + output port {output}, channel {channel} as constant, but it contains \ + non-constant data." + ); } - } - Ok(iteration < 20) + has_received_constant_flag |= marked_constant; + has_received_constant_output |= is_constant; + } } - })?; - drop(plugin); + Ok(curr_iter < 20) + })?; host.callback_error_check() .context("An error occured during a host callback")?; diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 8077a16..f422bc8 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -636,9 +636,12 @@ pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Re host.handle_callbacks_once(); let mut random_data = vec![0u8; 1024 * 1024]; - prng.fill(&mut random_data[..]); + let mut result = Ok(()); - let result = state.load(&random_data); + for _ in 0..10 { + prng.fill(&mut random_data[..]); + result = result.or(state.load(&random_data)); + } host.handle_callbacks_once(); host.callback_error_check() diff --git a/src/tests/plugin_library.rs b/src/tests/plugin_library.rs index ffef82c..bbcc9c3 100644 --- a/src/tests/plugin_library.rs +++ b/src/tests/plugin_library.rs @@ -1,18 +1,16 @@ //! Tests for entire plugin libraries. These are mostly used to test plugin scanning behavior. +use super::TestCase; +use crate::tests::TestStatus; +use anyhow::Result; use clap::ValueEnum; use std::path::Path; use std::process::Command; -use std::time::Duration; - -use super::{TestCase, TestResult}; mod factories; mod preset_discovery; mod scanning; -const SCAN_TIME_LIMIT: Duration = Duration::from_millis(100); - /// Tests for entire CLAP libraries. These are mostly to ensure good plugin scanning practices. See /// the module's heading for more information, and the `description` function below for a /// description of each test case. @@ -56,7 +54,7 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { ), PluginLibraryTestCase::ScanTime => format!( "Checks whether the plugin can be scanned in under {} milliseconds.", - SCAN_TIME_LIMIT.as_millis() + scanning::SCAN_TIME_LIMIT.as_millis() ), PluginLibraryTestCase::ScanRtldNow => String::from( "Checks whether the plugin loads correctly when loaded using 'dlopen(..., \ @@ -91,8 +89,8 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { .arg(test_name); } - fn run_in_process(&self, library_path: Self::TestArgs) -> TestResult { - let status = match self { + fn run_in_process(&self, library_path: Self::TestArgs) -> Result { + match self { PluginLibraryTestCase::PresetDiscoveryCrawl => { preset_discovery::test_crawl(library_path, false) } @@ -110,8 +108,6 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { PluginLibraryTestCase::CreateIdWithTrailingGarbage => { factories::test_create_id_with_trailing_garbage(library_path) } - }; - - self.create_result(status) + } } } diff --git a/src/tests/plugin_library/scanning.rs b/src/tests/plugin_library/scanning.rs index a16c99f..5a08229 100644 --- a/src/tests/plugin_library/scanning.rs +++ b/src/tests/plugin_library/scanning.rs @@ -3,12 +3,13 @@ use anyhow::{Context, Result}; use clap_sys::version::clap_version_is_compatible; use std::path::Path; -use std::time::Instant; +use std::time::{Duration, Instant}; -use super::SCAN_TIME_LIMIT; use crate::plugin::library::PluginLibrary; use crate::tests::TestStatus; +pub const SCAN_TIME_LIMIT: Duration = Duration::from_millis(100); + /// The test for `PluginLibraryTestCase::ScanTime`. pub fn test_scan_time(library_path: &Path) -> Result { let test_start = Instant::now(); diff --git a/src/validator.rs b/src/validator.rs index 0345130..2e34fd4 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -10,6 +10,7 @@ use serde::Serialize; use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; +use std::time::Instant; use strum::IntoEnumIterator; use crate::plugin::library::{PluginLibrary, PluginMetadata}; @@ -371,7 +372,8 @@ pub fn run_single_test(settings: &SingleTestSettings) -> Result<()> { fs::write( &settings.output_file, - serde_json::to_string(&result).context("Could not format the result as JSON")?, + serde_json::to_string(&TestStatus::from(result)) + .context("Could not format the result as JSON")?, ) .with_context(|| { format!( @@ -415,11 +417,19 @@ fn run_test<'a, T: TestCase<'a>>( settings: &ValidatorSettings, args: T::TestArgs, ) -> Result { - if settings.in_process { - Ok(test.run_in_process(args)) + let start = Instant::now(); + let status = if settings.in_process { + TestStatus::from(test.run_in_process(args)) } else { - test.run_out_of_process(args, verbosity, settings.hide_output) - } + test.run_out_of_process(args, verbosity, settings.hide_output)? + }; + + Ok(TestResult { + name: test.to_string(), + description: test.description(), + duration: start.elapsed(), + status, + }) } impl ValidationResult { @@ -482,6 +492,6 @@ impl ValidationResult { impl ValidationTally { /// Get the total number of tests run. pub fn total(&self) -> u32 { - self.num_passed + self.num_failed + self.num_skipped + self.num_passed + self.num_failed + self.num_skipped + self.num_warnings } } From 373f799a28cac6b38f9d9dd095f0e767e88edeb6 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 18 Dec 2025 22:00:48 +0400 Subject: [PATCH 010/114] implement more host interfaces and upgrade clap-sys to latest --- Cargo.lock | 4 +- Cargo.toml | 4 +- src/plugin/ext/audio_ports.rs | 10 +- src/plugin/ext/preset_load.rs | 2 +- src/plugin/host.rs | 55 ++++++++- src/plugin/instance.rs | 7 +- src/plugin/instance/process.rs | 18 +-- src/plugin/library.rs | 4 +- src/plugin/preset_discovery.rs | 2 +- src/plugin/preset_discovery/indexer.rs | 2 +- .../preset_discovery/metadata_receiver.rs | 12 +- src/plugin/preset_discovery/provider.rs | 3 +- src/tests/plugin/descriptor.rs | 106 +++++++++++++----- src/tests/plugin_library/preset_discovery.rs | 2 +- src/util.rs | 2 +- 15 files changed, 166 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dcfe69..7acc12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,8 +149,8 @@ dependencies = [ [[package]] name = "clap-sys" -version = "0.3.0" -source = "git+https://github.com/robbert-vdh/clap-sys.git?rev=04779b57663f6f3f710cb813bde0e499a6515d17#04779b57663f6f3f710cb813bde0e499a6515d17" +version = "0.5.0" +source = "git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156#25d7f53fdb6363ad63fbd80049cb7a42a97ac156" [[package]] name = "clap-validator" diff --git a/Cargo.toml b/Cargo.toml index 49a4209..abeace1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ either = "1.9.0" chrono = { version = "0.4.23", features = ["serde"] } # All the claps! clap = { version = "4.1.8", features = ["derive", "wrap_help"] } -# For CLAP 1.1.8 support -clap-sys = { git = "https://github.com/robbert-vdh/clap-sys.git", rev = "04779b57663f6f3f710cb813bde0e499a6515d17" } +# For CLAP 1.2.2 support +clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } colored = "2.0.0" crossbeam = "0.8.1" libloading = "0.7.3" diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index eb7fa13..714d7d2 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -1,13 +1,12 @@ //! Abstractions for interacting with the `audio-ports` extension. use anyhow::{Context, Result}; +use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::{ clap_audio_port_info, clap_plugin_audio_ports, CLAP_EXT_AUDIO_PORTS, CLAP_PORT_MONO, CLAP_PORT_STEREO, }; -use clap_sys::ext::draft::ambisonic::CLAP_PORT_AMBISONIC; -use clap_sys::ext::draft::cv::CLAP_PORT_CV; -use clap_sys::ext::draft::surround::CLAP_PORT_SURROUND; +use clap_sys::ext::surround::CLAP_PORT_SURROUND; use clap_sys::id::CLAP_INVALID_ID; use std::collections::HashMap; use std::ffi::CStr; @@ -247,10 +246,7 @@ fn is_audio_port_type_consistent(info: &clap_audio_port_info) -> Result<()> { info.channel_count ); } - } else if port_type == CLAP_PORT_SURROUND - || port_type == CLAP_PORT_CV - || port_type == CLAP_PORT_AMBISONIC - { + } else if port_type == CLAP_PORT_SURROUND || port_type == CLAP_PORT_AMBISONIC { // TODO: Test the channel counts by querying those extensions Ok(()) } else { diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 34156af..576261c 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -1,7 +1,7 @@ //! Abstractions for interacting with the `preset-load` extension. use anyhow::{Context, Result}; -use clap_sys::ext::draft::preset_load::{clap_plugin_preset_load, CLAP_EXT_PRESET_LOAD}; +use clap_sys::ext::preset_load::{clap_plugin_preset_load, CLAP_EXT_PRESET_LOAD}; use std::ffi::{CStr, CString}; use std::ptr::NonNull; diff --git a/src/plugin/host.rs b/src/plugin/host.rs index 6ef406a..c4aa541 100644 --- a/src/plugin/host.rs +++ b/src/plugin/host.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::{clap_host_audio_ports, CLAP_EXT_AUDIO_PORTS}; -use clap_sys::ext::draft::preset_load::{clap_host_preset_load, CLAP_EXT_PRESET_LOAD}; +use clap_sys::ext::latency::{clap_host_latency, CLAP_EXT_LATENCY}; use clap_sys::ext::note_ports::{ clap_host_note_ports, clap_note_dialect, CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, @@ -10,9 +10,12 @@ use clap_sys::ext::note_ports::{ use clap_sys::ext::params::{ clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, CLAP_EXT_PARAMS, }; +use clap_sys::ext::preset_load::{clap_host_preset_load, CLAP_EXT_PRESET_LOAD}; use clap_sys::ext::state::{clap_host_state, CLAP_EXT_STATE}; +use clap_sys::ext::tail::{clap_host_tail, CLAP_EXT_TAIL}; use clap_sys::ext::thread_check::{clap_host_thread_check, CLAP_EXT_THREAD_CHECK}; -use clap_sys::factory::draft::preset_discovery::clap_preset_discovery_location_kind; +use clap_sys::ext::voice_info::{clap_host_voice_info, CLAP_EXT_VOICE_INFO}; +use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; use clap_sys::host::clap_host; use clap_sys::id::clap_id; use clap_sys::plugin::clap_plugin; @@ -78,6 +81,9 @@ pub struct Host { clap_host_preset_load: clap_host_preset_load, clap_host_state: clap_host_state, clap_host_thread_check: clap_host_thread_check, + clap_host_latency: clap_host_latency, + clap_host_tail: clap_host_tail, + clap_host_voice_info: clap_host_voice_info, } /// Runtime information about a plugin instance. This keeps track of pending callbacks and things @@ -275,6 +281,15 @@ impl Host { is_main_thread: Some(Self::ext_thread_check_is_main_thread), is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), }, + clap_host_latency: clap_host_latency { + changed: Some(Self::ext_latency_changed), + }, + clap_host_tail: clap_host_tail { + changed: Some(Self::ext_tail_changed), + }, + clap_host_voice_info: clap_host_voice_info { + changed: Some(Self::ext_voice_info_changed), + }, }) } @@ -489,6 +504,12 @@ impl Host { &this.clap_host_state as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { &this.clap_host_thread_check as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_LATENCY { + &this.clap_host_latency as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_TAIL { + &this.clap_host_tail as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { + &this.clap_host_voice_info as *const _ as *const c_void } else { std::ptr::null() } @@ -682,4 +703,34 @@ impl Host { this.is_audio_thread(std::thread::current().id()) } + + unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let (instance, this) = InstanceState::from_clap_host_ptr(host); + + this.assert_main_thread("clap_host_latency::changed()"); + + if instance.status.load() != PluginStatus::Activating { + this.set_callback_error( + "'clap_host_latency::changed()' must only be called within \ + 'clap_plugin::activate()'", + ); + } + } + + unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let (_, this) = InstanceState::from_clap_host_ptr(host); + + this.assert_audio_thread("clap_host_tail::changed()"); + log::debug!("TODO: Handle 'clap_host_tail::changed()'"); + } + + unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let (_, this) = InstanceState::from_clap_host_ptr(host); + + this.assert_main_thread("clap_host_voice_info::changed()"); + log::debug!("TODO: Handle 'clap_host_voice_info::changed()'"); + } } diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 5cd7749..5309a3b 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -62,6 +62,7 @@ pub enum PluginStatus { #[default] Uninitialized, Deactivated, + Activating, Activated, Processing, } @@ -88,7 +89,7 @@ impl Drop for Plugin<'_> { match self.status() { PluginStatus::Uninitialized | PluginStatus::Deactivated => (), PluginStatus::Activated => self.deactivate(), - status @ PluginStatus::Processing => panic!( + status => panic!( "The plugin was in an invalid state '{status:?}' when the instance got dropped, \ this is a clap-validator bug" ), @@ -267,6 +268,9 @@ impl<'lib> Plugin<'lib> { assert!(min_buffer_size >= 1); assert!(max_buffer_size >= min_buffer_size); + // we need to track the `Activating` state to validate that we call clap_host_latency::changed only within the activation call. + self.state.status.store(PluginStatus::Activating); + let plugin = self.as_ptr(); if unsafe_clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size as u32, max_buffer_size as u32) @@ -274,6 +278,7 @@ impl<'lib> Plugin<'lib> { self.state.status.store(PluginStatus::Activated); Ok(()) } else { + self.state.status.store(PluginStatus::Deactivated); anyhow::bail!("'clap_plugin::activate()' returned false.") } } diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 4028cd6..c9c4171 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -20,7 +20,7 @@ use rand_pcg::Pcg32; use std::ffi::c_void; use std::fmt::Debug; use std::pin::Pin; -use std::ptr::null; +use std::ptr::null_mut; use crate::plugin::ext::audio_ports::AudioPortConfig; use crate::util::check_null_ptr; @@ -314,15 +314,15 @@ impl AudioBuffers { clap_inputs[input as usize] = Some(clap_audio_buffer { data32: if buffer.is_64bit() { - null() + null_mut() } else { - pointer_list.as_ptr() as *const _ + pointer_list.as_ptr() as *mut *mut f32 }, data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *const _ + pointer_list.as_ptr() as *mut *mut f64 } else { - null() + null_mut() }, channel_count: pointer_list.len() as u32, @@ -338,15 +338,15 @@ impl AudioBuffers { clap_outputs[output as usize] = Some(clap_audio_buffer { data32: if buffer.is_64bit() { - null() + null_mut() } else { - pointer_list.as_ptr() as *const _ + pointer_list.as_ptr() as *mut *mut f32 }, data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *const _ + pointer_list.as_ptr() as *mut *mut f64 } else { - null() + null_mut() }, channel_count: pointer_list.len() as u32, diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 054e566..e035444 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -2,10 +2,10 @@ use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; -use clap_sys::factory::draft::preset_discovery::{ +use clap_sys::factory::plugin_factory::{clap_plugin_factory, CLAP_PLUGIN_FACTORY_ID}; +use clap_sys::factory::preset_discovery::{ clap_preset_discovery_factory, CLAP_PRESET_DISCOVERY_FACTORY_ID, }; -use clap_sys::factory::plugin_factory::{clap_plugin_factory, CLAP_PLUGIN_FACTORY_ID}; use clap_sys::plugin::clap_plugin_descriptor; use clap_sys::version::clap_version; use serde::Serialize; diff --git a/src/plugin/preset_discovery.rs b/src/plugin/preset_discovery.rs index b5103e0..fae37ba 100644 --- a/src/plugin/preset_discovery.rs +++ b/src/plugin/preset_discovery.rs @@ -1,7 +1,7 @@ //! An abstraction for the preset discovery factory. use anyhow::{Context, Result}; -use clap_sys::factory::draft::preset_discovery::{ +use clap_sys::factory::preset_discovery::{ clap_preset_discovery_factory, clap_preset_discovery_provider_descriptor, }; use clap_sys::version::{clap_version, clap_version_is_compatible}; diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index e906d01..3e51032 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -11,7 +11,7 @@ use std::path::Path; use std::pin::Pin; use std::thread::ThreadId; -use clap_sys::factory::draft::preset_discovery::{ +use clap_sys::factory::preset_discovery::{ clap_preset_discovery_filetype, clap_preset_discovery_indexer, clap_preset_discovery_location, clap_preset_discovery_location_kind, clap_preset_discovery_soundpack, CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index c12ce52..da06754 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -4,11 +4,13 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; -use clap_sys::factory::draft::preset_discovery::{ - clap_plugin_id, clap_preset_discovery_metadata_receiver, clap_timestamp, - CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, - CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, +use clap_sys::factory::preset_discovery::{ + clap_preset_discovery_metadata_receiver, CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, + CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, + CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, }; +use clap_sys::timestamp::clap_timestamp; +use clap_sys::universal_plugin_id::clap_universal_plugin_id; use parking_lot::Mutex; use serde::Serialize; use std::cell::RefCell; @@ -507,7 +509,7 @@ impl<'a> MetadataReceiver<'a> { unsafe extern "C" fn add_plugin_id( receiver: *const clap_preset_discovery_metadata_receiver, - plugin_id: *const clap_plugin_id, + plugin_id: *const clap_universal_plugin_id, ) { check_null_ptr!(receiver, (*receiver).receiver_data, plugin_id); let this = &*((*receiver).receiver_data as *const Self); diff --git a/src/plugin/preset_discovery/provider.rs b/src/plugin/preset_discovery/provider.rs index 4106f86..7565d39 100644 --- a/src/plugin/preset_discovery/provider.rs +++ b/src/plugin/preset_discovery/provider.rs @@ -1,6 +1,7 @@ //! A wrapper around `clap_preset_discovery_provider`. use anyhow::{Context, Result}; +use clap_sys::factory::preset_discovery::clap_preset_discovery_provider; use std::collections::{BTreeMap, HashSet}; use std::ffi::CString; use std::marker::PhantomData; @@ -8,8 +9,6 @@ use std::pin::Pin; use std::ptr::NonNull; use walkdir::WalkDir; -use clap_sys::factory::draft::preset_discovery::clap_preset_discovery_provider; - use super::indexer::{Indexer, IndexerResults}; use super::metadata_receiver::{MetadataReceiver, PresetFile}; use super::{Location, LocationValue, PresetDiscoveryFactory, ProviderMetadata}; diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 6830a5a..98ceab6 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -1,22 +1,7 @@ //! Tests surrounding plugin features. use anyhow::{Context, Result}; -use clap_sys::ext::audio_ports::{clap_plugin_audio_ports, CLAP_EXT_AUDIO_PORTS}; -use clap_sys::ext::audio_ports_config::{ - clap_plugin_audio_ports_config, CLAP_EXT_AUDIO_PORTS_CONFIG, -}; -use clap_sys::ext::gui::{clap_plugin_gui, CLAP_EXT_GUI}; -use clap_sys::ext::latency::{clap_plugin_latency, CLAP_EXT_LATENCY}; -use clap_sys::ext::note_name::{clap_plugin_note_name, CLAP_EXT_NOTE_NAME}; -use clap_sys::ext::note_ports::{clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS}; -use clap_sys::ext::params::{clap_plugin_params, CLAP_EXT_PARAMS}; -use clap_sys::ext::posix_fd_support::{clap_plugin_posix_fd_support, CLAP_EXT_POSIX_FD_SUPPORT}; -use clap_sys::ext::render::{clap_plugin_render, CLAP_EXT_RENDER}; -use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE}; -use clap_sys::ext::tail::{clap_plugin_tail, CLAP_EXT_TAIL}; -use clap_sys::ext::thread_pool::{clap_plugin_thread_pool, CLAP_EXT_THREAD_POOL}; -use clap_sys::ext::timer_support::{clap_plugin_timer_support, CLAP_EXT_TIMER_SUPPORT}; -use clap_sys::ext::voice_info::{clap_plugin_voice_info, CLAP_EXT_VOICE_INFO}; +use clap_sys::ext::*; use clap_sys::plugin_features::{ CLAP_PLUGIN_FEATURE_ANALYZER, CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, CLAP_PLUGIN_FEATURE_INSTRUMENT, CLAP_PLUGIN_FEATURE_NOTE_DETECTOR, CLAP_PLUGIN_FEATURE_NOTE_EFFECT, @@ -132,20 +117,81 @@ pub fn test_methods_non_null(library: &PluginLibrary, plugin_id: &str) -> Result // Check known extensions. unsafe { - check_extension::(&plugin, CLAP_EXT_AUDIO_PORTS)?; - check_extension::(&plugin, CLAP_EXT_AUDIO_PORTS_CONFIG)?; - check_extension::(&plugin, CLAP_EXT_GUI)?; - check_extension::(&plugin, CLAP_EXT_NOTE_NAME)?; - check_extension::(&plugin, CLAP_EXT_NOTE_PORTS)?; - check_extension::(&plugin, CLAP_EXT_PARAMS)?; - check_extension::(&plugin, CLAP_EXT_STATE)?; - check_extension::(&plugin, CLAP_EXT_LATENCY)?; - check_extension::(&plugin, CLAP_EXT_TAIL)?; - check_extension::(&plugin, CLAP_EXT_POSIX_FD_SUPPORT)?; - check_extension::(&plugin, CLAP_EXT_TIMER_SUPPORT)?; - check_extension::(&plugin, CLAP_EXT_THREAD_POOL)?; - check_extension::(&plugin, CLAP_EXT_RENDER)?; - check_extension::(&plugin, CLAP_EXT_VOICE_INFO)?; + check_extension::( + &plugin, + ambisonic::CLAP_EXT_AMBISONIC, + )?; + check_extension::( + &plugin, + audio_ports::CLAP_EXT_AUDIO_PORTS, + )?; + check_extension::( + &plugin, + audio_ports_activation::CLAP_EXT_AUDIO_PORTS_ACTIVATION, + )?; + check_extension::( + &plugin, + audio_ports_config::CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, + )?; + check_extension::( + &plugin, + audio_ports_config::CLAP_EXT_AUDIO_PORTS_CONFIG, + )?; + check_extension::( + &plugin, + configurable_audio_ports::CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, + )?; + check_extension::( + &plugin, + context_menu::CLAP_EXT_CONTEXT_MENU, + )?; + check_extension::(&plugin, gui::CLAP_EXT_GUI)?; + check_extension::( + &plugin, + note_name::CLAP_EXT_NOTE_NAME, + )?; + check_extension::( + &plugin, + note_ports::CLAP_EXT_NOTE_PORTS, + )?; + check_extension::(&plugin, params::CLAP_EXT_PARAMS)?; + check_extension::( + &plugin, + param_indication::CLAP_EXT_PARAM_INDICATION, + )?; + check_extension::( + &plugin, + preset_load::CLAP_EXT_PRESET_LOAD, + )?; + check_extension::(&plugin, state::CLAP_EXT_STATE)?; + check_extension::( + &plugin, + state_context::CLAP_EXT_STATE_CONTEXT, + )?; + check_extension::(&plugin, render::CLAP_EXT_RENDER)?; + check_extension::( + &plugin, + remote_controls::CLAP_EXT_REMOTE_CONTROLS, + )?; + check_extension::(&plugin, surround::CLAP_EXT_SURROUND)?; + check_extension::(&plugin, latency::CLAP_EXT_LATENCY)?; + check_extension::(&plugin, tail::CLAP_EXT_TAIL)?; + check_extension::( + &plugin, + posix_fd_support::CLAP_EXT_POSIX_FD_SUPPORT, + )?; + check_extension::( + &plugin, + timer_support::CLAP_EXT_TIMER_SUPPORT, + )?; + check_extension::( + &plugin, + thread_pool::CLAP_EXT_THREAD_POOL, + )?; + check_extension::( + &plugin, + voice_info::CLAP_EXT_VOICE_INFO, + )?; } Ok(TestStatus::Success { details: None }) diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 3b8425e..67cdc5a 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -1,7 +1,7 @@ //! Tests involving the preset discovery factory. use anyhow::{Context, Result}; -use clap_sys::factory::draft::preset_discovery::CLAP_PRESET_DISCOVERY_FACTORY_ID; +use clap_sys::factory::preset_discovery::CLAP_PRESET_DISCOVERY_FACTORY_ID; use std::collections::BTreeMap; use std::path::Path; diff --git a/src/util.rs b/src/util.rs index 831e5eb..68aad2e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use chrono::{DateTime, TimeZone, Utc}; -use clap_sys::factory::draft::preset_discovery::{clap_timestamp, CLAP_TIMESTAMP_UNKNOWN}; +use clap_sys::timestamp::{clap_timestamp, CLAP_TIMESTAMP_UNKNOWN}; use std::ffi::CStr; use std::os::raw::c_char; use std::path::PathBuf; From 919f9349b57c427d7e8350808ac26fd1b3890ba2 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 20 Dec 2025 05:41:12 +0400 Subject: [PATCH 011/114] fix 'audio-process-constant-mask' test; add latency plugin extension --- src/plugin/ext.rs | 1 + src/plugin/ext/latency.rs | 41 ++++++++++++++++++++++++++++++++++ src/plugin/host.rs | 2 -- src/plugin/instance/process.rs | 19 +++++++++------- src/tests/plugin/processing.rs | 6 ++--- src/tests/rng.rs | 6 ++--- 6 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 src/plugin/ext/latency.rs diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index b676f48..325c410 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -6,6 +6,7 @@ use std::ffi::CStr; use std::ptr::NonNull; pub mod audio_ports; +pub mod latency; pub mod note_ports; pub mod params; pub mod preset_load; diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs new file mode 100644 index 0000000..c307068 --- /dev/null +++ b/src/plugin/ext/latency.rs @@ -0,0 +1,41 @@ +use crate::{ + plugin::{ + ext::Extension, + instance::{Plugin, PluginStatus}, + }, + util::unsafe_clap_call, +}; +use clap_sys::ext::latency::{clap_plugin_latency, CLAP_EXT_LATENCY}; +use std::{ffi::CStr, ptr::NonNull}; + +pub struct Latency<'a> { + plugin: &'a Plugin<'a>, + latency: NonNull, +} + +impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { + const EXTENSION_ID: &'static CStr = CLAP_EXT_LATENCY; + + type Struct = clap_plugin_latency; + + fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + latency: extension_struct, + } + } +} + +impl<'a> Latency<'a> { + pub fn get(&self) -> u32 { + assert!( + self.plugin.status() >= PluginStatus::Activating, + "The 'latency' extension's 'get' function can only be called while the plugin is \ + activating or active. This is a bug in the validator." + ); + + let latency = self.latency.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe_clap_call! { latency=>get(plugin) } + } +} diff --git a/src/plugin/host.rs b/src/plugin/host.rs index c4aa541..4a0f3d7 100644 --- a/src/plugin/host.rs +++ b/src/plugin/host.rs @@ -180,7 +180,6 @@ impl InstanceState { // We need to get the pointer to the pinned `InstanceState` into the `clap_host::host_data` // field instance.clap_host.lock().host_data = &*instance as *const Self as *mut c_void; - instance } @@ -709,7 +708,6 @@ impl Host { let (instance, this) = InstanceState::from_clap_host_ptr(host); this.assert_main_thread("clap_host_latency::changed()"); - if instance.status.load() != PluginStatus::Activating { this.set_callback_error( "'clap_host_latency::changed()' must only be called within \ diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index c9c4171..e89c010 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -250,7 +250,9 @@ impl<'a> ProcessData<'a> { /// Advance the transport by a certain number of samples. Make sure to also call /// [`clear_events()`][Self::clear_events()]. - pub fn advance_transport(&mut self, samples: u32) { + pub fn advance_next(&mut self, samples: u32) { + self.input_events.events.lock().clear(); + self.output_events.events.lock().clear(); self.sample_pos += samples; self.transport_info.song_pos_beats = @@ -261,13 +263,6 @@ impl<'a> ProcessData<'a> { * CLAP_SECTIME_FACTOR as f64) .round() as i64; } - - /// Clear the event queues. Make sure to also call - /// [`advance_transport()`][Self::advance_transport()]. - pub fn clear_events(&mut self) { - self.input_events.events.lock().clear(); - self.output_events.events.lock().clear(); - } } impl AudioBuffers { @@ -494,6 +489,14 @@ impl AudioBuffers { } } } + + for input in &mut self.clap_inputs { + input.constant_mask = 0; + } + + for output in &mut self.clap_outputs { + output.constant_mask = 0; + } } /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index b19e49a..7f32e02 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -76,9 +76,7 @@ impl<'a> ProcessingTest<'a> { // NOTE: We intentionally do not disable denormals here 'processing: while running { running &= callback(&plugin, &mut process_data)?; - - process_data.clear_events(); - process_data.advance_transport(process_data.block_size); + process_data.advance_next(process_data.block_size); // Restart processing as necessary if plugin @@ -641,7 +639,7 @@ pub fn test_process_audio_constant_mask( let is_constant = (0..buffer.len()) .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); - let marked_constant = process.buffers.output_constant_mask(output) + let marked_constant = dbg!(process.buffers.output_constant_mask(output)) & (1u64.unbounded_shl(channel as u32)) != 0; diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 9b21128..30c9839 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -4,8 +4,8 @@ use anyhow::{Context, Result}; use clap_sys::events::{ clap_event_header, clap_event_midi, clap_event_note, clap_event_note_expression, clap_event_param_value, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_CHOKE, - CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_VALUE, CLAP_NOTE_EXPRESSION_PRESSURE, - CLAP_NOTE_EXPRESSION_TUNING, CLAP_NOTE_EXPRESSION_VOLUME, + CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_VALUE, + CLAP_NOTE_EXPRESSION_PRESSURE, CLAP_NOTE_EXPRESSION_TUNING, CLAP_NOTE_EXPRESSION_VOLUME, }; use clap_sys::ext::note_ports::{ CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, @@ -321,7 +321,7 @@ impl NoteGenerator { size: std::mem::size_of::() as u32, time: time_offset, space_id: CLAP_CORE_EVENT_SPACE_ID, - type_: CLAP_EVENT_NOTE_CHOKE, + type_: CLAP_EVENT_NOTE_EXPRESSION, flags: 0, }, expression_id, From 761f6503b289ac81d105299cd0bd35c46117d9bf Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 20 Dec 2025 17:57:54 +0400 Subject: [PATCH 012/114] cleanup --- src/tests/plugin.rs | 2 +- src/tests/plugin/processing.rs | 6 ++---- src/tests/rng.rs | 7 +++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index f5b69e0..4598daa 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -140,7 +140,7 @@ impl<'a> TestCase<'a> for PluginTestCase { params::FUZZ_RUNS_PER_PERMUTATION ), PluginTestCase::ParamSetWrongNamespace => String::from( - "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event tyep but with \ + "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ a mismatching namespace ID. Asserts that the plugin's parameter values don't \ change.", ), diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 7f32e02..162d22b 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -52,9 +52,7 @@ impl<'a> ProcessingTest<'a> { where Callback: FnMut(&PluginAudioThread, &mut ProcessData) -> Result + Send, { - // Handle callbacks the plugin may have made during init or these queries. The - // `ProcessingTest::run*` functions will implicitly handle all outstanding callbacks before they - // return. + // Handle callbacks the plugin may have made during init or these queries. self.plugin.host().handle_callbacks_once(); self.plugin @@ -639,7 +637,7 @@ pub fn test_process_audio_constant_mask( let is_constant = (0..buffer.len()) .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); - let marked_constant = dbg!(process.buffers.output_constant_mask(output)) + let marked_constant = process.buffers.output_constant_mask(output) & (1u64.unbounded_shl(channel as u32)) != 0; diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 30c9839..d4ab5cd 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -515,6 +515,13 @@ impl NoteGenerator { validator" ); } + + pub fn reset(&mut self) { + self.next_note_id = 0; + for active_notes in &mut self.active_notes { + active_notes.clear(); + } + } } impl NoteEventType { From 0841638969723eb9c8711bb4ab3748a481bbcb9c Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 20 Dec 2025 20:51:55 +0400 Subject: [PATCH 013/114] relax 'state-*' tests: binary equality check now emits a warning instead --- src/tests/plugin/state.rs | 61 ++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index f422bc8..40542cd 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -235,12 +235,14 @@ pub fn test_state_reproducibility_null_cookies( expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; - anyhow::bail!( - "Re-saving the loaded state resulted in a different state file. Expected: '{}'. \ - Actual: '{}'.", - expected_state_file_path.display(), - actual_state_file_path.display(), - ) + Ok(TestStatus::Warning { + details: Some(format!( + "The saved state after loading differs from the original saved state. Expected: \ + '{}'. Actual: '{}'.", + expected_state_file_path.display(), + actual_state_file_path.display(), + )), + }) } } @@ -442,12 +444,14 @@ pub fn test_state_reproducibility_flush( expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; - anyhow::bail!( - "Sending the same parameter values to two different instances of the plugin resulted \ - in different state files. Expected: '{}'. Actual: '{}'.", - expected_state_file_path.display(), - actual_state_file_path.display(), - ) + Ok(TestStatus::Warning { + details: Some(format!( + "Sending the same parameter values to two different instances of the plugin \ + resulted in different state files. Expected: '{}'. Actual: '{}'.", + expected_state_file_path.display(), + actual_state_file_path.display(), + )), + }) } } @@ -583,9 +587,9 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; host.handle_callbacks_once(); - host.callback_error_check() .context("An error occured during a host callback")?; + if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { @@ -598,18 +602,25 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; - anyhow::bail!( - "Re-saving the loaded state resulted in a different state file. The original state \ - file being compared to was written unbuffered, reloaded by allowing the plugin to \ - read only {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written again by \ - allowing the plugin to write only {BUFFERED_SAVE_MAX_BYTES} bytes at a time. \ - Expected: '{}'. Actual: '{}'.", - expected_state_file_path.display(), - actual_state_file_path.display(), - ) + Ok(TestStatus::Warning { + details: Some(format!( + "Re-saving the loaded state resulted in a different state file. The original \ + state file being compared to was written unbuffered, reloaded by allowing the \ + plugin to read only {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written \ + again by allowing the plugin to write only {BUFFERED_SAVE_MAX_BYTES} bytes at a \ + time. Expected: '{}'. Actual: '{}'.", + expected_state_file_path.display(), + actual_state_file_path.display(), + )), + }) } } +fn compare_approx(actual: f64, expected: f64) -> bool { + const EPSILON: f64 = 1e-5; + (actual - expected).abs() <= EPSILON +} + /// The test for `PluginTestCase::StateRandomGarbage`. pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); @@ -672,7 +683,7 @@ fn format_mismatching_values( .into_iter() .filter_map(|(param_id, actual_value)| { let expected_value = expected_param_values[¶m_id]; - if actual_value == expected_value { + if compare_approx(actual_value, expected_value) { None } else { let param_name = ¶m_infos[¶m_id].name; @@ -690,8 +701,6 @@ fn compare_params_lenient( actual: &BTreeMap, expected: &BTreeMap, ) -> bool { - const EPSILON: f64 = 1e-6; - if actual.len() != expected.len() { return false; } @@ -702,7 +711,7 @@ fn compare_params_lenient( None => return false, }; - if (actual_value - expected_value).abs() > EPSILON { + if !compare_approx(*actual_value, *expected_value) { return false; } } From fdc8fb649cd87e3657d2c4314f82edbfafb3578d Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 30 Dec 2025 18:00:38 +0400 Subject: [PATCH 014/114] refactor process (again); add `process-reset-determinism` test --- src/plugin/instance/audio_thread.rs | 2 +- src/plugin/instance/process.rs | 131 ++++++- src/tests/plugin.rs | 14 +- src/tests/plugin/params.rs | 23 +- src/tests/plugin/processing.rs | 356 +++++++++---------- src/tests/plugin/state.rs | 47 ++- src/tests/plugin_library/preset_discovery.rs | 10 +- src/tests/rng.rs | 17 +- 8 files changed, 373 insertions(+), 227 deletions(-) diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 922b287..02bc315 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -145,7 +145,7 @@ impl<'a> PluginAudioThread<'a> { /// Reset the internal state of the plugin. pub fn reset(&self) { - assert_plugin_state_eq!(self, PluginStatus::Activated); + assert_plugin_state_eq!(self, PluginStatus::Processing); let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>reset(plugin) }; diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index e89c010..a87fa00 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -21,8 +21,11 @@ use std::ffi::c_void; use std::fmt::Debug; use std::pin::Pin; use std::ptr::null_mut; +use std::sync::atomic::Ordering; use crate::plugin::ext::audio_ports::AudioPortConfig; +use crate::plugin::instance::audio_thread::PluginAudioThread; +use crate::plugin::instance::Plugin; use crate::util::check_null_ptr; /// The input and output data for a call to `clap_plugin::process()`. @@ -46,6 +49,14 @@ pub struct ProcessData<'a> { // TODO: Maybe do something with `steady_time` } +/// Control flow for the processing loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessControlFlow { + Continue, + Reset, + Exit, +} + /// The general context information for a process call. #[derive(Debug, Clone, Copy)] pub struct ProcessConfig { @@ -128,7 +139,7 @@ pub struct EventQueue { vtable_output: clap_output_events, /// The actual event queue. Since we're going for correctness over performance, this uses a very /// suboptimal memory layout by just using an `enum` instead of doing fancy bit packing. - pub events: Mutex>, + events: Mutex>, } /// An event sent to or from the plugin. This uses an enum to make the implementation simple and @@ -248,13 +259,12 @@ impl<'a> ProcessData<'a> { self.transport_info } - /// Advance the transport by a certain number of samples. Make sure to also call - /// [`clear_events()`][Self::clear_events()]. - pub fn advance_next(&mut self, samples: u32) { - self.input_events.events.lock().clear(); - self.output_events.events.lock().clear(); - self.sample_pos += samples; + /// Advance the transport by a certain number of samples. + pub fn advance_next(&mut self) { + self.input_events.clear(); + self.output_events.clear(); + self.sample_pos += self.block_size; self.transport_info.song_pos_beats = ((self.sample_pos as f64 / self.config.sample_rate / 60.0 * self.transport_info.tempo) * CLAP_BEATTIME_FACTOR as f64) @@ -263,6 +273,81 @@ impl<'a> ProcessData<'a> { * CLAP_SECTIME_FACTOR as f64) .round() as i64; } + + pub fn reset(&mut self) { + self.sample_pos = 0; + self.transport_info.song_pos_beats = 0; + self.transport_info.song_pos_seconds = 0; + self.input_events.clear(); + self.output_events.clear(); + } + + pub fn run(&mut self, plugin: &Plugin, mut process: Process) -> Result<()> + where + Process: FnMut(&PluginAudioThread, &mut Self) -> Result + Send, + { + let mut running = true; + while running { + plugin.activate(self.config.sample_rate, 1, self.buffers.len())?; + plugin.host().handle_callbacks_once(); + self.reset(); + + plugin.on_audio_thread(|plugin| -> Result<()> { + plugin.start_processing()?; + + // This test can be repeated a couple of times + // NOTE: We intentionally do not disable denormals here + 'processing: while running { + let flow = process(&plugin, self)?; + running &= flow != ProcessControlFlow::Exit; + self.advance_next(); + + // Restart processing as necessary + if plugin + .state() + .requested_restart + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + log::trace!( + "Restarting the plugin during processing cycle after a call to \ + 'clap_host::request_restart()'", + ); + break 'processing; + } + + if flow == ProcessControlFlow::Reset { + break 'processing; + } + } + + plugin.stop_processing(); + + Ok(()) + })?; + + plugin.deactivate(); + } + + // Handle callbacks the plugin may have made during deactivate + plugin.host().handle_callbacks_once(); + + Ok(()) + } + + pub fn run_once(&mut self, plugin: &Plugin, process: Process) -> Result<()> + where + Process: FnOnce(&PluginAudioThread, &mut Self) -> Result<()> + Send, + { + let mut process = Some(process); + self.run(plugin, |plugin, instance| { + if let Some(process) = process.take() { + process(plugin, instance)?; + } + + Ok(ProcessControlFlow::Exit) + }) + } } impl AudioBuffers { @@ -443,6 +528,21 @@ impl AudioBuffers { &self.buffers } + /// Check whether the audio buffers are identical to another set of audio buffers. + pub fn is_same(&self, other: &Self) -> bool { + if self.buffers.len() != other.buffers.len() { + return false; + } + + for (this, other) in self.buffers.iter().zip(other.buffers.iter()) { + if !this.is_same(other) { + return false; + } + } + + true + } + /// Fill the input and output buffers with arbitrary values. pub fn fill(&mut self, mut fill: impl AudioBufferFill) { for bus in &mut self.buffers { @@ -691,6 +791,23 @@ impl EventQueue { queue } + pub fn clear(&self) { + self.events.lock().clear(); + } + + pub fn add_events(&self, extend: impl IntoIterator) { + let mut events = self.events.lock(); + let should_sort = !events.is_empty(); + events.extend(extend); + if should_sort { + events.sort_by_key(|event| event.header().time); + } + } + + pub fn read(&self) -> Vec { + self.events.lock().clone() + } + /// Get the vtable pointer for input events. pub fn vtable_input(self: &Pin>) -> *const clap_input_events { &self.vtable_input diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 4598daa..ba21c9d 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -8,11 +8,9 @@ use std::process::Command; mod descriptor; mod params; -mod processing; +pub mod processing; mod state; -pub use processing::ProcessingTest; - /// The tests for individual CLAP plugins. See the module's heading for more information, and the /// `description` function below for a description of each test case. #[derive(strum_macros::Display, strum_macros::EnumString, strum_macros::EnumIter)] @@ -41,6 +39,8 @@ pub enum PluginTestCase { ProcessVaryingBlockSizes, #[strum(serialize = "process-random-block-sizes")] ProcessRandomBlockSizes, + #[strum(serialize = "process-reset-determinism")] + ProcessResetDeterminism, #[strum(serialize = "param-conversions")] ParamConversions, #[strum(serialize = "param-fuzz-basic")] @@ -127,6 +127,11 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses out-of-place audio processing.", ), + PluginTestCase::ProcessResetDeterminism => String::from( + "Asserts that resetting the plugin via 'clap_plugin::reset()' and via \ + re-activation results in deterministic output when processing the same audio and \ + events again.", + ), PluginTestCase::ParamConversions => String::from( "Asserts that value to string and string to value conversions are supported for \ ether all or none of the plugin's parameters, and that conversions between \ @@ -231,6 +236,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessRandomBlockSizes => { processing::test_process_random_block_sizes(library, plugin_id) } + PluginTestCase::ProcessResetDeterminism => { + processing::test_process_reset_determinism(library, plugin_id) + } PluginTestCase::ParamConversions => params::test_param_conversions(library, plugin_id), PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id), PluginTestCase::ParamSetWrongNamespace => { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 6b775ca..52c6f89 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -13,9 +13,9 @@ use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::params::Params; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{AudioBuffers, Event}; +use crate::plugin::instance::process::{AudioBuffers, Event, ProcessData}; use crate::plugin::library::PluginLibrary; -use crate::tests::plugin::ProcessingTest; +use crate::tests::plugin::processing::run_simple; use crate::tests::rng::{new_prng, NoteGenerator, ParamFuzzer}; use crate::tests::{TestCase, TestStatus}; @@ -238,16 +238,21 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result let mut current_events: Option>; let mut previous_events: Option> = None; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); let mut have_set_parameters = false; - let run_result = ProcessingTest::new(&plugin, &mut audio_buffers).run_simple( + let run_result = run_simple( + &plugin, + &mut process_data, FUZZ_RUNS_PER_PERMUTATION, |process_data| { if !have_set_parameters { - *process_data.input_events.events.lock() = current_events.clone().unwrap(); + process_data + .input_events + .add_events(current_events.clone().unwrap()); have_set_parameters = true; } @@ -378,9 +383,15 @@ pub fn test_param_set_wrong_namespace( } let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - ProcessingTest::new(&plugin, &mut audio_buffers).run_once(|process_data| { + let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); + + process_data.run_once(&plugin, move |plugin, process_data| { process_data.buffers.randomize(&mut prng); - *process_data.input_events.events.lock() = random_param_set_events; + process_data + .input_events + .add_events(random_param_set_events); + plugin.process(process_data)?; + Ok(()) })?; diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 162d22b..5c64e52 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -4,167 +4,57 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::audio_thread::PluginAudioThread; -use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; +use crate::plugin::instance::process::{ + AudioBuffers, ProcessConfig, ProcessControlFlow, ProcessData, +}; use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::rng::{new_prng, NoteGenerator}; use crate::tests::TestStatus; use anyhow::{Context, Result}; use rand::Rng; -use std::sync::atomic::Ordering; - -/// A helper to handle the boilerplate that comes with testing a plugin's audio processing behavior. -/// Run the standard audio processing test for a still **deactivated** plugin. This calls the -/// process function `num_iters` times, and checks the output for consistency each time. -/// -/// The `Preprocess` closure is called before each processing cycle to allow the process data to be -/// modified for the next process cycle. -/// -/// Main-thread callbacks that were made to the plugin while the audio thread was active are -/// handled implicitly. -pub struct ProcessingTest<'a> { - plugin: &'a Plugin<'a>, - buffers: &'a mut AudioBuffers, - config: ProcessConfig, -} - -impl<'a> ProcessingTest<'a> { - pub fn new(plugin: &'a Plugin<'a>, buffers: &'a mut AudioBuffers) -> Self { - Self { - plugin, - buffers, - config: ProcessConfig::default(), - } - } - - pub fn with_sample_rate(self, sample_rate: f64) -> Self { - Self { - config: ProcessConfig { - sample_rate, - ..self.config - }, - ..self - } - } - - pub fn run(self, mut callback: Callback) -> Result<()> - where - Callback: FnMut(&PluginAudioThread, &mut ProcessData) -> Result + Send, - { - // Handle callbacks the plugin may have made during init or these queries. - self.plugin.host().handle_callbacks_once(); - - self.plugin - .state - .requested_restart - .store(false, Ordering::SeqCst); - - let buffer_size = self.buffers.len(); - let mut process_data = ProcessData::new(self.buffers, self.config); - let mut running = true; - while running { - self.plugin - .activate(self.config.sample_rate, 1, buffer_size)?; - - self.plugin.on_audio_thread(|plugin| -> Result<()> { - plugin.start_processing()?; - - // This test can be repeated a couple of times - // NOTE: We intentionally do not disable denormals here - 'processing: while running { - running &= callback(&plugin, &mut process_data)?; - process_data.advance_next(process_data.block_size); - - // Restart processing as necessary - if plugin - .state() - .requested_restart - .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) - .is_ok() - { - log::trace!( - "Restarting the plugin during processing cycle after a call to \ - 'clap_host::request_restart()'", - ); - break 'processing; - } - } - - plugin.stop_processing(); - Ok(()) - })?; +pub fn run_simple( + plugin: &Plugin, + data: &mut ProcessData, + num_iters: usize, + mut preprocess: Callback, +) -> Result<()> +where + Callback: FnMut(&mut ProcessData) -> Result<()> + Send, +{ + let mut original_buffers = data.buffers.clone(); + let mut curr_iter = 0; - self.plugin.deactivate(); - } + data.run(plugin, |plugin, process| { + curr_iter += 1; - // Handle callbacks the plugin may have made during deactivate - self.plugin.host().handle_callbacks_once(); + preprocess(process).with_context(|| { + format!( + "Failed to preprocess cycle {} out of {}", + curr_iter, num_iters + ) + })?; - Ok(()) - } + original_buffers.clone_from(&process.buffers); - /// Run the standard audio processing test for a still **deactivated** plugin. This calls the - /// process function `num_iters` times, and checks the output for consistency each time. - /// - /// The `Preprocess` closure is called before each processing cycle to allow the process data to be - /// modified for the next process cycle. - /// - /// Main-thread callbacks that were made to the plugin while the audio thread was active are - /// handled implicitly. - pub fn run_simple(self, num_iters: usize, mut preprocess: Callback) -> Result<()> - where - Callback: FnMut(&mut ProcessData) -> Result<()> + Send, - { - let mut original_buffers = self.buffers.clone(); - let mut curr_iter = 0; - - self.run(|plugin, process| { - curr_iter += 1; - - preprocess(process).with_context(|| { - format!( - "Failed to preprocess cycle {} out of {}", - curr_iter, num_iters - ) - })?; - - original_buffers.clone_from(&process.buffers); - - plugin.process(process).with_context(|| { - format!("Failed to process cycle {} out of {}", curr_iter, num_iters) - })?; - - check_process_call_consistency(process, &original_buffers, true).with_context( - || { - format!( - "Failed to validate cycle {} out of {}", - curr_iter, num_iters - ) - }, - )?; + plugin.process(process).with_context(|| { + format!("Failed to process cycle {} out of {}", curr_iter, num_iters) + })?; - Ok(curr_iter < num_iters) - }) - } + check_process_call_consistency(process, &original_buffers, true).with_context(|| { + format!( + "Failed to validate cycle {} out of {}", + curr_iter, num_iters + ) + })?; - /// Run the standard audio processing test for a still **deactivated** plugin. This is identical - /// to the [`run()`][Self::run()] function, except that it does exactly one processing cycle and - /// thus non-copy values can be moved into the closure. - /// - /// Main-thread callbacks that were made to the plugin while the audio thread was active are - /// handled implicitly. - pub fn run_once(self, preprocess: Preprocess) -> Result<()> - where - Preprocess: FnOnce(&mut ProcessData) -> Result<()> + Send, - { - let mut preprocess = Some(preprocess); - self.run_simple(1, |data| match preprocess.take() { - Some(preprocess) => preprocess(data), - None => Ok(()), - }) - } + if curr_iter < num_iters { + Ok(ProcessControlFlow::Continue) + } else { + Ok(ProcessControlFlow::Exit) + } + }) } /// The test for `PluginTestCase::ProcessAudioOutOfPlaceBasic`. @@ -195,7 +85,8 @@ pub fn test_process_audio_out_of_place_basic( }; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); Ok(()) })?; @@ -246,7 +137,8 @@ pub fn test_process_audio_in_place_basic( } let mut audio_buffers = AudioBuffers::new_in_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); Ok(()) })?; @@ -306,7 +198,9 @@ pub fn test_process_note_out_of_place_basic( // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(note_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + + run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( &mut prng, @@ -369,9 +263,10 @@ pub fn test_process_note_inconsistent( // This RNG (Random Note Generator) allows generates mismatching events let mut note_event_rng = NoteGenerator::new(note_port_config).with_inconsistent_events(); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); // TODO: Use in-place processing for this test - ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(5, |process_data| { + run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( &mut prng, @@ -423,26 +318,31 @@ pub fn test_process_varying_sample_rates( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let mut process_data = ProcessData::new( + &mut audio_buffers, + ProcessConfig { + sample_rate, + ..Default::default() + }, + ); - ProcessingTest::new(&plugin, &mut audio_buffers) - .with_sample_rate(sample_rate) - .run_simple(5, |process_data| { - process_data.buffers.randomize(&mut prng); + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - )?; - } + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + } - Ok(()) - }) - .context(format!( - "Error while processing with {:.2}hz sample rate", - sample_rate - ))?; + Ok(()) + }) + .context(format!( + "Error while processing with {:.2}hz sample rate", + sample_rate + ))?; host.callback_error_check() .context("An error occured during a host callback")?; @@ -488,10 +388,14 @@ pub fn test_process_varying_block_sizes( let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); let num_iters = (32768 / buffer_size).min(5); - ProcessingTest::new(&plugin, &mut audio_buffers) - .run_simple(num_iters as usize, |process_data| { + run_simple( + &plugin, + &mut process_data, + num_iters as usize, + |process_data| { process_data.buffers.randomize(&mut prng); if let Some(note_event_rng) = note_event_rng.as_mut() { @@ -503,11 +407,12 @@ pub fn test_process_varying_block_sizes( } Ok(()) - }) - .context(format!( - "Error while processing with buffer size of {}", - buffer_size - ))?; + }, + ) + .context(format!( + "Error while processing with buffer size of {}", + buffer_size + ))?; host.callback_error_check() .context("An error occured during a host callback")?; @@ -550,8 +455,9 @@ pub fn test_process_random_block_sizes( let mut note_event_rng = note_ports_config.map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - ProcessingTest::new(&plugin, &mut audio_buffers).run_simple(20, |process_data| { + run_simple(&plugin, &mut process_data, 20, |process_data| { process_data.block_size = if prng.gen_bool(0.8) { prng.gen_range(2..=MAX_BUFFER_SIZE) } else { @@ -606,12 +512,14 @@ pub fn test_process_audio_constant_mask( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); let mut original_buffers = audio_buffers.clone(); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + let mut curr_iter = 0; let mut has_received_constant_output = false; let mut has_received_constant_flag = false; - ProcessingTest::new(&plugin, &mut audio_buffers).run(|plugin, process| { + process_data.run(&plugin, |plugin, process| { process.buffers.randomize(&mut prng); if curr_iter != 1 { @@ -654,7 +562,11 @@ pub fn test_process_audio_constant_mask( } } - Ok(curr_iter < 20) + if curr_iter < 20 { + Ok(ProcessControlFlow::Continue) + } else { + Ok(ProcessControlFlow::Exit) + } })?; host.callback_error_check() @@ -671,6 +583,90 @@ pub fn test_process_audio_constant_mask( Ok(TestStatus::Success { details: None }) } +/// The test for `PluginTestCase::ProcessResetDeterminism`. +pub fn test_process_reset_determinism( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => Some( + note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + ), + None => None, + }; + + let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32( + &audio_ports_config, + 4096, /* we do it in one block to simplify the test */ + ); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + let mut curr_iter = 0; + let mut audio_output = vec![]; + + process_data.run(&plugin, |plugin, process_data| { + let mut rng = new_prng(); + + process_data.buffers.randomize(&mut rng); + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut new_prng(), + &process_data.input_events, + process_data.block_size, + )?; + } + + let result = match curr_iter { + 0 => ProcessControlFlow::Reset, + 1 => ProcessControlFlow::Continue, + _ => { + plugin.reset(); + process_data.reset(); + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.reset(); + } + + ProcessControlFlow::Exit + } + }; + + plugin.process(process_data)?; + audio_output.push(process_data.buffers.clone()); + curr_iter += 1; + + Ok(result) + })?; + + if !audio_output[0].is_same(&audio_output[1]) { + return Ok(TestStatus::Warning { + details: Some(format!( + "Plugin output does not seem to be deterministic after reactivation" + )), + }); + } + + if !audio_output[1].is_same(&audio_output[2]) { + anyhow::bail!("Plugin output differs after reset"); + } + + Ok(TestStatus::Success { details: None }) +} + /// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. @@ -759,7 +755,7 @@ fn check_process_call_consistency( // If the plugin output any events, then they should be in a monotonically increasing order let mut last_event_time = 0; - for event in process_data.output_events.events.lock().iter() { + for event in process_data.output_events.read() { let event_time = event.header().time; if event_time < last_event_time { anyhow::bail!( diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 40542cd..2150cbf 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -12,9 +12,10 @@ use crate::plugin::ext::params::{ParamInfo, Params}; use crate::plugin::ext::state::State; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::{AudioBuffers, Event, EventQueue}; +use crate::plugin::instance::process::{ + AudioBuffers, Event, EventQueue, ProcessConfig, ProcessData, +}; use crate::plugin::library::PluginLibrary; -use crate::tests::plugin::ProcessingTest; use crate::tests::rng::{new_prng, ParamFuzzer}; use crate::tests::{TestCase, TestStatus}; @@ -138,8 +139,13 @@ pub fn test_state_reproducibility_null_cookies( } let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { - *process_data.input_events.events.lock() = random_param_set_events; + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + + process_data.run_once(&plugin, move |plugin, process_data| { + process_data + .input_events + .add_events(random_param_set_events); + plugin.process(process_data)?; Ok(()) })?; @@ -308,7 +314,8 @@ pub fn test_state_reproducibility_flush( let input_events = EventQueue::new(); let output_events = EventQueue::new(); - *input_events.events.lock() = random_param_set_events.clone(); + + input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); host.handle_callbacks_once(); @@ -321,7 +328,7 @@ pub fn test_state_reproducibility_flush( host.handle_callbacks_once(); // Plugins with no parameters at all should of course not trigger this error - if expected_param_values == initial_param_values && !param_infos.is_empty() { + if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { anyhow::bail!( "'clap_plugin_params::flush()' has been called with random parameter values, but \ the plugin's reported parameter values have not changed." @@ -403,8 +410,13 @@ pub fn test_state_reproducibility_flush( // In the previous pass we used flush, and here we use the process funciton let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { - *process_data.input_events.events.lock() = new_random_param_set_events; + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + + process_data.run_once(&plugin, move |plugin, process_data| { + process_data + .input_events + .add_events(new_random_param_set_events); + plugin.process(process_data)?; Ok(()) })?; @@ -504,8 +516,12 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> param_fuzzer.randomize_params_at(&mut prng, 0).collect(); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - ProcessingTest::new(&plugin, &mut audio_buffers).run_once(move |process_data| { - *process_data.input_events.events.lock() = random_param_set_events; + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + process_data.run_once(&plugin, move |plugin, process_data| { + process_data + .input_events + .add_events(random_param_set_events); + plugin.process(process_data)?; Ok(()) })?; @@ -616,11 +632,6 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> } } -fn compare_approx(actual: f64, expected: f64) -> bool { - const EPSILON: f64 = 1e-5; - (actual - expected).abs() <= EPSILON -} - /// The test for `PluginTestCase::StateRandomGarbage`. pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); @@ -668,6 +679,12 @@ pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Re }), } } + +fn compare_approx(actual: f64, expected: f64) -> bool { + const EPSILON: f64 = 1e-5; + (actual - expected).abs() <= EPSILON +} + /// Build a string containing all different values between two sets of values. /// /// # Panics diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 67cdc5a..608e1a7 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -9,10 +9,9 @@ use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::preset_load::PresetLoad; use crate::plugin::ext::Extension; use crate::plugin::host::Host; -use crate::plugin::instance::process::AudioBuffers; +use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::library::PluginLibrary; use crate::plugin::preset_discovery::{LocationValue, PluginAbi, Preset, PresetFile}; -use crate::tests::plugin::ProcessingTest; use crate::tests::TestStatus; // TODO: Test for duplicate locations and soundpacks in declared data across all providers @@ -178,8 +177,11 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // We'll process a single buffer of silent audio just to make sure everything's // settled in - ProcessingTest::new(&plugin, &mut audio_buffers) - .run_once(move |_| Ok(())) + ProcessData::new(&mut audio_buffers, ProcessConfig::default()) + .run_once(&plugin, move |plugin, data| { + plugin.process(data)?; + Ok(()) + }) .with_context(|| { format!( "Error while processing an audio buffer after loading a preset for \ diff --git a/src/tests/rng.rs b/src/tests/rng.rs index d4ab5cd..8f8714b 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -120,19 +120,14 @@ impl NoteGenerator { // the previous event. const SAMPLE_OFFSET_RANGE: RangeInclusive = -6..=5; - let mut events = queue.events.lock(); - let should_sort = !events.is_empty(); - - let mut current_sample = prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; - while current_sample < num_samples { - events.push(self.generate(prng, current_sample)?); - - current_sample += prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; + let mut events = vec![]; + let mut sample = prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; + while sample < num_samples { + events.push(self.generate(prng, sample)?); + sample += prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; } - if should_sort { - events.sort_by_key(|event| event.header().time); - } + queue.add_events(events); Ok(()) } From 3f62efee020f15af4be4ce68c306e30abffe86e6 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 30 Dec 2025 18:13:16 +0400 Subject: [PATCH 015/114] refactor 'assert_plugin_state' --- src/plugin.rs | 47 ++++++++++++++--------------- src/plugin/ext/latency.rs | 7 ++--- src/plugin/ext/params.rs | 10 +++--- src/plugin/instance.rs | 12 ++++---- src/plugin/instance/audio_thread.rs | 17 +++++------ 5 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/plugin.rs b/src/plugin.rs index a46a364..0aab2bf 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -10,8 +10,8 @@ pub mod preset_discovery; /// if this is not the case. This is used to ensure the validator's correctness. /// /// Requires a `.status()` method to exist on `$self`. -macro_rules! assert_plugin_state_eq { - ($self:expr, $expected:expr) => { +macro_rules! assert_plugin_state { + ($self:expr, state == $expected:expr) => { let status = $self.status(); if status != $expected { panic!( @@ -21,42 +21,39 @@ macro_rules! assert_plugin_state_eq { ) } }; -} -/// Used for asserting that the plugin is a lower state then the specified one before calling a -/// function. Hard panics if this is not the case. This is used to ensure the validator's -/// correctness. -/// -/// Requires a `.status()` method to exist on `$self`. -macro_rules! assert_plugin_state_lt { - ($self:expr, $other:expr) => { + ($self:expr, state != $expected:expr) => { + let status = $self.status(); + if status == $expected { + panic!( + "Invalid plugin function call while the plugin is in an incorrect state ({:?} != \ + {:?}). This is a bug in the validator.", + status, $expected + ) + } + }; + + ($self:expr, state < $expected:expr) => { let status = $self.status(); - if status >= $other { + if status >= $expected { panic!( "Invalid plugin function call while the plugin is in an incorrect state ({:?} >= \ {:?}). This is a bug in the validator.", - status, $other + status, $expected ) } }; -} -/// Used for asserting that the plugin has been initialized. Hard panics if this is not the case. -/// This is used to ensure the validator's correctness. -/// -/// Requires a `.status()` method to exist on `$self`. -macro_rules! assert_plugin_state_initialized { - ($self:expr) => { + ($self:expr, state >= $expected:expr) => { let status = $self.status(); - if status == PluginStatus::Uninitialized { + if status < $expected { panic!( - "Invalid plugin function call while the plugin has not yet been initialized. This \ - is a bug in the validator." + "Invalid plugin function call while the plugin is in an incorrect state ({:?} <= \ + {:?}). This is a bug in the validator.", + status, $expected ) } }; } -pub(crate) use assert_plugin_state_eq; -pub(crate) use assert_plugin_state_initialized; -pub(crate) use assert_plugin_state_lt; +pub(crate) use assert_plugin_state; diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index c307068..278b7f8 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -1,5 +1,6 @@ use crate::{ plugin::{ + assert_plugin_state, ext::Extension, instance::{Plugin, PluginStatus}, }, @@ -28,11 +29,7 @@ impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { impl<'a> Latency<'a> { pub fn get(&self) -> u32 { - assert!( - self.plugin.status() >= PluginStatus::Activating, - "The 'latency' extension's 'get' function can only be called while the plugin is \ - activating or active. This is a bug in the validator." - ); + assert_plugin_state!(self.plugin, state == PluginStatus::Activating); let latency = self.latency.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 42769c5..98e9358 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -2,24 +2,24 @@ use anyhow::{Context, Result}; use clap_sys::ext::params::{ - CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, + clap_param_info, clap_param_info_flags, clap_plugin_params, CLAP_EXT_PARAMS, + CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, CLAP_PARAM_IS_AUTOMATABLE_PER_KEY, CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID, CLAP_PARAM_IS_AUTOMATABLE_PER_PORT, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_HIDDEN, CLAP_PARAM_IS_MODULATABLE, CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL, CLAP_PARAM_IS_MODULATABLE_PER_KEY, CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID, CLAP_PARAM_IS_MODULATABLE_PER_PORT, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, - clap_param_info, clap_param_info_flags, clap_plugin_params, }; use clap_sys::id::clap_id; use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::collections::BTreeMap; -use std::ffi::{CStr, CString, c_void}; +use std::ffi::{c_void, CStr, CString}; use std::ops::RangeInclusive; use std::pin::Pin; use std::ptr::NonNull; use super::Extension; -use crate::plugin::assert_plugin_state_lt; +use crate::plugin::assert_plugin_state; use crate::plugin::instance::process::EventQueue; use crate::plugin::instance::{Plugin, PluginStatus}; use crate::util::{self, c_char_slice_to_string, unsafe_clap_call}; @@ -329,7 +329,7 @@ impl Params<'_> { pub fn flush(&self, input_events: &Pin>, output_events: &Pin>) { // This may only be called on the audio thread when the plugin is active. This object is the // main thread interface for the parameters extension. - assert_plugin_state_lt!(self, PluginStatus::Activated); + assert_plugin_state!(self, state < PluginStatus::Activated); let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 5309a3b..6017db7 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -11,9 +11,9 @@ use std::ptr::NonNull; use std::rc::Rc; use std::sync::Arc; +use super::assert_plugin_state; use super::ext::Extension; use super::library::{PluginLibrary, PluginMetadata}; -use super::{assert_plugin_state_eq, assert_plugin_state_initialized}; use crate::plugin::host::{CallbackTask, Host, InstanceState}; use crate::util::unsafe_clap_call; use audio_thread::PluginAudioThread; @@ -175,7 +175,7 @@ impl<'lib> Plugin<'lib> { /// this extension. Returns `None` if it does not. The plugin needs to be initialized using /// [`init()`][Self::init()] before this may be called. pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { - assert_plugin_state_initialized!(self); + assert_plugin_state!(self, state != PluginStatus::Uninitialized); let plugin = self.as_ptr(); let extension_ptr = unsafe_clap_call! { @@ -202,7 +202,7 @@ impl<'lib> Plugin<'lib> { &'a self, f: F, ) -> T { - assert_plugin_state_eq!(self, PluginStatus::Activated); + assert_plugin_state!(self, state == PluginStatus::Activated); crossbeam::scope(|s| { let unsafe_self_wrapper = PluginSendWrapper(self); @@ -242,7 +242,7 @@ impl<'lib> Plugin<'lib> { /// Initialize the plugin. This needs to be called before doing anything else. pub fn init(&self) -> Result<()> { - assert_plugin_state_eq!(self, PluginStatus::Uninitialized); + assert_plugin_state!(self, state == PluginStatus::Uninitialized); let plugin = self.as_ptr(); if unsafe_clap_call! { plugin=>init(plugin) } { @@ -262,7 +262,7 @@ impl<'lib> Plugin<'lib> { min_buffer_size: usize, max_buffer_size: usize, ) -> Result<()> { - assert_plugin_state_eq!(self, PluginStatus::Deactivated); + assert_plugin_state!(self, state == PluginStatus::Deactivated); // Apparently 0 is invalid here assert!(min_buffer_size >= 1); @@ -287,7 +287,7 @@ impl<'lib> Plugin<'lib> { /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. pub fn deactivate(&self) { - assert_plugin_state_eq!(self, PluginStatus::Activated); + assert_plugin_state!(self, state == PluginStatus::Activated); let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>deactivate(plugin) }; diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 02bc315..4c12661 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -11,13 +11,12 @@ use std::pin::Pin; use std::ptr::NonNull; use std::sync::Arc; -use crate::plugin::host::InstanceState; -use crate::util::unsafe_clap_call; - +use super::assert_plugin_state; use super::process::ProcessData; -use super::{assert_plugin_state_eq, assert_plugin_state_initialized}; use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; +use crate::plugin::host::InstanceState; +use crate::util::unsafe_clap_call; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. @@ -85,7 +84,7 @@ impl<'a> PluginAudioThread<'a> { // TODO: Remove this unused attribute once we implement audio thread extensions #[allow(unused)] pub fn get_extension>(&'a self) -> Option { - assert_plugin_state_initialized!(self); + assert_plugin_state!(self, state != PluginStatus::Uninitialized); let plugin = self.as_ptr(); let extension_ptr = @@ -105,7 +104,7 @@ impl<'a> PluginAudioThread<'a> { /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. pub fn start_processing(&self) -> Result<()> { - assert_plugin_state_eq!(self, PluginStatus::Activated); + assert_plugin_state!(self, state == PluginStatus::Activated); let plugin = self.as_ptr(); if unsafe_clap_call! { plugin=>start_processing(plugin) } { @@ -121,7 +120,7 @@ impl<'a> PluginAudioThread<'a> { /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. pub fn process(&self, process_data: &mut ProcessData) -> Result { - assert_plugin_state_eq!(self, PluginStatus::Processing); + assert_plugin_state!(self, state == PluginStatus::Processing); let plugin = self.as_ptr(); let result = process_data.with_clap_process_data(|clap_process_data| { @@ -145,7 +144,7 @@ impl<'a> PluginAudioThread<'a> { /// Reset the internal state of the plugin. pub fn reset(&self) { - assert_plugin_state_eq!(self, PluginStatus::Processing); + assert_plugin_state!(self, state >= PluginStatus::Activated); let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>reset(plugin) }; @@ -155,7 +154,7 @@ impl<'a> PluginAudioThread<'a> { /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. pub fn stop_processing(&self) { - assert_plugin_state_eq!(self, PluginStatus::Processing); + assert_plugin_state!(self, state == PluginStatus::Processing); let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>stop_processing(plugin) }; From a8f51150dc9a9f09b830705d61276bbeceead1e9 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 30 Dec 2025 18:24:20 +0400 Subject: [PATCH 016/114] fix unhandled callback error --- src/plugin/host.rs | 7 ++++--- src/tests/plugin/processing.rs | 15 +++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/plugin/host.rs b/src/plugin/host.rs index 4a0f3d7..9e3f9c8 100644 --- a/src/plugin/host.rs +++ b/src/plugin/host.rs @@ -227,10 +227,11 @@ impl InstanceState { impl Drop for Host { fn drop(&mut self) { if let Some(error) = self.callback_error.borrow_mut().take() { - log::error!( + // not an error because this can happen on a failing test + log::trace!( "The validator's host has detected a callback error but this error has not been \ - used as part of the test result. This is a clap-validator bug. The error message \ - is: {error}" + used as part of the test result. This could be a clap-validator bug. The error \ + message is: {error}" ) } } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 5c64e52..b2cb31d 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -343,11 +343,11 @@ pub fn test_process_varying_sample_rates( "Error while processing with {:.2}hz sample rate", sample_rate ))?; - - host.callback_error_check() - .context("An error occured during a host callback")?; } + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) } @@ -413,11 +413,11 @@ pub fn test_process_varying_block_sizes( "Error while processing with buffer size of {}", buffer_size ))?; - - host.callback_error_check() - .context("An error occured during a host callback")?; } + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) } @@ -664,6 +664,9 @@ pub fn test_process_reset_determinism( anyhow::bail!("Plugin output differs after reset"); } + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) } From af16313a7e617a7e354bed633f4122960aebd855 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 30 Dec 2025 18:36:53 +0400 Subject: [PATCH 017/114] add 'param-default-values' test --- src/tests/plugin.rs | 9 +++++++ src/tests/plugin/params.rs | 51 ++++++++++++++++++++++++++++++++++++++ src/tests/plugin/state.rs | 10 +++----- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index ba21c9d..d44e733 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -47,6 +47,8 @@ pub enum PluginTestCase { ParamFuzzBasic, #[strum(serialize = "param-set-wrong-namespace")] ParamSetWrongNamespace, + #[strum(serialize = "param-default-values")] + ParamDefaultValues, #[strum(serialize = "state-invalid")] StateInvalid, #[strum(serialize = "state-reproducibility-basic")] @@ -149,6 +151,10 @@ impl<'a> TestCase<'a> for PluginTestCase { a mismatching namespace ID. Asserts that the plugin's parameter values don't \ change.", ), + PluginTestCase::ParamDefaultValues => String::from( + "Asserts that the values for all parameters are set correctly to their default \ + values when the plugin is initialized.", + ), PluginTestCase::StateInvalid => String::from( "The plugin should return false when 'clap_plugin_state::load()' is called with \ an empty state.", @@ -244,6 +250,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamSetWrongNamespace => { params::test_param_set_wrong_namespace(library, plugin_id) } + PluginTestCase::ParamDefaultValues => { + params::test_param_default_values(library, plugin_id) + } PluginTestCase::StateInvalid => state::test_state_invalid(library, plugin_id), PluginTestCase::StateReproducibilityBasic => { state::test_state_reproducibility_null_cookies(library, plugin_id, false) diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 52c6f89..a48e2ac 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -418,3 +418,54 @@ pub fn test_param_set_wrong_namespace( }) } } + +pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Result { + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let params = match plugin.get_extension::() { + Some(params) => params, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + Params::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + host.handle_callbacks_once(); + + let param_infos = params + .info() + .context("Failure while fetching the plugin's parameters")?; + + for (param_id, param_info) in param_infos { + let default_value = params + .get(param_id) + .with_context(|| format!("Could not get value for parameter {param_id}"))?; + + if !param_compare_approx(default_value, param_info.default) { + anyhow::bail!( + "The default value for parameter {param_id} ('{}') is {}, but the actual \ + parameter value after initialization is {}.", + param_info.name, + param_info.default, + default_value + ); + } + } + + host.callback_error_check() + .context("An error occured during a host callback")?; + + Ok(TestStatus::Success { details: None }) +} + +pub fn param_compare_approx(actual: f64, expected: f64) -> bool { + const EPSILON: f64 = 1e-5; + (actual - expected).abs() <= EPSILON +} diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 2150cbf..9fbe9d2 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -16,6 +16,7 @@ use crate::plugin::instance::process::{ AudioBuffers, Event, EventQueue, ProcessConfig, ProcessData, }; use crate::plugin::library::PluginLibrary; +use crate::tests::plugin::params::param_compare_approx; use crate::tests::rng::{new_prng, ParamFuzzer}; use crate::tests::{TestCase, TestStatus}; @@ -680,11 +681,6 @@ pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Re } } -fn compare_approx(actual: f64, expected: f64) -> bool { - const EPSILON: f64 = 1e-5; - (actual - expected).abs() <= EPSILON -} - /// Build a string containing all different values between two sets of values. /// /// # Panics @@ -700,7 +696,7 @@ fn format_mismatching_values( .into_iter() .filter_map(|(param_id, actual_value)| { let expected_value = expected_param_values[¶m_id]; - if compare_approx(actual_value, expected_value) { + if param_compare_approx(actual_value, expected_value) { None } else { let param_name = ¶m_infos[¶m_id].name; @@ -728,7 +724,7 @@ fn compare_params_lenient( None => return false, }; - if !compare_approx(*actual_value, *expected_value) { + if !param_compare_approx(*actual_value, *expected_value) { return false; } } From 8be177afade372e910f97d0f69e077afe11a554c Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 13 Jan 2026 04:17:50 +0400 Subject: [PATCH 018/114] add `param-fuzz-bounds` and `param-fuzz-sample-accurate` tests --- src/plugin/ext/note_ports.rs | 10 +- src/plugin/ext/params.rs | 8 + src/tests/plugin.rs | 19 +++ src/tests/plugin/descriptor.rs | 2 + src/tests/plugin/params.rs | 296 ++++++++++++++++++++++++++++++--- src/tests/rng.rs | 58 +++++-- 6 files changed, 353 insertions(+), 40 deletions(-) diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 181c20e..1874867 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -1,19 +1,17 @@ //! Abstractions for interacting with the `note-ports` extension. +use super::Extension; +use crate::plugin::instance::Plugin; +use crate::util::unsafe_clap_call; use anyhow::Result; use clap_sys::ext::note_ports::{ - CLAP_EXT_NOTE_PORTS, clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, + clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS, }; use std::collections::HashSet; use std::ffi::CStr; use std::mem; use std::ptr::NonNull; -use crate::plugin::instance::Plugin; -use crate::util::unsafe_clap_call; - -use super::Extension; - /// Abstraction for the `note-ports` extension covering the main thread functionality. #[derive(Debug)] pub struct NotePorts<'a> { diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 98e9358..2b7030a 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -61,6 +61,9 @@ pub struct Param { pub flags: clap_param_info_flags, } +unsafe impl Send for Param {} +unsafe impl Sync for Param {} + impl Params<'_> { /// Used by the status assertion macros. fn status(&self) -> PluginStatus { @@ -358,4 +361,9 @@ impl Param { pub fn stepped(&self) -> bool { (self.flags & CLAP_PARAM_IS_STEPPED) != 0 } + + /// Whether this parameter is automatable. + pub fn automatable(&self) -> bool { + (self.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0 + } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index d44e733..c0132b8 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -45,6 +45,10 @@ pub enum PluginTestCase { ParamConversions, #[strum(serialize = "param-fuzz-basic")] ParamFuzzBasic, + #[strum(serialize = "param-fuzz-bounds")] + ParamFuzzBounds, + #[strum(serialize = "param-fuzz-sample-accurate")] + ParamFuzzSampleAccurate, #[strum(serialize = "param-set-wrong-namespace")] ParamSetWrongNamespace, #[strum(serialize = "param-default-values")] @@ -146,6 +150,17 @@ impl<'a> TestCase<'a> for PluginTestCase { params::FUZZ_NUM_PERMUTATIONS, params::FUZZ_RUNS_PER_PERMUTATION ), + PluginTestCase::ParamFuzzBounds => format!( + "The exact same test as {}, but this time the parameter values are snapped to the \ + minimum and maximum values.", + PluginTestCase::ParamFuzzBasic + ), + PluginTestCase::ParamFuzzSampleAccurate => String::from( + "Generates and sets parameter values in a sample-accurate fashion while \ + processing audio, generating them at fixed intervals (1, 100, 1000 samples). The \ + plugin passes the test if it doesn't produce any infinite or NaN values, and \ + doesn't crash.", + ), PluginTestCase::ParamSetWrongNamespace => String::from( "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ a mismatching namespace ID. Asserts that the plugin's parameter values don't \ @@ -247,6 +262,10 @@ impl<'a> TestCase<'a> for PluginTestCase { } PluginTestCase::ParamConversions => params::test_param_conversions(library, plugin_id), PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id), + PluginTestCase::ParamFuzzBounds => params::test_param_fuzz_bounds(library, plugin_id), + PluginTestCase::ParamFuzzSampleAccurate => { + params::test_param_fuzz_sample_accurate(library, plugin_id) + } PluginTestCase::ParamSetWrongNamespace => { params::test_param_set_wrong_namespace(library, plugin_id) } diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 98ceab6..468794b 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -50,6 +50,8 @@ pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result Result { + /// SAFETY: + /// Assumes that extension 'T' is a repr(C) struct with function pointers only. unsafe fn check_extension(plugin: &Plugin<'_>, extension: &CStr) -> Result<()> { let extension_ptr = unsafe_clap_call! { plugin.as_ptr()=>get_extension(plugin.as_ptr(), extension.as_ptr()) }; if extension_ptr.is_null() { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index a48e2ac..1c3fddc 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; -use crate::plugin::ext::params::Params; +use crate::plugin::ext::params::{ParamInfo, Params}; use crate::plugin::ext::Extension; use crate::plugin::host::Host; use crate::plugin::instance::process::{AudioBuffers, Event, ProcessData}; @@ -41,6 +41,23 @@ struct ParamValue<'a> { value: f64, } +impl<'a> ParamValue<'a> { + fn from_events(events: Option>, param_infos: &'a ParamInfo) -> Vec { + events + .into_iter() + .flatten() + .map(|event| match event { + Event::ParamValue(event) => ParamValue { + id: event.param_id, + name: ¶m_infos[&event.param_id].name, + value: event.value, + }, + _ => panic!("Unexpected event type. This is a clap-validator bug."), + }) + .collect() + } +} + /// The test for `ProcessingTest::ParamConversions`. pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); @@ -281,25 +298,146 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result PluginTestCase::ParamFuzzBasic .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; - let create_param_values_vec = |events: Option>| match events { - Some(events) => events - .into_iter() - .map(|event| match event { - Event::ParamValue(event) => ParamValue { - id: event.param_id, - name: ¶m_infos[&event.param_id].name, - value: event.value, - }, - _ => panic!("Unexpected event type. This is a clap-validator bug."), - }) - .collect(), - None => Vec::new(), - }; - let previous_param_values: Vec = create_param_values_vec(previous_events); - let current_param_values: Vec = create_param_values_vec(current_events); + serde_json::to_writer_pretty( + previous_param_values_file, + &ParamValue::from_events(previous_events, ¶m_infos), + )?; + serde_json::to_writer_pretty( + current_param_values_file, + &ParamValue::from_events(current_events, ¶m_infos), + )?; + + // This is a bit weird and there may be a better way to do this, but we only want to + // write the parameter values if we know the run has failed, and we only know the + // filename after writing those values to a file + return Err(run_result + .with_context(|| { + format!( + "Invalid output detected in parameter value permutation {} of {} ('{}' \ + and '{}' contain the current and previous parameter values)", + permutation_no, + FUZZ_NUM_PERMUTATIONS, + current_param_values_file_path.display(), + previous_param_values_file_path.display(), + ) + }) + .unwrap_err()); + } + + std::mem::swap(&mut previous_events, &mut current_events); + } + + // `ProcessingTest::run()` already handled callbacks for us + host.callback_error_check() + .context("An error occured during a host callback")?; + + Ok(TestStatus::Success { details: None }) +} + +/// The test for `ProcessingTest::ParamFuzzBounds`. +pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + // Both audio and note ports are optional + let audio_ports = plugin.get_extension::(); + let note_ports = plugin.get_extension::(); + let params = match plugin.get_extension::() { + Some(params) => params, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + Params::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + host.handle_callbacks_once(); + + let audio_ports_config = audio_ports + .map(|ports| ports.config()) + .transpose() + .context("Could not fetch the plugin's audio port config")? + .unwrap_or_default(); + let note_ports_config = note_ports + .map(|ports| ports.config()) + .transpose() + .context("Could not fetch the plugin's note port config")? + // Don't try to generate notes if the plugin supports the note ports extension but doesn't + // actually have any note ports. JUCE does this. + .filter(|config| !config.inputs.is_empty()); + let param_infos = params + .info() + .context("Could not fetch the plugin's parameters")?; + + // For each set of runs we'll generate new parameter values, and if the plugin supports notes + // we'll also generate note events. + let param_fuzzer = ParamFuzzer::new(¶m_infos).with_snap_to_bounds(); + let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + + // We'll keep track of the current and the previous set of parameter value so we can write them + // to a file if the test fails + let mut current_events: Option>; + let mut previous_events: Option> = None; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); - serde_json::to_writer_pretty(previous_param_values_file, &previous_param_values)?; - serde_json::to_writer_pretty(current_param_values_file, ¤t_param_values)?; + for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { + current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); + + let mut have_set_parameters = false; + let run_result = run_simple( + &plugin, + &mut process_data, + FUZZ_RUNS_PER_PERMUTATION, + |process_data| { + if !have_set_parameters { + process_data + .input_events + .add_events(current_events.clone().unwrap()); + have_set_parameters = true; + } + + // Audio and MIDI/note events are randomized in accordance to what the plugin + // supports + if let Some(note_event_rng) = note_event_rng.as_mut() { + // This includes a sort if `random_param_set_events` also contained a queue + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + )?; + } + process_data.buffers.randomize(&mut prng); + + Ok(()) + }, + ); + + // If the run failed we'll want to write the parameter values to a file first + if run_result.is_err() { + let (previous_param_values_file_path, previous_param_values_file) = + PluginTestCase::ParamFuzzBounds + .temporary_file(plugin_id, PREVIOUS_PARAM_VALUES_FILE_NAME)?; + let (current_param_values_file_path, current_param_values_file) = + PluginTestCase::ParamFuzzBounds + .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; + + serde_json::to_writer_pretty( + previous_param_values_file, + &ParamValue::from_events(previous_events, ¶m_infos), + )?; + + serde_json::to_writer_pretty( + current_param_values_file, + &ParamValue::from_events(current_events, ¶m_infos), + )?; // This is a bit weird and there may be a better way to do this, but we only want to // write the parameter values if we know the run has failed, and we only know the @@ -328,6 +466,125 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result Ok(TestStatus::Success { details: None }) } +/// The test for `ProcessingTest::ParamFuzzSampleAccurate`. +pub fn test_param_fuzz_sample_accurate( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + const INTERVALS: &[u32] = &[1000, 100, 1]; + + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + // Both audio and note ports are optional + let audio_ports = plugin.get_extension::(); + let note_ports = plugin.get_extension::(); + let params = match plugin.get_extension::() { + Some(params) => params, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + Params::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + host.handle_callbacks_once(); + + let audio_ports_config = audio_ports + .map(|ports| ports.config()) + .transpose() + .context("Could not fetch the plugin's audio port config")? + .unwrap_or_default(); + let note_ports_config = note_ports + .map(|ports| ports.config()) + .transpose() + .context("Could not fetch the plugin's note port config")? + // Don't try to generate notes if the plugin supports the note ports extension but doesn't + // actually have any note ports. JUCE does this. + .filter(|config| !config.inputs.is_empty()); + let param_infos = params + .info() + .context("Could not fetch the plugin's parameters")?; + + // For each set of runs we'll generate new parameter values, and if the plugin supports notes + // we'll also generate note events. + let param_fuzzer = ParamFuzzer::new(¶m_infos); + let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut current_events: Option> = None; + + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); + + for &interval in INTERVALS { + let mut current_sample = 0; + let run_result = run_simple(&plugin, &mut process_data, 5, |process_data| { + while current_sample < BUFFER_SIZE as u32 { + let events: Vec = param_fuzzer + .randomize_params_at(&mut prng, current_sample) + .collect(); + + process_data.input_events.add_events(events.clone()); + current_events = Some(events); + current_sample += interval; + } + + // Audio and MIDI/note events are randomized in accordance to what the plugin + // supports + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + )?; + } + + process_data.buffers.randomize(&mut prng); + current_sample -= BUFFER_SIZE as u32; + + Ok(()) + }); + + // If the run failed we'll want to write the parameter values to a file first + if run_result.is_err() { + let (current_param_values_file_path, current_param_values_file) = + PluginTestCase::ParamFuzzSampleAccurate + .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; + + serde_json::to_writer_pretty( + current_param_values_file, + &ParamValue::from_events(current_events, ¶m_infos), + )?; + + // This is a bit weird and there may be a better way to do this, but we only want to + // write the parameter values if we know the run has failed, and we only know the + // filename after writing those values to a file + return Err(run_result + .with_context(|| { + format!( + "Invalid output detected when automating parameters with interval of {} \ + samples ('{}' contains the current parameter values)", + interval, + current_param_values_file_path.display(), + ) + }) + .unwrap_err()); + } + } + + // `ProcessingTest::run()` already handled callbacks for us + host.callback_error_check() + .context("An error occured during a host callback")?; + + Ok(TestStatus::Success { details: None }) +} + /// The test for `ProcessingTest::ParamSetWrongNamespace`. pub fn test_param_set_wrong_namespace( library: &PluginLibrary, @@ -419,6 +676,7 @@ pub fn test_param_set_wrong_namespace( } } +/// The test for `ProcessingTest::ParamDefaultValues`. pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Result { let host = Host::new(); let plugin = library diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 8f8714b..c9ce30d 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -3,9 +3,10 @@ use anyhow::{Context, Result}; use clap_sys::events::{ clap_event_header, clap_event_midi, clap_event_note, clap_event_note_expression, - clap_event_param_value, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_CHOKE, - CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_VALUE, - CLAP_NOTE_EXPRESSION_PRESSURE, CLAP_NOTE_EXPRESSION_TUNING, CLAP_NOTE_EXPRESSION_VOLUME, + clap_event_param_value, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_IS_LIVE, CLAP_EVENT_MIDI, + CLAP_EVENT_NOTE_CHOKE, CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, + CLAP_EVENT_PARAM_VALUE, CLAP_NOTE_EXPRESSION_PRESSURE, CLAP_NOTE_EXPRESSION_TUNING, + CLAP_NOTE_EXPRESSION_VOLUME, }; use clap_sys::ext::note_ports::{ CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, @@ -47,7 +48,9 @@ pub struct NoteGenerator { /// A helper to generate random parameter automation and modulation events in a couple different /// ways to stress test a plugin's parameter handling. pub struct ParamFuzzer<'a> { - config: &'a ParamInfo, + params: &'a ParamInfo, + notes: NotePortConfig, + snap_to_bounds: bool, } /// The description of an active note in the [`NoteGenerator`]. @@ -569,22 +572,35 @@ impl NoteEventType { impl<'a> ParamFuzzer<'a> { /// Create a new parameter fuzzer. This ignores parameters that are readonly or hidden. - pub fn new(config: &'a ParamInfo) -> Self { - ParamFuzzer { config } + pub fn new(params: &'a ParamInfo) -> Self { + ParamFuzzer { + params, + notes: NotePortConfig::default(), + snap_to_bounds: false, + } + } + + pub fn with_note_config(mut self, notes: NotePortConfig) -> Self { + self.notes = notes; + self + } + + pub fn with_snap_to_bounds(mut self) -> Self { + self.snap_to_bounds = true; + self } // TODO: Modulation and per-{key,channel,port,note_id} modulation // TODO: Variants similar to `fill_event_queue` from `NoteGenerator` - // TODO: A variant that snaps to the minimum or maximum value - /// Randomize all parameters at a certain sample index using **automation**, returning an + /// Randomize _all_ parameters at a certain sample index using **automation**, returning an /// iterator yielding automation events for all parameters. pub fn randomize_params_at( &'a self, prng: &'a mut Pcg32, time_offset: u32, ) -> impl Iterator + 'a { - self.config + self.params .iter() .filter_map(move |(param_id, param_info)| { // We can send parameter changes for parameters that are not automatable: @@ -594,12 +610,20 @@ impl<'a> ParamFuzzer<'a> { return None; } - let value = if param_info.stepped() { - // We already confirmed that the range starts and ends in an integer when - // constructing the parameter info - prng.gen_range(param_info.range.clone()).round() + let value = if self.snap_to_bounds { + if prng.gen_bool(0.5) { + *param_info.range.start() + } else { + *param_info.range.end() + } } else { - prng.gen_range(param_info.range.clone()) + if param_info.stepped() { + // We already confirmed that the range starts and ends in an integer when + // constructing the parameter info + prng.gen_range(param_info.range.clone()).round() + } else { + prng.gen_range(param_info.range.clone()) + } }; Some(Event::ParamValue(clap_event_param_value { @@ -608,7 +632,11 @@ impl<'a> ParamFuzzer<'a> { time: time_offset, space_id: CLAP_CORE_EVENT_SPACE_ID, type_: CLAP_EVENT_PARAM_VALUE, - flags: 0, + flags: if param_info.automatable() { + 0 + } else { + CLAP_EVENT_IS_LIVE + }, }, param_id: *param_id, cookie: param_info.cookie, From 8e18f9e49840b234c70ded892d3c4f27d45051a8 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 13 Jan 2026 15:27:06 +0400 Subject: [PATCH 019/114] add 'audio-ports-config' and 'audio-ports-config-info' extension support; add 'process-audio-*-config' tests --- src/plugin/ext.rs | 1 + src/plugin/ext/audio_ports.rs | 32 ++- src/plugin/ext/audio_ports_config.rs | 117 +++++++++ src/tests/plugin.rs | 71 ++++-- src/tests/plugin/processing.rs | 348 +++++++++++++++++---------- 5 files changed, 420 insertions(+), 149 deletions(-) create mode 100644 src/plugin/ext/audio_ports_config.rs diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index 325c410..e562f5d 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -6,6 +6,7 @@ use std::ffi::CStr; use std::ptr::NonNull; pub mod audio_ports; +pub mod audio_ports_config; pub mod latency; pub mod note_ports; pub mod params; diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 714d7d2..5f30b93 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -3,8 +3,8 @@ use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::{ - clap_audio_port_info, clap_plugin_audio_ports, CLAP_EXT_AUDIO_PORTS, CLAP_PORT_MONO, - CLAP_PORT_STEREO, + clap_audio_port_info, clap_plugin_audio_ports, CLAP_AUDIO_PORT_IS_MAIN, CLAP_EXT_AUDIO_PORTS, + CLAP_PORT_MONO, CLAP_PORT_STEREO, }; use clap_sys::ext::surround::CLAP_PORT_SURROUND; use clap_sys::id::CLAP_INVALID_ID; @@ -36,6 +36,9 @@ pub struct AudioPortConfig { /// The configuration for a single audio port. #[derive(Debug)] pub struct AudioPort { + /// Whether this is the main audio port. + pub is_main: bool, + /// The number of channels for an audio port. pub num_channels: u32, /// The index if the output/input port this input/output port should be connected to. This is @@ -97,13 +100,24 @@ impl AudioPorts<'_> { // We'll convert these stable IDs to vector indices later if input_stable_index_pairs.contains_key(&info.id) { anyhow::bail!( - "The stable ID of input audio port {i} ({}) is a duplicate.", + "The stable ID of input audio port {i} (id={}) is a duplicate.", info.id ); } input_stable_index_pairs.insert(info.id, (i as usize, info.in_place_pair)); + // Check is main + let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; + if is_main && i != 0 { + anyhow::bail!( + "Input audio port {i} (id={}) is marked as main, but it is not the first port \ + in the list.", + info.id + ); + } + config.inputs.push(AudioPort { + is_main, num_channels: info.channel_count, // These are reconstructed from `input_stable_index_pairs` and // `output_stable_index_pairs` later @@ -130,13 +144,23 @@ impl AudioPorts<'_> { if output_stable_index_pairs.contains_key(&info.id) { anyhow::bail!( - "The stable ID of output audio port {i} ({}) is a duplicate.", + "The stable ID of output audio port {i} (id={}) is a duplicate.", info.id ); } output_stable_index_pairs.insert(info.id, (i as usize, info.in_place_pair)); + let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; + if is_main && i != 0 { + anyhow::bail!( + "Output audio port {i} (id={}) is marked as main, but it is not the first \ + port in the list.", + info.id + ); + } + config.outputs.push(AudioPort { + is_main, num_channels: info.channel_count, in_place_pair_idx: None, }); diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs new file mode 100644 index 0000000..cb7dd26 --- /dev/null +++ b/src/plugin/ext/audio_ports_config.rs @@ -0,0 +1,117 @@ +use crate::{ + plugin::{ext::Extension, instance::Plugin}, + util::{c_char_slice_to_string, clap_call, unsafe_clap_call}, +}; +use anyhow::Result; +use clap_sys::{ + ext::audio_ports_config::{ + clap_audio_ports_config, clap_plugin_audio_ports_config, + clap_plugin_audio_ports_config_info, CLAP_EXT_AUDIO_PORTS_CONFIG, + CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, + }, + id::clap_id, +}; +use std::{ffi::CStr, mem::zeroed, ptr::NonNull}; + +#[derive(Debug)] +pub struct AudioPortsConfig<'a> { + plugin: &'a Plugin<'a>, + audio_ports_config: NonNull, +} + +#[derive(Debug)] +pub struct AudioPortsConfigInfo<'a> { + plugin: &'a Plugin<'a>, + audio_ports_config_info: NonNull, +} + +/// A configuration +#[derive(Debug, Clone)] +pub struct AudioPortsConfigConfig { + pub id: clap_id, + pub name: String, + + pub input_port_count: u32, + pub output_port_count: u32, + + pub main_input_channel_count: Option, + pub main_output_channel_count: Option, +} + +impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfig<'a> { + const EXTENSION_ID: &'static CStr = CLAP_EXT_AUDIO_PORTS_CONFIG; + + type Struct = clap_plugin_audio_ports_config; + + fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + audio_ports_config: extension_struct, + } + } +} + +impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfigInfo<'a> { + const EXTENSION_ID: &'static CStr = CLAP_EXT_AUDIO_PORTS_CONFIG_INFO; + + type Struct = clap_plugin_audio_ports_config_info; + + fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + audio_ports_config_info: extension_struct, + } + } +} + +impl AudioPortsConfig<'_> { + pub fn enumerate(&self) -> Result> { + let audio_ports_config = self.audio_ports_config.as_ptr(); + let plugin = self.plugin.as_ptr(); + let count = unsafe_clap_call! { audio_ports_config=>count(plugin) }; + + (0..count) + .map(|i| unsafe { + let mut dst = clap_audio_ports_config { ..zeroed() }; + let result = clap_call! { audio_ports_config=>get(plugin, i, &mut dst) }; + if !result { + anyhow::bail!("audio_ports_config::get({}) returned false", i); + } + + Ok(AudioPortsConfigConfig { + id: dst.id, + name: c_char_slice_to_string(&dst.name)?, + input_port_count: dst.input_port_count, + output_port_count: dst.output_port_count, + main_input_channel_count: dst + .has_main_input + .then_some(dst.main_input_channel_count), + main_output_channel_count: dst + .has_main_output + .then_some(dst.main_output_channel_count), + }) + }) + .collect() + } + + pub fn select(&self, config_id: clap_id) -> Result<()> { + let audio_ports_config = self.audio_ports_config.as_ptr(); + let plugin = self.plugin.as_ptr(); + let result = unsafe_clap_call! { audio_ports_config=>select(plugin, config_id) }; + if !result { + anyhow::bail!("audio_ports_config::select() returned false"); + } + + Ok(()) + } +} + +impl AudioPortsConfigInfo<'_> { + pub fn current(&self) -> clap_id { + let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe_clap_call! { audio_ports_config_info=>current_config(plugin) } + } + + // TODO: +} diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index c0132b8..7d20d6e 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -1,7 +1,13 @@ //! Tests for individual plugin instances. use super::TestCase; -use crate::{plugin::library::PluginLibrary, tests::TestStatus}; +use crate::{ + plugin::{ + ext::{audio_ports_config::AudioPortsConfig, Extension}, + library::PluginLibrary, + }, + tests::TestStatus, +}; use anyhow::Result; use clap::ValueEnum; use std::process::Command; @@ -27,8 +33,14 @@ pub enum PluginTestCase { ProcessAudioOutOfPlaceBasic, #[strum(serialize = "process-audio-in-place-basic")] ProcessAudioInPlaceBasic, + #[strum(serialize = "process-audio-out-of-place-layouts")] + ProcessAudioOutOfPlaceConfig, + #[strum(serialize = "process-audio-in-place-layouts")] + ProcessAudioInPlaceConfig, #[strum(serialize = "process-audio-constant-mask")] ProcessAudioConstantMask, + #[strum(serialize = "process-audio-reset-determinism")] + ProcessAudioResetDeterminism, #[strum(serialize = "process-note-out-of-place-basic")] ProcessNoteOutOfPlaceBasic, #[strum(serialize = "process-note-inconsistent")] @@ -39,8 +51,6 @@ pub enum PluginTestCase { ProcessVaryingBlockSizes, #[strum(serialize = "process-random-block-sizes")] ProcessRandomBlockSizes, - #[strum(serialize = "process-reset-determinism")] - ProcessResetDeterminism, #[strum(serialize = "param-conversions")] ParamConversions, #[strum(serialize = "param-fuzz-basic")] @@ -55,6 +65,8 @@ pub enum PluginTestCase { ParamDefaultValues, #[strum(serialize = "state-invalid")] StateInvalid, + #[strum(serialize = "state-random-garbage")] + StateRandomGarbage, #[strum(serialize = "state-reproducibility-basic")] StateReproducibilityBasic, #[strum(serialize = "state-reproducibility-null-cookies")] @@ -63,8 +75,6 @@ pub enum PluginTestCase { StateReproducibilityFlush, #[strum(serialize = "state-buffered-streams")] StateBufferedStreams, - #[strum(serialize = "state-random-garbage")] - StateRandomGarbage, } impl<'a> TestCase<'a> for PluginTestCase { @@ -98,6 +108,18 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses in-place audio processing for buses that support it.", ), + PluginTestCase::ProcessAudioOutOfPlaceConfig => format!( + "Performs the same test as {}, but this time it tries all available port \ + configurations exposed via the '{}' extension.", + PluginTestCase::ProcessAudioOutOfPlaceBasic, + AudioPortsConfig::EXTENSION_ID.to_str().unwrap() + ), + PluginTestCase::ProcessAudioInPlaceConfig => format!( + "Performs the same test as {}, but this time it tries all available port \ + configurations exposed via the '{}' extension.", + PluginTestCase::ProcessAudioInPlaceBasic, + AudioPortsConfig::EXTENSION_ID.to_str().unwrap() + ), PluginTestCase::ProcessAudioConstantMask => String::from( "Processes random audio through the plugin with its default parameter values \ while setting the constant mask on silent blocks, and tests whether the output \ @@ -133,7 +155,7 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses out-of-place audio processing.", ), - PluginTestCase::ProcessResetDeterminism => String::from( + PluginTestCase::ProcessAudioResetDeterminism => String::from( "Asserts that resetting the plugin via 'clap_plugin::reset()' and via \ re-activation results in deterministic output when processing the same audio and \ events again.", @@ -156,10 +178,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamFuzzBasic ), PluginTestCase::ParamFuzzSampleAccurate => String::from( - "Generates and sets parameter values in a sample-accurate fashion while \ - processing audio, generating them at fixed intervals (1, 100, 1000 samples). The \ - plugin passes the test if it doesn't produce any infinite or NaN values, and \ - doesn't crash.", + "Sets parameter values in a sample-accurate fashion while processing audio, \ + generating them at fixed intervals (1, 100, 1000 samples). The plugin passes the \ + test if it doesn't produce any infinite or NaN values, and doesn't crash.", ), PluginTestCase::ParamSetWrongNamespace => String::from( "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ @@ -200,8 +221,8 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::StateReproducibilityBasic ), PluginTestCase::StateRandomGarbage => String::from( - "Loads 10 chunks of random bytes via 'clap_plugin_state::load()' and asserts that \ - the plugin doesn't crash.", + "Loads 10x1MB chunks of random bytes via 'clap_plugin_state::load()' and asserts \ + that the plugin doesn't crash.", ), } } @@ -234,19 +255,28 @@ impl<'a> TestCase<'a> for PluginTestCase { descriptor::test_features_duplicates(library, plugin_id) } PluginTestCase::ProcessAudioOutOfPlaceBasic => { - processing::test_process_audio_out_of_place_basic(library, plugin_id) + processing::test_process_audio_basic(library, plugin_id, false) } PluginTestCase::ProcessAudioInPlaceBasic => { - processing::test_process_audio_in_place_basic(library, plugin_id) + processing::test_process_audio_basic(library, plugin_id, true) + } + PluginTestCase::ProcessAudioOutOfPlaceConfig => { + processing::test_process_audio_config(library, plugin_id, false) + } + PluginTestCase::ProcessAudioInPlaceConfig => { + processing::test_process_audio_config(library, plugin_id, true) } PluginTestCase::ProcessAudioConstantMask => { processing::test_process_audio_constant_mask(library, plugin_id) } + PluginTestCase::ProcessAudioResetDeterminism => { + processing::test_process_audio_reset_determinism(library, plugin_id) + } PluginTestCase::ProcessNoteOutOfPlaceBasic => { - processing::test_process_note_out_of_place_basic(library, plugin_id) + processing::test_process_note_out_of_place(library, plugin_id, true) } PluginTestCase::ProcessNoteInconsistent => { - processing::test_process_note_inconsistent(library, plugin_id) + processing::test_process_note_out_of_place(library, plugin_id, false) } PluginTestCase::ProcessVaryingSampleRates => { processing::test_process_varying_sample_rates(library, plugin_id) @@ -257,9 +287,6 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessRandomBlockSizes => { processing::test_process_random_block_sizes(library, plugin_id) } - PluginTestCase::ProcessResetDeterminism => { - processing::test_process_reset_determinism(library, plugin_id) - } PluginTestCase::ParamConversions => params::test_param_conversions(library, plugin_id), PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id), PluginTestCase::ParamFuzzBounds => params::test_param_fuzz_bounds(library, plugin_id), @@ -273,6 +300,9 @@ impl<'a> TestCase<'a> for PluginTestCase { params::test_param_default_values(library, plugin_id) } PluginTestCase::StateInvalid => state::test_state_invalid(library, plugin_id), + PluginTestCase::StateRandomGarbage => { + state::test_state_random_garbage(library, plugin_id) + } PluginTestCase::StateReproducibilityBasic => { state::test_state_reproducibility_null_cookies(library, plugin_id, false) } @@ -285,9 +315,6 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::StateBufferedStreams => { state::test_state_buffered_streams(library, plugin_id) } - PluginTestCase::StateRandomGarbage => { - state::test_state_random_garbage(library, plugin_id) - } } } } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index b2cb31d..829a552 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -1,6 +1,7 @@ //! Contains most of the boilerplate around testing audio processing. use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; +use crate::plugin::ext::audio_ports_config::{AudioPortsConfig, AudioPortsConfigInfo}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; @@ -14,6 +15,8 @@ use crate::tests::TestStatus; use anyhow::{Context, Result}; use rand::Rng; +const BUFFER_SIZE: usize = 512; + pub fn run_simple( plugin: &Plugin, data: &mut ProcessData, @@ -57,10 +60,11 @@ where }) } -/// The test for `PluginTestCase::ProcessAudioOutOfPlaceBasic`. -pub fn test_process_audio_out_of_place_basic( +/// The test for `PluginTestCase::ProcessAudioOutOfPlaceBasic` and `PluginTestCase::ProcessAudioInPlaceBasic`. +pub fn test_process_audio_basic( library: &PluginLibrary, plugin_id: &str, + in_place: bool, ) -> Result { let mut prng = new_prng(); @@ -84,59 +88,12 @@ pub fn test_process_audio_out_of_place_basic( } }; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - Ok(()) - })?; - - // The `Host` contains built-in thread safety checks - host.callback_error_check() - .context("An error occured during a host callback")?; - Ok(TestStatus::Success { details: None }) -} - -/// The test for `PluginTestCase::ProcessAudioInPlaceBasic`. -pub fn test_process_audio_in_place_basic( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { - let mut prng = new_prng(); - - let host = Host::new(); - let plugin = library - .create_plugin(plugin_id, host.clone()) - .context("Could not create the plugin instance")?; - plugin.init().context("Error during initialization")?; - - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - AudioPorts::EXTENSION_ID.to_str().unwrap(), - )), - }); - } + let mut audio_buffers = if in_place { + AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE) + } else { + AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE) }; - if audio_ports_config - .inputs - .iter() - .all(|x| x.in_place_pair_idx.is_none()) - { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not have any in-place audio port pairs.", - )), - }); - } - - let mut audio_buffers = AudioBuffers::new_in_place_f32(&audio_ports_config, 512); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); @@ -149,12 +106,13 @@ pub fn test_process_audio_in_place_basic( Ok(TestStatus::Success { details: None }) } -/// The test for `PluginTestCase::ProcessNoteOutOfPlaceBasic`. This test is very similar to +/// The test for `PluginTestCase::ProcessNoteOutOfPlaceBasic` and `PluginTestCase::ProcessNoteInconsistent`. This test is very similar to /// `ProcessAudioOutOfPlaceBasic`, but it requires the `note-ports` extension, sends notes and/or /// MIDI to the plugin, and doesn't require the `audio-ports` extension. -pub fn test_process_note_out_of_place_basic( +pub fn test_process_note_out_of_place( library: &PluginLibrary, plugin_id: &str, + consistent: bool, ) -> Result { let mut prng = new_prng(); @@ -171,6 +129,7 @@ pub fn test_process_note_out_of_place_basic( .context("Error while querying 'audio-ports' IO configuration")?, None => AudioPortConfig::default(), }; + let note_ports_config = match plugin.get_extension::() { Some(note_ports) => note_ports .config() @@ -184,6 +143,7 @@ pub fn test_process_note_out_of_place_basic( }); } }; + if note_ports_config.inputs.is_empty() { return Ok(TestStatus::Skipped { details: Some(format!( @@ -197,75 +157,13 @@ pub fn test_process_note_out_of_place_basic( // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(note_ports_config); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - )?; - Ok(()) - })?; - - host.callback_error_check() - .context("An error occured during a host callback")?; - Ok(TestStatus::Success { details: None }) -} - -/// The test for `PluginTestCase::ProcessNoteInconsistent`. This is the same test as -/// `ProcessAudioOutOfPlaceBasic`, but without requiring matched note on/off pairs and similar -/// invariants -pub fn test_process_note_inconsistent( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { - let mut prng = new_prng(); - - let host = Host::new(); - let plugin = library - .create_plugin(plugin_id, host.clone()) - .context("Could not create the plugin instance")?; - plugin.init().context("Error during initialization")?; - - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => AudioPortConfig::default(), - }; - let note_port_config = match plugin.get_extension::() { - Some(note_ports) => note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - NotePorts::EXTENSION_ID.to_str().unwrap(), - )), - }); - } - }; - if note_port_config.inputs.is_empty() { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin implements the '{}' extension but it does not have any input note \ - ports.", - NotePorts::EXTENSION_ID.to_str().unwrap() - )), - }); + if !consistent { + note_event_rng = note_event_rng.with_inconsistent_events(); } - host.handle_callbacks_once(); - - // This RNG (Random Note Generator) allows generates mismatching events - let mut note_event_rng = NoteGenerator::new(note_port_config).with_inconsistent_events(); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - // TODO: Use in-place processing for this test run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); note_event_rng.fill_event_queue( @@ -510,7 +408,7 @@ pub fn test_process_audio_constant_mask( } }; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut original_buffers = audio_buffers.clone(); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -584,7 +482,7 @@ pub fn test_process_audio_constant_mask( } /// The test for `PluginTestCase::ProcessResetDeterminism`. -pub fn test_process_reset_determinism( +pub fn test_process_audio_reset_determinism( library: &PluginLibrary, plugin_id: &str, ) -> Result { @@ -613,7 +511,7 @@ pub fn test_process_reset_determinism( let mut note_event_rng = note_ports_config.map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32( &audio_ports_config, - 4096, /* we do it in one block to simplify the test */ + BUFFER_SIZE * 8, /* we do it in one block to simplify the test */ ); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); let mut curr_iter = 0; @@ -670,6 +568,210 @@ pub fn test_process_reset_determinism( Ok(TestStatus::Success { details: None }) } +/// The test for `PluginTestCase::ProcessAudioOutOfPlaceConfig` and `PluginTestCase::ProcessAudioInPlaceConfig`. +pub fn test_process_audio_config( + library: &PluginLibrary, + plugin_id: &str, + in_place: bool, +) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports = match plugin.get_extension::() { + Some(audio_ports) => audio_ports, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let audio_ports_config_info = plugin.get_extension::(); + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports_config) => audio_ports_config, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPortsConfig::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => Some( + note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + ), + None => None, + }; + + for config_audio_ports_config in audio_ports_config + .enumerate() + .context("Could not enumerate audio port configurations")? + { + audio_ports_config + .select(config_audio_ports_config.id) + .with_context(|| { + format!( + "Could not select audio port configuration '{}' ({})", + config_audio_ports_config.name, config_audio_ports_config.id, + ) + })?; + + let config_audio_ports = audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?; + + // Check that the audio-ports-config info matches the actual audio-ports config + { + let main_input_channels = config_audio_ports + .inputs + .iter() + .next() + .filter(|x| x.is_main) + .map(|x| x.num_channels); + + let main_output_channels = config_audio_ports + .outputs + .iter() + .next() + .filter(|x| x.is_main) + .map(|x| x.num_channels); + + anyhow::ensure!( + config_audio_ports.inputs.len() as u32 + == config_audio_ports_config.input_port_count, + "The number of input audio ports for configuration '{}' ({}) does not match the \ + number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + config_audio_ports_config.input_port_count, + config_audio_ports.inputs.len() as u32, + ); + + anyhow::ensure!( + config_audio_ports.outputs.len() as u32 + == config_audio_ports_config.output_port_count, + "The number of output audio ports for configuration '{}' ({}) does not match the \ + number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + config_audio_ports_config.output_port_count, + config_audio_ports.outputs.len() as u32, + ); + + match ( + main_input_channels, + config_audio_ports_config.main_input_channel_count, + ) { + (None, None) => {} + (Some(a), Some(b)) => anyhow::ensure!( + a == b, + "The number of channels in the main input port for the '{}' configuration \ + info ({}) does not match the number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + b, + a, + ), + (None, Some(_)) => { + anyhow::bail!( + "The configuration '{}' reports that a main input port exists, but \ + 'audio-ports' does not.", + config_audio_ports_config.name, + ) + } + (Some(_), None) => anyhow::bail!( + "The configuration '{}' reports that main input port does not exist, but \ + according to 'audio-ports' it does.", + config_audio_ports_config.name, + ), + } + + match ( + main_output_channels, + config_audio_ports_config.main_output_channel_count, + ) { + (None, None) => {} + (Some(a), Some(b)) => anyhow::ensure!( + a == b, + "The number of channels in the main output port for the '{}' configuration \ + info ({}) does not match the number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + b, + a, + ), + (None, Some(_)) => { + anyhow::bail!( + "The configuration '{}' reports that a main output port exists, but \ + 'audio-ports' does not.", + config_audio_ports_config.name, + ) + } + (Some(_), None) => anyhow::bail!( + "The configuration '{}' reports that main output port does not exist, but \ + according to 'audio-ports' it does.", + config_audio_ports_config.name, + ), + } + } + + // Check that the audio-ports-config-info matches the current config + if let Some(audio_ports_config_info) = &audio_ports_config_info { + anyhow::ensure!( + audio_ports_config_info.current() == config_audio_ports_config.id, + "The current configuration ID reported by 'audio-ports-config-info' ({}) does not \ + match the last selected configuration ID ({})", + audio_ports_config_info.current(), + config_audio_ports_config.id, + ); + + // TODO: check info + } + + let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let mut audio_buffers = if in_place { + AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE) + } else { + AudioBuffers::new_out_of_place_f32(&config_audio_ports, BUFFER_SIZE) + }; + + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); + + if let Some(note_event_rng) = note_event_rng.as_mut() { + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + )?; + } + + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with IO configuration '{}' ({})", + config_audio_ports_config.name, config_audio_ports_config.id, + ) + })?; + } + + // The `Host` contains built-in thread safety checks + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) +} + /// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. From 296c5b10bc42523182fddd23f38317c7dbb5ad3e Mon Sep 17 00:00:00 2001 From: Quant1um Date: Tue, 13 Jan 2026 15:43:51 +0400 Subject: [PATCH 020/114] fix & refactor some tests --- src/tests/plugin.rs | 26 ++++---- src/tests/plugin/descriptor.rs | 30 +++++---- src/tests/plugin/state.rs | 110 +++++++++++++++++---------------- 3 files changed, 84 insertions(+), 82 deletions(-) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 7d20d6e..5f584b1 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -63,10 +63,10 @@ pub enum PluginTestCase { ParamSetWrongNamespace, #[strum(serialize = "param-default-values")] ParamDefaultValues, - #[strum(serialize = "state-invalid")] - StateInvalid, - #[strum(serialize = "state-random-garbage")] - StateRandomGarbage, + #[strum(serialize = "state-invalid-empty")] + StateInvalidEmpty, + #[strum(serialize = "state-invalid-random")] + StateInvalidRandom, #[strum(serialize = "state-reproducibility-basic")] StateReproducibilityBasic, #[strum(serialize = "state-reproducibility-null-cookies")] @@ -191,10 +191,14 @@ impl<'a> TestCase<'a> for PluginTestCase { "Asserts that the values for all parameters are set correctly to their default \ values when the plugin is initialized.", ), - PluginTestCase::StateInvalid => String::from( + PluginTestCase::StateInvalidEmpty => String::from( "The plugin should return false when 'clap_plugin_state::load()' is called with \ an empty state.", ), + PluginTestCase::StateInvalidRandom => String::from( + "Loads 3x1MB chunks of random bytes via 'clap_plugin_state::load()' and asserts \ + that the plugin doesn't crash.", + ), PluginTestCase::StateReproducibilityBasic => String::from( "Randomizes a plugin's parameters, saves its state, recreates the plugin \ instance, reloads the state, and then checks whether the parameter values are \ @@ -220,10 +224,6 @@ impl<'a> TestCase<'a> for PluginTestCase { when reloading and resaving the state.", PluginTestCase::StateReproducibilityBasic ), - PluginTestCase::StateRandomGarbage => String::from( - "Loads 10x1MB chunks of random bytes via 'clap_plugin_state::load()' and asserts \ - that the plugin doesn't crash.", - ), } } @@ -299,9 +299,11 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamDefaultValues => { params::test_param_default_values(library, plugin_id) } - PluginTestCase::StateInvalid => state::test_state_invalid(library, plugin_id), - PluginTestCase::StateRandomGarbage => { - state::test_state_random_garbage(library, plugin_id) + PluginTestCase::StateInvalidEmpty => { + state::test_state_invalid_empty(library, plugin_id) + } + PluginTestCase::StateInvalidRandom => { + state::test_state_invalid_random(library, plugin_id) } PluginTestCase::StateReproducibilityBasic => { state::test_state_reproducibility_null_cookies(library, plugin_id, false) diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 468794b..7ddf6fb 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -83,40 +83,38 @@ pub fn test_methods_non_null(library: &PluginLibrary, plugin_id: &str) -> Result // Check `clap_plugin` methods. // SAFETY: `plugin.as_ptr()` is guaranteed to be a valid pointer as long as `plugin` is alive. unsafe { - let plugin = plugin.as_ptr(); + let ptr = plugin.as_ptr(); - anyhow::ensure!((*plugin).init.is_some(), "clap_plugin::init is null"); - anyhow::ensure!((*plugin).destroy.is_some(), "clap_plugin::destroy is null"); - anyhow::ensure!((*plugin).process.is_some(), "clap_plugin::process is null"); - anyhow::ensure!((*plugin).reset.is_some(), "clap_plugin::reset is null"); + anyhow::ensure!((*ptr).init.is_some(), "clap_plugin::init is null"); + + plugin.init().context("Error during initialization")?; + + anyhow::ensure!((*ptr).destroy.is_some(), "clap_plugin::destroy is null"); + anyhow::ensure!((*ptr).process.is_some(), "clap_plugin::process is null"); + anyhow::ensure!((*ptr).reset.is_some(), "clap_plugin::reset is null"); anyhow::ensure!( - (*plugin).get_extension.is_some(), + (*ptr).get_extension.is_some(), "clap_plugin::get_extension is null" ); anyhow::ensure!( - (*plugin).on_main_thread.is_some(), + (*ptr).on_main_thread.is_some(), "clap_plugin::on_main_thread is null" ); + anyhow::ensure!((*ptr).activate.is_some(), "clap_plugin::activate is null"); anyhow::ensure!( - (*plugin).activate.is_some(), - "clap_plugin::activate is null" - ); - anyhow::ensure!( - (*plugin).deactivate.is_some(), + (*ptr).deactivate.is_some(), "clap_plugin::deactivate is null" ); anyhow::ensure!( - (*plugin).start_processing.is_some(), + (*ptr).start_processing.is_some(), "clap_plugin::start_processing is null" ); anyhow::ensure!( - (*plugin).stop_processing.is_some(), + (*ptr).stop_processing.is_some(), "clap_plugin::stop_processing is null" ); } - plugin.init().context("Error during initialization")?; - // Check known extensions. unsafe { check_extension::( diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 9fbe9d2..ec7e4bd 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -25,8 +25,8 @@ const EXPECTED_STATE_FILE_NAME: &str = "state-expected"; /// The file name we'll use to dump the actual state when a test fails. const ACTUAL_STATE_FILE_NAME: &str = "state-actual"; -/// The test for `PluginTestCase::StateInvalid`. -pub fn test_state_invalid(library: &PluginLibrary, plugin_id: &str) -> Result { +/// The test for `PluginTestCase::StateInvalidEmpty`. +pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Result { let host = Host::new(); let plugin = library .create_plugin(plugin_id, host.clone()) @@ -47,10 +47,12 @@ pub fn test_state_invalid(library: &PluginLibrary, plugin_id: &str) -> Result anyhow::bail!( - "The plugin returned true when 'clap_plugin_state::load()' was called when an empty \ - state, this is likely a bug." - ), + Ok(_) => Ok(TestStatus::Warning { + details: Some(format!( + "The plugin returned true when 'clap_plugin_state::load()' was called when an \ + empty state, this is likely a bug." + )), + }), Err(_) => { host.handle_callbacks_once(); host.callback_error_check() @@ -61,6 +63,54 @@ pub fn test_state_invalid(library: &PluginLibrary, plugin_id: &str) -> Result Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + + plugin.init().context("Error during initialization")?; + + let state = match plugin.get_extension::() { + Some(state) => state, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + State::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + + host.handle_callbacks_once(); + + let mut random_data = vec![0u8; 1024 * 1024]; + let mut result = Ok(()); + + for _ in 0..3 { + prng.fill(&mut random_data[..]); + result = result.or(state.load(&random_data)); + } + + host.handle_callbacks_once(); + host.callback_error_check() + .context("An error occured during a host callback")?; + + match result { + Err(_) => Ok(TestStatus::Success { details: None }), + Ok(_) => Ok(TestStatus::Warning { + details: Some(String::from( + "The plugin loaded random bytes successfully, which is unexpected, but the plugin \ + did not crash.", + )), + }), + } +} + /// The test for `PluginTestCase::StateReproducibilityNullCookies`. See the description of this test /// for a detailed explanation, but we essentially check if saving a loaded state results in the /// same state file, and whether a plugin's parameters are the same after loading the state. @@ -633,54 +683,6 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> } } -/// The test for `PluginTestCase::StateRandomGarbage`. -pub fn test_state_random_garbage(library: &PluginLibrary, plugin_id: &str) -> Result { - let mut prng = new_prng(); - - let host = Host::new(); - let plugin = library - .create_plugin(plugin_id, host.clone()) - .context("Could not create the plugin instance")?; - - plugin.init().context("Error during initialization")?; - - let state = match plugin.get_extension::() { - Some(state) => state, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - State::EXTENSION_ID.to_str().unwrap(), - )), - }) - } - }; - - host.handle_callbacks_once(); - - let mut random_data = vec![0u8; 1024 * 1024]; - let mut result = Ok(()); - - for _ in 0..10 { - prng.fill(&mut random_data[..]); - result = result.or(state.load(&random_data)); - } - - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; - - match result { - Err(_) => Ok(TestStatus::Success { details: None }), - Ok(_) => Ok(TestStatus::Warning { - details: Some(String::from( - "The plugin loaded random bytes successfully, which is unexpected, but the plugin \ - did not crash.", - )), - }), - } -} - /// Build a string containing all different values between two sets of values. /// /// # Panics From f5c543ebc7ff9d8989a66a1a903631015286ff35 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 14 Jan 2026 01:47:39 +0400 Subject: [PATCH 021/114] bump dependencies --- Cargo.lock | 317 +++++++++++++++++---------------- Cargo.toml | 17 +- src/index.rs | 5 +- src/plugin/ext/latency.rs | 2 + src/plugin/ext/note_ports.rs | 1 + src/plugin/instance.rs | 6 + src/plugin/instance/process.rs | 9 +- src/tests/plugin.rs | 7 - src/tests/plugin/descriptor.rs | 153 ---------------- src/tests/plugin/params.rs | 8 +- src/tests/plugin/processing.rs | 4 +- src/tests/rng.rs | 113 ++++++------ 12 files changed, 254 insertions(+), 388 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7acc12a..829ec54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,6 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_with", "simplelog", "strum", "strum_macros", @@ -203,10 +202,10 @@ version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -223,19 +222,18 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "lazy_static", "windows-sys 0.48.0", ] [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -314,41 +312,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - [[package]] name = "deranged" version = "0.3.11" @@ -380,21 +343,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "r-efi", + "wasip2", ] [[package]] @@ -403,6 +361,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -432,12 +396,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -475,12 +433,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.178" @@ -489,12 +441,12 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" -version = "0.7.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", - "winapi", + "windows-link", ] [[package]] @@ -509,6 +461,12 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "lock_api" version = "0.4.10" @@ -615,7 +573,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -648,22 +606,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -671,18 +634,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] [[package]] name = "rand_pcg" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" dependencies = [ "rand_core", ] @@ -775,10 +738,17 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "rustix" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] [[package]] name = "ryu" @@ -828,7 +798,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -844,28 +814,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "simplelog" version = "0.12.1" @@ -897,32 +845,20 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] @@ -960,32 +896,32 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "libc", - "winapi", + "rustix 0.37.23", + "windows-sys 0.48.0", ] [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 0.37.23", - "windows-sys 0.48.0", + "rustix 1.1.3", + "windows-sys 0.60.2", ] [[package]] name = "textwrap" -version = "0.15.2" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", - "terminal_size 0.1.17", + "terminal_size 0.4.3", "unicode-linebreak", "unicode-width", ] @@ -997,7 +933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi", "winapi", ] @@ -1048,9 +984,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "utf8parse" @@ -1075,10 +1011,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasm-bindgen" @@ -1101,7 +1040,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.111", + "syn", "wasm-bindgen-shared", ] @@ -1123,7 +1062,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1171,7 +1110,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -1186,7 +1125,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1204,13 +1152,30 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1219,38 +1184,92 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index abeace1..8eb24b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,29 +17,28 @@ chrono = { version = "0.4.23", features = ["serde"] } clap = { version = "4.1.8", features = ["derive", "wrap_help"] } # For CLAP 1.2.2 support clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } -colored = "2.0.0" +colored = "3.0.0" crossbeam = "0.8.1" -libloading = "0.7.3" +libloading = "0.9.0" log = "0.4" log-panics = "2.0" midi-consts = "0.1.0" parking_lot = "0.12.1" -rand = "0.8.5" -rand_pcg = "0.3.1" +rand = "0.9.2" +rand_pcg = "0.9.0" rayon = "1.6.1" regex = "1.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_with = "1.12.0" simplelog = "0.12" -strum = "0.24.1" -strum_macros = "0.24.1" +strum = "0.27.2" +strum_macros = "0.27.2" tempfile = "3.3" -textwrap = { version = "0.15.0", features = ["terminal_size"] } +textwrap = { version = "0.16.2", features = ["terminal_size"] } walkdir = "2.3" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "0.9.3" +core-foundation = "0.10.1" [profile.profiling] inherits = "release" diff --git a/src/index.rs b/src/index.rs index 277a406..9333328 100644 --- a/src/index.rs +++ b/src/index.rs @@ -92,8 +92,7 @@ pub struct ProviderPresets { // All presets declared by the plugin, indexed by their location. Represented by a tuple list // because JSON object keys must be strings, and with the change from URIs to a location // kind+value that's not longer the case. - #[serde(with = "serde_with::rust::btreemap_as_tuple_list")] - pub presets: BTreeMap, + pub presets: Vec<(LocationValue, PresetFile)>, } /// Index the presets for one or more plugins. [`index()`] can be used to build a list of all @@ -155,7 +154,7 @@ where provider_name: provider_metadata.name, provider_vendor: provider_metadata.vendor, soundpacks: declared_data.soundpacks.clone(), - presets, + presets: presets.into_iter().collect(), }); } diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 278b7f8..a0197c1 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -9,6 +9,7 @@ use crate::{ use clap_sys::ext::latency::{clap_plugin_latency, CLAP_EXT_LATENCY}; use std::{ffi::CStr, ptr::NonNull}; +#[allow(unused)] pub struct Latency<'a> { plugin: &'a Plugin<'a>, latency: NonNull, @@ -28,6 +29,7 @@ impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { } impl<'a> Latency<'a> { + #[allow(unused)] pub fn get(&self) -> u32 { assert_plugin_state!(self.plugin, state == PluginStatus::Activating); diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 1874867..a91748d 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -31,6 +31,7 @@ pub struct NotePortConfig { /// The configuration for a single note port. #[derive(Debug, Clone)] pub struct NotePort { + #[allow(unused)] /// The preferred dialect for this note port. This should only ever contain a single value. pub prefered_dialect: clap_note_dialect, /// All supported note dialects for this port. All of these note dialect values will only ever diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 6017db7..efd0c4c 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -246,6 +246,12 @@ impl<'lib> Plugin<'lib> { let plugin = self.as_ptr(); if unsafe_clap_call! { plugin=>init(plugin) } { + // If the plugin never calls `request_callback`, the validator won't catch this + anyhow::ensure!( + unsafe { (*plugin).on_main_thread.is_some() }, + "clap_plugin::on_main_thread is null" + ); + self.state.status.store(PluginStatus::Deactivated); Ok(()) } else { diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index a87fa00..40d27cf 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -607,14 +607,14 @@ impl AudioBuffers { impl AudioBufferFill for Randomize<'_> { fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { for sample in slice.iter_mut() { - let y = self.0.gen_range(-1.0..=1.0f32); + let y = self.0.random_range(-1.0..=1.0f32); *sample = if y.is_subnormal() { 0.0 } else { y }; } } fn fill_input_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { for sample in slice.iter_mut() { - let y = self.0.gen_range(-1.0..=1.0f64); + let y = self.0.random_range(-1.0..=1.0f64); *sample = if y.is_subnormal() { 0.0 } else { y }; } } @@ -622,7 +622,7 @@ impl AudioBuffers { // fill with random NaN values so we can detect if a plugin left the output uninitialized fn fill_output_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { for sample in slice.iter_mut() { - let y: u32 = self.0.gen(); + let y: u32 = self.0.random(); let y = f32::from_bits(y | 0x7F800001); assert!(y.is_nan()); *sample = y; @@ -631,7 +631,7 @@ impl AudioBuffers { fn fill_output_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { for sample in slice.iter_mut() { - let y: u64 = self.0.gen(); + let y: u64 = self.0.random(); let y = f64::from_bits(y | 0x7FF0000000000001); assert!(y.is_nan()); *sample = y; @@ -821,7 +821,6 @@ impl EventQueue { unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { check_null_ptr!(list, (*list).ctx); let this = &*((*list).ctx as *const Self); - this.events.lock().len() as u32 } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 5f584b1..1725696 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -23,8 +23,6 @@ mod state; pub enum PluginTestCase { #[strum(serialize = "descriptor-consistency")] DescriptorConsistency, - #[strum(serialize = "methods-non-null")] - MethodsNonNull, #[strum(serialize = "features-categories")] FeaturesCategories, #[strum(serialize = "features-duplicates")] @@ -84,10 +82,6 @@ impl<'a> TestCase<'a> for PluginTestCase { fn description(&self) -> String { match self { - PluginTestCase::MethodsNonNull => String::from( - "Asserts that all methods of the 'clap_plugin' object and some known extension \ - are non-null.", - ), PluginTestCase::DescriptorConsistency => String::from( "The plugin descriptor returned from the plugin factory and the plugin descriptor \ stored on the 'clap_plugin object should be equivalent.", @@ -244,7 +238,6 @@ impl<'a> TestCase<'a> for PluginTestCase { fn run_in_process(&self, (library, plugin_id): Self::TestArgs) -> Result { match self { - PluginTestCase::MethodsNonNull => descriptor::test_methods_non_null(library, plugin_id), PluginTestCase::DescriptorConsistency => { descriptor::test_consistency(library, plugin_id) } diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 7ddf6fb..3153a3a 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -1,19 +1,15 @@ //! Tests surrounding plugin features. use anyhow::{Context, Result}; -use clap_sys::ext::*; use clap_sys::plugin_features::{ CLAP_PLUGIN_FEATURE_ANALYZER, CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, CLAP_PLUGIN_FEATURE_INSTRUMENT, CLAP_PLUGIN_FEATURE_NOTE_DETECTOR, CLAP_PLUGIN_FEATURE_NOTE_EFFECT, }; use std::collections::HashSet; -use std::ffi::CStr; use crate::plugin::host::Host; -use crate::plugin::instance::Plugin; use crate::plugin::library::PluginLibrary; use crate::tests::TestStatus; -use crate::util::unsafe_clap_call; /// Verifies that the descriptor stored in the factory and the descriptor stored on the plugin /// object are equivalent. @@ -48,155 +44,6 @@ pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result Result { - /// SAFETY: - /// Assumes that extension 'T' is a repr(C) struct with function pointers only. - unsafe fn check_extension(plugin: &Plugin<'_>, extension: &CStr) -> Result<()> { - let extension_ptr = unsafe_clap_call! { plugin.as_ptr()=>get_extension(plugin.as_ptr(), extension.as_ptr()) }; - if extension_ptr.is_null() { - return Ok(()); - } - - let methods = std::slice::from_raw_parts( - extension_ptr as *const *const (), - std::mem::size_of::() / std::mem::size_of::<*const ()>(), - ); - - for &method in methods.iter() { - if method.is_null() { - anyhow::bail!( - "Extension '{}' has a method that is null.", - extension.to_string_lossy() - ); - } - } - - Ok(()) - } - - let host = Host::new(); - let plugin = library - .create_plugin(plugin_id, host) - .context("Could not create the plugin instance")?; - - // Check `clap_plugin` methods. - // SAFETY: `plugin.as_ptr()` is guaranteed to be a valid pointer as long as `plugin` is alive. - unsafe { - let ptr = plugin.as_ptr(); - - anyhow::ensure!((*ptr).init.is_some(), "clap_plugin::init is null"); - - plugin.init().context("Error during initialization")?; - - anyhow::ensure!((*ptr).destroy.is_some(), "clap_plugin::destroy is null"); - anyhow::ensure!((*ptr).process.is_some(), "clap_plugin::process is null"); - anyhow::ensure!((*ptr).reset.is_some(), "clap_plugin::reset is null"); - anyhow::ensure!( - (*ptr).get_extension.is_some(), - "clap_plugin::get_extension is null" - ); - anyhow::ensure!( - (*ptr).on_main_thread.is_some(), - "clap_plugin::on_main_thread is null" - ); - anyhow::ensure!((*ptr).activate.is_some(), "clap_plugin::activate is null"); - anyhow::ensure!( - (*ptr).deactivate.is_some(), - "clap_plugin::deactivate is null" - ); - anyhow::ensure!( - (*ptr).start_processing.is_some(), - "clap_plugin::start_processing is null" - ); - anyhow::ensure!( - (*ptr).stop_processing.is_some(), - "clap_plugin::stop_processing is null" - ); - } - - // Check known extensions. - unsafe { - check_extension::( - &plugin, - ambisonic::CLAP_EXT_AMBISONIC, - )?; - check_extension::( - &plugin, - audio_ports::CLAP_EXT_AUDIO_PORTS, - )?; - check_extension::( - &plugin, - audio_ports_activation::CLAP_EXT_AUDIO_PORTS_ACTIVATION, - )?; - check_extension::( - &plugin, - audio_ports_config::CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, - )?; - check_extension::( - &plugin, - audio_ports_config::CLAP_EXT_AUDIO_PORTS_CONFIG, - )?; - check_extension::( - &plugin, - configurable_audio_ports::CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, - )?; - check_extension::( - &plugin, - context_menu::CLAP_EXT_CONTEXT_MENU, - )?; - check_extension::(&plugin, gui::CLAP_EXT_GUI)?; - check_extension::( - &plugin, - note_name::CLAP_EXT_NOTE_NAME, - )?; - check_extension::( - &plugin, - note_ports::CLAP_EXT_NOTE_PORTS, - )?; - check_extension::(&plugin, params::CLAP_EXT_PARAMS)?; - check_extension::( - &plugin, - param_indication::CLAP_EXT_PARAM_INDICATION, - )?; - check_extension::( - &plugin, - preset_load::CLAP_EXT_PRESET_LOAD, - )?; - check_extension::(&plugin, state::CLAP_EXT_STATE)?; - check_extension::( - &plugin, - state_context::CLAP_EXT_STATE_CONTEXT, - )?; - check_extension::(&plugin, render::CLAP_EXT_RENDER)?; - check_extension::( - &plugin, - remote_controls::CLAP_EXT_REMOTE_CONTROLS, - )?; - check_extension::(&plugin, surround::CLAP_EXT_SURROUND)?; - check_extension::(&plugin, latency::CLAP_EXT_LATENCY)?; - check_extension::(&plugin, tail::CLAP_EXT_TAIL)?; - check_extension::( - &plugin, - posix_fd_support::CLAP_EXT_POSIX_FD_SUPPORT, - )?; - check_extension::( - &plugin, - timer_support::CLAP_EXT_TIMER_SUPPORT, - )?; - check_extension::( - &plugin, - thread_pool::CLAP_EXT_THREAD_POOL, - )?; - check_extension::( - &plugin, - voice_info::CLAP_EXT_VOICE_INFO, - )?; - } - - Ok(TestStatus::Success { details: None }) -} - /// Check whether the plugin's categories are consistent. Currently this just makes sure that the /// plugin has one of the four main plugin category features. pub fn test_features_categories(library: &PluginLibrary, plugin_id: &str) -> Result { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 1c3fddc..84ffbde 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -104,10 +104,10 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul let values: [f64; VALUES_PER_PARAM] = [ *param_info.range.start(), *param_info.range.end(), - prng.gen_range(param_info.range.clone()), - prng.gen_range(param_info.range.clone()), - prng.gen_range(param_info.range.clone()), - prng.gen_range(param_info.range), + prng.random_range(param_info.range.clone()), + prng.random_range(param_info.range.clone()), + prng.random_range(param_info.range.clone()), + prng.random_range(param_info.range), ]; 'value_loop: for starting_value in values { // If the plugin rounds string representations then `value` may very diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 829a552..3566a15 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -356,8 +356,8 @@ pub fn test_process_random_block_sizes( let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); run_simple(&plugin, &mut process_data, 20, |process_data| { - process_data.block_size = if prng.gen_bool(0.8) { - prng.gen_range(2..=MAX_BUFFER_SIZE) + process_data.block_size = if prng.random_bool(0.8) { + prng.random_range(2..=MAX_BUFFER_SIZE) } else { 1 }; diff --git a/src/tests/rng.rs b/src/tests/rng.rs index c9ce30d..97d4c24 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -124,10 +124,10 @@ impl NoteGenerator { const SAMPLE_OFFSET_RANGE: RangeInclusive = -6..=5; let mut events = vec![]; - let mut sample = prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; + let mut sample = prng.random_range(SAMPLE_OFFSET_RANGE).max(0) as u32; while sample < num_samples { events.push(self.generate(prng, sample)?); - sample += prng.gen_range(SAMPLE_OFFSET_RANGE).max(0) as u32; + sample += prng.random_range(SAMPLE_OFFSET_RANGE).max(0) as u32; } queue.add_events(events); @@ -145,7 +145,7 @@ impl NoteGenerator { // We'll ignore the prefered note dialect and pick from all of the supported note dialects. // The plugin may get a CLAP note on and a MIDI note off if it supports both of those things - let note_port_idx = prng.gen_range(0..self.config.inputs.len()); + let note_port_idx = prng.random_range(0..self.config.inputs.len()); let supports_clap_note_events = self.config.inputs[note_port_idx] .supported_dialects .contains(&CLAP_NOTE_DIALECT_CLAP); @@ -167,13 +167,14 @@ impl NoteGenerator { // We could do this in a smarter way to avoid generating impossible event types (like a note // off when there are no active notes), but this should work fine. + for _ in 0..1024 { - let event_type = prng.sample(rand::distributions::Slice::new(possible_events).unwrap()); + let event_type = prng.sample(rand::distr::slice::Choose::new(possible_events).unwrap()); match event_type { NoteEventType::ClapNoteOn => { let note = if self.only_consistent_events { - let key = prng.gen_range(0..128); - let channel = prng.gen_range(0..16); + let key = prng.random_range(0..128); + let channel = prng.random_range(0..16); let note_id = self.next_note_id; let note = Note { key, @@ -190,14 +191,14 @@ impl NoteGenerator { note } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let velocity = prng.gen_range(0.0..=1.0); + let velocity = prng.random_range(0.0..=1.0); return Ok(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -220,18 +221,18 @@ impl NoteGenerator { continue; } - let note_idx = prng.gen_range(0..self.active_notes[note_port_idx].len()); + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx].remove(note_idx) } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let velocity = prng.gen_range(0.0..=1.0); + let velocity = prng.random_range(0.0..=1.0); return Ok(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -254,7 +255,7 @@ impl NoteGenerator { } // A note can only be choked once - let note_idx = prng.gen_range(0..self.active_notes[note_port_idx].len()); + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); let note = &mut self.active_notes[note_port_idx][note_idx]; if note.choked { continue; @@ -264,15 +265,15 @@ impl NoteGenerator { *note } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; // Does a velocity make any sense here? Probably not. - let velocity = prng.gen_range(0.0..=1.0); + let velocity = prng.random_range(0.0..=1.0); return Ok(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -294,25 +295,25 @@ impl NoteGenerator { continue; } - let note_idx = prng.gen_range(0..self.active_notes[note_port_idx].len()); + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx][note_idx] } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let expression_id = - prng.gen_range(CLAP_NOTE_EXPRESSION_VOLUME..=CLAP_NOTE_EXPRESSION_PRESSURE); + let expression_id = prng + .random_range(CLAP_NOTE_EXPRESSION_VOLUME..=CLAP_NOTE_EXPRESSION_PRESSURE); let value_range = match expression_id { CLAP_NOTE_EXPRESSION_VOLUME => 0.0..=4.0, CLAP_NOTE_EXPRESSION_TUNING => -128.0..=128.0, _ => 0.0..=1.0, }; - let value = prng.gen_range(value_range); + let value = prng.random_range(value_range); return Ok(Event::NoteExpression(clap_event_note_expression { header: clap_event_header { @@ -332,8 +333,8 @@ impl NoteGenerator { } NoteEventType::MidiNoteOn => { let note = if self.only_consistent_events { - let key = prng.gen_range(0..128); - let channel = prng.gen_range(0..16); + let key = prng.random_range(0..128); + let channel = prng.random_range(0..16); let note_id = self.next_note_id; let note = Note { key, @@ -350,14 +351,14 @@ impl NoteGenerator { note } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let velocity = prng.gen_range(0.0..=1.0); + let velocity = prng.random_range(0.0..=1.0); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -380,18 +381,18 @@ impl NoteGenerator { continue; } - let note_idx = prng.gen_range(0..self.active_notes[note_port_idx].len()); + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx].remove(note_idx) } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let velocity = prng.gen_range(0.0..=1.0); + let velocity = prng.random_range(0.0..=1.0); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -409,8 +410,8 @@ impl NoteGenerator { })); } NoteEventType::MidiChannelPressure => { - let channel = prng.gen_range(0..16); - let pressure = prng.gen_range(0..128); + let channel = prng.random_range(0..16); + let pressure = prng.random_range(0..128); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -429,18 +430,18 @@ impl NoteGenerator { continue; } - let note_idx = prng.gen_range(0..self.active_notes[note_port_idx].len()); + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx][note_idx] } else { Note { - key: prng.gen_range(0..128), - channel: prng.gen_range(0..16), - note_id: prng.gen_range(0..100), + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), choked: false, } }; - let pressure = prng.gen_range(0..128); + let pressure = prng.random_range(0..128); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -459,9 +460,9 @@ impl NoteGenerator { } NoteEventType::MidiPitchBend => { // May as well just generate the two bytes directly instead of doing fancy things - let channel = prng.gen_range(0..16); - let byte1 = prng.gen_range(0..128); - let byte2 = prng.gen_range(0..128); + let channel = prng.random_range(0..16); + let byte1 = prng.random_range(0..128); + let byte2 = prng.random_range(0..128); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -475,9 +476,9 @@ impl NoteGenerator { })); } NoteEventType::MidiCc => { - let channel = prng.gen_range(0..16); - let cc = prng.gen_range(0..128); - let value = prng.gen_range(0..128); + let channel = prng.random_range(0..16); + let cc = prng.random_range(0..128); + let value = prng.random_range(0..128); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -491,8 +492,8 @@ impl NoteGenerator { })); } NoteEventType::MidiProgramChange => { - let channel = prng.gen_range(0..16); - let program_number = prng.gen_range(0..128); + let channel = prng.random_range(0..16); + let program_number = prng.random_range(0..128); return Ok(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, @@ -611,7 +612,7 @@ impl<'a> ParamFuzzer<'a> { } let value = if self.snap_to_bounds { - if prng.gen_bool(0.5) { + if prng.random_bool(0.5) { *param_info.range.start() } else { *param_info.range.end() @@ -620,9 +621,9 @@ impl<'a> ParamFuzzer<'a> { if param_info.stepped() { // We already confirmed that the range starts and ends in an integer when // constructing the parameter info - prng.gen_range(param_info.range.clone()).round() + prng.random_range(param_info.range.clone()).round() } else { - prng.gen_range(param_info.range.clone()) + prng.random_range(param_info.range.clone()) } }; From c5d9ae19a82c0316a50881c395d5623c0ca1c3bf Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 14 Jan 2026 12:44:18 +0400 Subject: [PATCH 022/114] add 'param-fuzz-polyphonic' test --- src/plugin/ext/note_ports.rs | 14 ++ src/plugin/ext/params.rs | 25 +++ src/tests/plugin.rs | 10 + src/tests/plugin/params.rs | 91 +++++++- src/tests/plugin/processing.rs | 24 +- src/tests/rng.rs | 391 ++++++++++++++++++++------------- 6 files changed, 386 insertions(+), 169 deletions(-) diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index a91748d..68fccc1 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -6,6 +6,7 @@ use crate::util::unsafe_clap_call; use anyhow::Result; use clap_sys::ext::note_ports::{ clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS, + CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, }; use std::collections::HashSet; use std::ffi::CStr; @@ -154,3 +155,16 @@ impl NotePorts<'_> { Ok(config) } } + +impl NotePort { + pub fn supports_clap(&self) -> bool { + self.supported_dialects.contains(&CLAP_NOTE_DIALECT_CLAP) + } + + pub fn supports_midi(&self) -> bool { + self.supported_dialects.contains(&CLAP_NOTE_DIALECT_MIDI) + || self + .supported_dialects + .contains(&CLAP_NOTE_DIALECT_MIDI_MPE) + } +} diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 2b7030a..31110a9 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -366,4 +366,29 @@ impl Param { pub fn automatable(&self) -> bool { (self.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0 } + + /// Whether this parameter is automatable per note ID, key, channel, or port. + pub fn poly_automatable(&self) -> bool { + (self.flags + & (CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID + | CLAP_PARAM_IS_AUTOMATABLE_PER_KEY + | CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL + | CLAP_PARAM_IS_AUTOMATABLE_PER_PORT)) + != 0 + } + + /// Whether this parameter is modulatable. + pub fn modulatable(&self) -> bool { + (self.flags & CLAP_PARAM_IS_MODULATABLE) != 0 + } + + /// Whether this parameter is modulatable per note ID, key, channel, or port. + pub fn poly_modulatable(&self) -> bool { + (self.flags + & (CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID + | CLAP_PARAM_IS_MODULATABLE_PER_KEY + | CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL + | CLAP_PARAM_IS_MODULATABLE_PER_PORT)) + != 0 + } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 1725696..f895c3b 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -57,6 +57,8 @@ pub enum PluginTestCase { ParamFuzzBounds, #[strum(serialize = "param-fuzz-sample-accurate")] ParamFuzzSampleAccurate, + #[strum(serialize = "param-fuzz-polyphonic")] + ParamFuzzPolyphonic, #[strum(serialize = "param-set-wrong-namespace")] ParamSetWrongNamespace, #[strum(serialize = "param-default-values")] @@ -176,6 +178,11 @@ impl<'a> TestCase<'a> for PluginTestCase { generating them at fixed intervals (1, 100, 1000 samples). The plugin passes the \ test if it doesn't produce any infinite or NaN values, and doesn't crash.", ), + PluginTestCase::ParamFuzzPolyphonic => String::from( + "Sends polyphonic parameter change events alongside with note events, and has the \ + plugin process them. The plugin passes the test if it doesn't produce any \ + infinite or NaN values, and doesn't crash.", + ), PluginTestCase::ParamSetWrongNamespace => String::from( "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ a mismatching namespace ID. Asserts that the plugin's parameter values don't \ @@ -286,6 +293,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamFuzzSampleAccurate => { params::test_param_fuzz_sample_accurate(library, plugin_id) } + PluginTestCase::ParamFuzzPolyphonic => { + params::test_param_fuzz_polyphonic(library, plugin_id) + } PluginTestCase::ParamSetWrongNamespace => { params::test_param_set_wrong_namespace(library, plugin_id) } diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 84ffbde..a96e0ee 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -248,7 +248,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails @@ -281,7 +281,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result &mut prng, &process_data.input_events, BUFFER_SIZE as u32, - )?; + ); } process_data.buffers.randomize(&mut prng); @@ -379,7 +379,7 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos).with_snap_to_bounds(); - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails @@ -412,7 +412,7 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul &mut prng, &process_data.input_events, BUFFER_SIZE as u32, - )?; + ); } process_data.buffers.randomize(&mut prng); @@ -516,7 +516,7 @@ pub fn test_param_fuzz_sample_accurate( // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut current_events: Option> = None; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); @@ -542,7 +542,7 @@ pub fn test_param_fuzz_sample_accurate( &mut prng, &process_data.input_events, BUFFER_SIZE as u32, - )?; + ); } process_data.buffers.randomize(&mut prng); @@ -585,6 +585,85 @@ pub fn test_param_fuzz_sample_accurate( Ok(TestStatus::Success { details: None }) } +/// The test for `ProcessingTest::ParamFuzzPolyphonic`. +pub fn test_param_fuzz_polyphonic(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Could not fetch the plugin's audio port config")?, + None => AudioPortConfig::default(), + }; + + let note_ports = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Could not fetch the plugin's note port config")?, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + NotePorts::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + + let params = match plugin.get_extension::() { + Some(params) => params, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + Params::EXTENSION_ID.to_str().unwrap(), + )), + }) + } + }; + + let param_infos = params + .info() + .context("Could not fetch the plugin's parameters")?; + + if param_infos + .values() + .all(|param| !param.poly_automatable() && !param.poly_modulatable()) + { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not have any poly-modulatable parameters.", + )), + }); + } + + let mut note_event_rng = NoteGenerator::new(¬e_ports).with_params(¶m_infos); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); + + // TODO: mix in mono parameter changes as well? + + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); + Ok(()) + })?; + + host.handle_callbacks_once(); + + Ok(TestStatus::Success { details: None }) +} + /// The test for `ProcessingTest::ParamSetWrongNamespace`. pub fn test_param_set_wrong_namespace( library: &PluginLibrary, diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 3566a15..b0bcd86 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -156,7 +156,7 @@ pub fn test_process_note_out_of_place( // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports - let mut note_event_rng = NoteGenerator::new(note_ports_config); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -170,7 +170,7 @@ pub fn test_process_note_out_of_place( &mut prng, &process_data.input_events, process_data.block_size, - )?; + ); Ok(()) })?; @@ -215,7 +215,7 @@ pub fn test_process_varying_sample_rates( let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { - let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut process_data = ProcessData::new( &mut audio_buffers, ProcessConfig { @@ -232,7 +232,7 @@ pub fn test_process_varying_sample_rates( &mut prng, &process_data.input_events, process_data.block_size, - )?; + ); } Ok(()) @@ -283,7 +283,7 @@ pub fn test_process_varying_block_sizes( }; for &buffer_size in BLOCK_SIZES { - let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -301,7 +301,7 @@ pub fn test_process_varying_block_sizes( &mut prng, &process_data.input_events, process_data.block_size, - )?; + ); } Ok(()) @@ -350,7 +350,7 @@ pub fn test_process_random_block_sizes( None => None, }; - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -369,7 +369,7 @@ pub fn test_process_random_block_sizes( &mut prng, &process_data.input_events, process_data.block_size, - )?; + ); } Ok(()) @@ -508,7 +508,7 @@ pub fn test_process_audio_reset_determinism( None => None, }; - let mut note_event_rng = note_ports_config.map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut audio_buffers = AudioBuffers::new_out_of_place_f32( &audio_ports_config, BUFFER_SIZE * 8, /* we do it in one block to simplify the test */ @@ -526,7 +526,7 @@ pub fn test_process_audio_reset_determinism( &mut new_prng(), &process_data.input_events, process_data.block_size, - )?; + ); } let result = match curr_iter { @@ -737,7 +737,7 @@ pub fn test_process_audio_config( // TODO: check info } - let mut note_event_rng = note_ports_config.clone().map(NoteGenerator::new); + let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); let mut audio_buffers = if in_place { AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE) } else { @@ -753,7 +753,7 @@ pub fn test_process_audio_config( &mut prng, &process_data.input_events, process_data.block_size, - )?; + ); } Ok(()) diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 97d4c24..c4b968e 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -1,6 +1,5 @@ //! Utilities for generating pseudo-random data. -use anyhow::{Context, Result}; use clap_sys::events::{ clap_event_header, clap_event_midi, clap_event_note, clap_event_note_expression, clap_event_param_value, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_IS_LIVE, CLAP_EVENT_MIDI, @@ -8,16 +7,14 @@ use clap_sys::events::{ CLAP_EVENT_PARAM_VALUE, CLAP_NOTE_EXPRESSION_PRESSURE, CLAP_NOTE_EXPRESSION_TUNING, CLAP_NOTE_EXPRESSION_VOLUME, }; -use clap_sys::ext::note_ports::{ - CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, -}; use midi_consts::channel_event as midi; +use rand::seq::IteratorRandom; use rand::Rng; use rand_pcg::Pcg32; use std::ops::RangeInclusive; use crate::plugin::ext::note_ports::NotePortConfig; -use crate::plugin::ext::params::ParamInfo; +use crate::plugin::ext::params::{Param, ParamInfo}; use crate::plugin::instance::process::{Event, EventQueue}; /// Create a new pseudo-random number generator with a fixed seed. @@ -28,14 +25,22 @@ pub fn new_prng() -> Pcg32 { /// A random note and MIDI event generator that generates consistent events based on the /// capabilities stored in a [`NotePortConfig`] #[derive(Debug, Clone)] -pub struct NoteGenerator { +pub struct NoteGenerator<'a> { /// The note ports to generate random events for. - config: NotePortConfig, + config: &'a NotePortConfig, + + /// The parameter info to generate random poly modulation and automation events for. + params: Option<&'a ParamInfo>, + /// Only generate consistent events. This prevents things like note off events for notes that /// aren't playing, double note on events, and generating note expressions for notes that aren't /// active. only_consistent_events: bool, + /// The range for the next event's timing relative to the previous event. + /// This will be capped to 0 when generating events + sample_offset_range: RangeInclusive, + /// Contains the currently playing notes per-port. We'll be nice and not send overlapping notes /// or note-offs without a corresponding note-on. /// @@ -64,7 +69,7 @@ struct Note { } /// The different kinds of events we can generate. The event type chosen depends on the plugin. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum NoteEventType { ClapNoteOn, ClapNoteOff, @@ -77,24 +82,39 @@ enum NoteEventType { MidiPitchBend, MidiCc, MidiProgramChange, + ParamValue, + ParamModulation, } -impl NoteGenerator { +impl<'a> NoteGenerator<'a> { /// Create a new random note generator based on a plugin's note port configuration. By default /// these events are consistent, meaning that there are no things like note offs before a note /// on, duplicate note ons, or note expressions for notes that don't exist. - pub fn new(config: NotePortConfig) -> Self { + pub fn new(config: &'a NotePortConfig) -> Self { let num_inputs = config.inputs.len(); NoteGenerator { config, + params: None, + only_consistent_events: true, + // The range for the next event's timing relative to the `current_sample`. This will be + // capped at 0, so there's a ~58% chance the next event occurs on the same time interval as + // the previous event. + sample_offset_range: -6..=5, + active_notes: vec![Vec::new(); num_inputs], next_note_id: 0, } } + /// Set the parameter info to generate random polyphonic automation and modulation events for. + pub fn with_params(mut self, params: &'a ParamInfo) -> Self { + self.params = Some(params); + self + } + /// Allow inconsistent events, like note off events without a corresponding note on and note /// expression events for notes that aren't currently playing. pub fn with_inconsistent_events(mut self) -> Self { @@ -104,84 +124,93 @@ impl NoteGenerator { /// Fill an event queue with random events for the next `num_samples` samples. This does not /// clear the event queue. If the queue was not empty, then this will do a stable sort after - /// inserting _all_ events. If an error was returned, then the queue will not have been sorted. - /// - /// Returns an error if generating random events failed. This can happen if the plugin doesn't - /// support any note event types. - pub fn fill_event_queue( - &mut self, - prng: &mut Pcg32, - queue: &EventQueue, - num_samples: u32, - ) -> Result<()> { - if self.config.inputs.is_empty() { - return Ok(()); + /// inserting _all_ events. + pub fn fill_event_queue(&mut self, prng: &mut Pcg32, queue: &EventQueue, num_samples: u32) { + let mut events = vec![]; + let mut sample = prng.random_range(self.sample_offset_range.clone()).max(0) as u32; + while sample < num_samples { + let Some(event) = self.generate(prng, sample) else { + return; + }; + + events.push(event); + sample += prng.random_range(self.sample_offset_range.clone()).max(0) as u32; } - // The range for the next event's timing relative to the `current_sample`. This will be - // capped at 0, so there's a ~58% chance the next event occurs on the same time interval as - // the previous event. - const SAMPLE_OFFSET_RANGE: RangeInclusive = -6..=5; + queue.add_events(events); + } + #[allow(unused)] + pub fn stop_all_voices(&mut self, queue: &EventQueue, time_offset: u32) { let mut events = vec![]; - let mut sample = prng.random_range(SAMPLE_OFFSET_RANGE).max(0) as u32; - while sample < num_samples { - events.push(self.generate(prng, sample)?); - sample += prng.random_range(SAMPLE_OFFSET_RANGE).max(0) as u32; + for (note_port_idx, active_notes) in self.active_notes.drain(..).enumerate() { + let supports_clap = self.config.inputs[note_port_idx].supports_clap(); + + for note in active_notes { + if supports_clap { + events.push(Event::Note(clap_event_note { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_NOTE_OFF, + flags: 0, + }, + note_id: note.note_id, + port_index: note_port_idx as i16, + channel: note.channel, + key: note.key, + velocity: 0.0, + })); + } else { + events.push(Event::Midi(clap_event_midi { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_MIDI, + flags: 0, + }, + port_index: note_port_idx as u16, + data: [midi::NOTE_OFF | note.channel as u8, note.key as u8, 0], + })); + } + } } queue.add_events(events); - - Ok(()) } /// Generate a random note event for one of the plugin's note ports depending on the port's /// capabilities. Returns an error if the plugin doesn't have any note ports or if the note /// ports don't support either MIDI or CLAP note events. - pub fn generate(&mut self, prng: &mut Pcg32, time_offset: u32) -> Result { + pub fn generate(&mut self, prng: &mut Pcg32, time_offset: u32) -> Option { if self.config.inputs.is_empty() { - anyhow::bail!("Cannot generate note events for a plugin with no input note ports."); + return None; } - // We'll ignore the prefered note dialect and pick from all of the supported note dialects. - // The plugin may get a CLAP note on and a MIDI note off if it supports both of those things let note_port_idx = prng.random_range(0..self.config.inputs.len()); - let supports_clap_note_events = self.config.inputs[note_port_idx] - .supported_dialects - .contains(&CLAP_NOTE_DIALECT_CLAP); - let supports_midi_events = self.config.inputs[note_port_idx] - .supported_dialects - .contains(&CLAP_NOTE_DIALECT_MIDI) - || self.config.inputs[note_port_idx] - .supported_dialects - .contains(&CLAP_NOTE_DIALECT_MIDI_MPE); - let possible_events = - NoteEventType::supported_types(supports_clap_note_events, supports_midi_events) - .with_context(|| { - format!( - "Note input port {note_port_idx} supports neither CLAP note events nor \ - MIDI. This is technically allowed, but few hosts will be able to \ - interact with the plugin." - ) - })?; // We could do this in a smarter way to avoid generating impossible event types (like a note // off when there are no active notes), but this should work fine. - for _ in 0..1024 { - let event_type = prng.sample(rand::distr::slice::Choose::new(possible_events).unwrap()); + // We'll ignore the prefered note dialect and pick from all of the supported note dialects. + // The plugin may get a CLAP note on and a MIDI note off if it supports both of those things + let event_type = NoteEventType::supported_types( + self.config.inputs[note_port_idx].supports_clap(), + self.config.inputs[note_port_idx].supports_midi(), + self.params.is_some(), + ) + .choose(prng)?; + match event_type { NoteEventType::ClapNoteOn => { let note = if self.only_consistent_events { - let key = prng.random_range(0..128); - let channel = prng.random_range(0..16); - let note_id = self.next_note_id; let note = Note { - key, - channel, - note_id, - choked: false, + note_id: self.next_note_id, + ..Note::random(prng) }; + if self.active_notes[note_port_idx].contains(¬e) { continue; } @@ -190,16 +219,11 @@ impl NoteGenerator { note } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let velocity = prng.random_range(0.0..=1.0); - return Ok(Event::Note(clap_event_note { + return Some(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -224,16 +248,11 @@ impl NoteGenerator { let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx].remove(note_idx) } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let velocity = prng.random_range(0.0..=1.0); - return Ok(Event::Note(clap_event_note { + return Some(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -264,17 +283,12 @@ impl NoteGenerator { *note } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; // Does a velocity make any sense here? Probably not. let velocity = prng.random_range(0.0..=1.0); - return Ok(Event::Note(clap_event_note { + return Some(Event::Note(clap_event_note { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -298,12 +312,7 @@ impl NoteGenerator { let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx][note_idx] } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let expression_id = prng @@ -315,7 +324,7 @@ impl NoteGenerator { }; let value = prng.random_range(value_range); - return Ok(Event::NoteExpression(clap_event_note_expression { + return Some(Event::NoteExpression(clap_event_note_expression { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -333,15 +342,11 @@ impl NoteGenerator { } NoteEventType::MidiNoteOn => { let note = if self.only_consistent_events { - let key = prng.random_range(0..128); - let channel = prng.random_range(0..16); - let note_id = self.next_note_id; let note = Note { - key, - channel, - note_id, - choked: false, + note_id: self.next_note_id, + ..Note::random(prng) }; + if self.active_notes[note_port_idx].contains(¬e) { continue; } @@ -350,16 +355,11 @@ impl NoteGenerator { note } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let velocity = prng.random_range(0.0..=1.0); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -384,16 +384,11 @@ impl NoteGenerator { let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx].remove(note_idx) } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let velocity = prng.random_range(0.0..=1.0); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -412,7 +407,7 @@ impl NoteGenerator { NoteEventType::MidiChannelPressure => { let channel = prng.random_range(0..16); let pressure = prng.random_range(0..128); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -433,16 +428,11 @@ impl NoteGenerator { let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); self.active_notes[note_port_idx][note_idx] } else { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } + Note::random(prng) }; let pressure = prng.random_range(0..128); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -463,7 +453,7 @@ impl NoteGenerator { let channel = prng.random_range(0..16); let byte1 = prng.random_range(0..128); let byte2 = prng.random_range(0..128); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -479,7 +469,7 @@ impl NoteGenerator { let channel = prng.random_range(0..16); let cc = prng.random_range(0..128); let value = prng.random_range(0..128); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -494,7 +484,7 @@ impl NoteGenerator { NoteEventType::MidiProgramChange => { let channel = prng.random_range(0..16); let program_number = prng.random_range(0..128); - return Ok(Event::Midi(clap_event_midi { + return Some(Event::Midi(clap_event_midi { header: clap_event_header { size: std::mem::size_of::() as u32, time: time_offset, @@ -506,6 +496,92 @@ impl NoteGenerator { data: [midi::PROGRAM_CHANGE | channel, program_number, 0], })); } + NoteEventType::ParamValue => { + let Some(params) = self.params else { + continue; + }; + + let Some((param_id, param)) = params + .iter() + .filter(|(_, param)| { + !param.readonly() && !param.hidden() && param.poly_automatable() + }) + .choose(prng) + else { + continue; + }; + + let note = if self.only_consistent_events { + if self.active_notes[note_port_idx].is_empty() { + continue; + } + + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); + self.active_notes[note_port_idx][note_idx] + } else { + Note::random(prng) + }; + + return Some(Event::ParamValue(clap_event_param_value { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_PARAM_VALUE, + flags: 0, + }, + param_id: *param_id, + cookie: param.cookie, + note_id: note.note_id, + port_index: note_port_idx as i16, + channel: note.channel, + key: note.key, + value: ParamFuzzer::random_value(param, prng), + })); + } + NoteEventType::ParamModulation => { + let Some(params) = self.params else { + continue; + }; + + let Some((param_id, param)) = params + .iter() + .filter(|(_, param)| { + !param.readonly() && !param.hidden() && param.poly_modulatable() + }) + .choose(prng) + else { + continue; + }; + + let note = if self.only_consistent_events { + if self.active_notes[note_port_idx].is_empty() { + continue; + } + + let note_idx = prng.random_range(0..self.active_notes[note_port_idx].len()); + self.active_notes[note_port_idx][note_idx] + } else { + Note::random(prng) + }; + + return Some(Event::ParamValue(clap_event_param_value { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_PARAM_VALUE, + flags: 0, + }, + param_id: *param_id, + cookie: param.cookie, + note_id: note.note_id, + port_index: note_port_idx as i16, + channel: note.channel, + key: note.key, + value: ParamFuzzer::random_value(param, prng), + })); + } } } @@ -524,19 +600,6 @@ impl NoteGenerator { } impl NoteEventType { - const ALL: &'static [NoteEventType] = &[ - NoteEventType::ClapNoteOn, - NoteEventType::ClapNoteOff, - NoteEventType::ClapNoteChoke, - NoteEventType::ClapNoteExpression, - NoteEventType::MidiNoteOn, - NoteEventType::MidiNoteOff, - NoteEventType::MidiChannelPressure, - NoteEventType::MidiPolyKeyPressure, - NoteEventType::MidiPitchBend, - NoteEventType::MidiCc, - NoteEventType::MidiProgramChange, - ]; const CLAP_EVENTS: &'static [NoteEventType] = &[ NoteEventType::ClapNoteOn, NoteEventType::ClapNoteOff, @@ -552,21 +615,43 @@ impl NoteEventType { NoteEventType::MidiCc, NoteEventType::MidiProgramChange, ]; + const PARAM_EVENTS: &'static [NoteEventType] = + &[NoteEventType::ParamValue, NoteEventType::ParamModulation]; /// Get a slice containing the event types supported by a plugin. Returns None if the plugin /// supports neither CLAP note events nor MIDI. pub fn supported_types( supports_clap_note_events: bool, supports_midi_events: bool, - ) -> Option<&'static [NoteEventType]> { - if supports_clap_note_events && supports_midi_events { - Some(NoteEventType::ALL) - } else if supports_clap_note_events { - Some(NoteEventType::CLAP_EVENTS) - } else if supports_midi_events { - Some(NoteEventType::MIDI_EVENTS) + supports_param_events: bool, + ) -> impl Iterator { + let clap = if supports_clap_note_events { + Self::CLAP_EVENTS } else { - None + &[] + }; + let midi = if supports_midi_events { + Self::MIDI_EVENTS + } else { + &[] + }; + let param = if supports_param_events { + Self::PARAM_EVENTS + } else { + &[] + }; + + clap.iter().chain(midi.iter()).chain(param.iter()).copied() + } +} + +impl Note { + fn random(prng: &mut Pcg32) -> Self { + Note { + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), + choked: false, } } } @@ -618,13 +703,7 @@ impl<'a> ParamFuzzer<'a> { *param_info.range.end() } } else { - if param_info.stepped() { - // We already confirmed that the range starts and ends in an integer when - // constructing the parameter info - prng.random_range(param_info.range.clone()).round() - } else { - prng.random_range(param_info.range.clone()) - } + ParamFuzzer::random_value(param_info, prng) }; Some(Event::ParamValue(clap_event_param_value { @@ -649,4 +728,14 @@ impl<'a> ParamFuzzer<'a> { })) }) } + + pub fn random_value(param: &Param, prng: &mut Pcg32) -> f64 { + if param.stepped() { + // We already confirmed that the range starts and ends in an integer when + // constructing the parameter info + prng.random_range(param.range.clone()).round() + } else { + prng.random_range(param.range.clone()) + } + } } From 8e648169d0549c714eb4a30f7fa7ca33a3a31a44 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 14 Jan 2026 21:43:14 +0400 Subject: [PATCH 023/114] replace 'param-fuzz-polyphonic' with 'param-fuzz-modulation' --- src/plugin/preset_discovery/indexer.rs | 2 + src/tests/plugin.rs | 17 ++- src/tests/plugin/params.rs | 95 +++++------- src/tests/plugin/processing.rs | 198 +++++++++++-------------- src/tests/rng.rs | 98 ++++++++++-- 5 files changed, 218 insertions(+), 192 deletions(-) diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 3e51032..cfc9fa8 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -58,7 +58,9 @@ pub struct IndexerResults { /// Data parsed from a `clap_preset_discovery_filetype`. #[derive(Debug, Clone)] pub struct FileType { + #[allow(unused)] pub name: String, + #[allow(unused)] pub description: Option, /// The file extension, doesn't contain a leading period. pub extension: String, diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index f895c3b..b1b1f92 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -57,8 +57,8 @@ pub enum PluginTestCase { ParamFuzzBounds, #[strum(serialize = "param-fuzz-sample-accurate")] ParamFuzzSampleAccurate, - #[strum(serialize = "param-fuzz-polyphonic")] - ParamFuzzPolyphonic, + #[strum(serialize = "param-fuzz-modulation")] + ParamFuzzModulation, #[strum(serialize = "param-set-wrong-namespace")] ParamSetWrongNamespace, #[strum(serialize = "param-default-values")] @@ -178,10 +178,11 @@ impl<'a> TestCase<'a> for PluginTestCase { generating them at fixed intervals (1, 100, 1000 samples). The plugin passes the \ test if it doesn't produce any infinite or NaN values, and doesn't crash.", ), - PluginTestCase::ParamFuzzPolyphonic => String::from( - "Sends polyphonic parameter change events alongside with note events, and has the \ - plugin process them. The plugin passes the test if it doesn't produce any \ - infinite or NaN values, and doesn't crash.", + PluginTestCase::ParamFuzzModulation => String::from( + "Sends parameter change events, including monophonic modulation and polyphonic \ + automation/modulation events at random irregular unsynchronized intervals, and \ + has the plugin process them. The plugin passes the test if it doesn't produce \ + any infinite or NaN values, and doesn't crash.", ), PluginTestCase::ParamSetWrongNamespace => String::from( "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ @@ -293,8 +294,8 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamFuzzSampleAccurate => { params::test_param_fuzz_sample_accurate(library, plugin_id) } - PluginTestCase::ParamFuzzPolyphonic => { - params::test_param_fuzz_polyphonic(library, plugin_id) + PluginTestCase::ParamFuzzModulation => { + params::test_param_fuzz_modulation(library, plugin_id) } PluginTestCase::ParamSetWrongNamespace => { params::test_param_set_wrong_namespace(library, plugin_id) diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index a96e0ee..10838b7 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -9,7 +9,7 @@ use std::collections::BTreeMap; use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; -use crate::plugin::ext::note_ports::NotePorts; +use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::ext::params::{ParamInfo, Params}; use crate::plugin::ext::Extension; use crate::plugin::host::Host; @@ -238,9 +238,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result .map(|ports| ports.config()) .transpose() .context("Could not fetch the plugin's note port config")? - // Don't try to generate notes if the plugin supports the note ports extension but doesn't - // actually have any note ports. JUCE does this. - .filter(|config| !config.inputs.is_empty()); + .unwrap_or_default(); let param_infos = params .info() .context("Could not fetch the plugin's parameters")?; @@ -248,7 +246,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails @@ -273,16 +271,11 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result have_set_parameters = true; } - // Audio and MIDI/note events are randomized in accordance to what the plugin - // supports - if let Some(note_event_rng) = note_event_rng.as_mut() { - // This includes a sort if `random_param_set_events` also contained a queue - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + ); process_data.buffers.randomize(&mut prng); Ok(()) @@ -369,9 +362,7 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul .map(|ports| ports.config()) .transpose() .context("Could not fetch the plugin's note port config")? - // Don't try to generate notes if the plugin supports the note ports extension but doesn't - // actually have any note ports. JUCE does this. - .filter(|config| !config.inputs.is_empty()); + .unwrap_or_default(); let param_infos = params .info() .context("Could not fetch the plugin's parameters")?; @@ -379,7 +370,7 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos).with_snap_to_bounds(); - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails @@ -406,14 +397,11 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul // Audio and MIDI/note events are randomized in accordance to what the plugin // supports - if let Some(note_event_rng) = note_event_rng.as_mut() { - // This includes a sort if `random_param_set_events` also contained a queue - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + ); process_data.buffers.randomize(&mut prng); Ok(()) @@ -506,9 +494,7 @@ pub fn test_param_fuzz_sample_accurate( .map(|ports| ports.config()) .transpose() .context("Could not fetch the plugin's note port config")? - // Don't try to generate notes if the plugin supports the note ports extension but doesn't - // actually have any note ports. JUCE does this. - .filter(|config| !config.inputs.is_empty()); + .unwrap_or_default(); let param_infos = params .info() .context("Could not fetch the plugin's parameters")?; @@ -516,7 +502,7 @@ pub fn test_param_fuzz_sample_accurate( // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut current_events: Option> = None; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); @@ -537,14 +523,11 @@ pub fn test_param_fuzz_sample_accurate( // Audio and MIDI/note events are randomized in accordance to what the plugin // supports - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - } - + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + BUFFER_SIZE as u32, + ); process_data.buffers.randomize(&mut prng); current_sample -= BUFFER_SIZE as u32; @@ -585,8 +568,8 @@ pub fn test_param_fuzz_sample_accurate( Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ParamFuzzPolyphonic`. -pub fn test_param_fuzz_polyphonic(library: &PluginLibrary, plugin_id: &str) -> Result { +/// The test for `ProcessingTest::ParamFuzzModulation`. +pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let host = Host::new(); @@ -606,14 +589,7 @@ pub fn test_param_fuzz_polyphonic(library: &PluginLibrary, plugin_id: &str) -> R Some(note_ports) => note_ports .config() .context("Could not fetch the plugin's note port config")?, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - NotePorts::EXTENSION_ID.to_str().unwrap(), - )), - }) - } + None => NotePortConfig::default(), }; let params = match plugin.get_extension::() { @@ -632,25 +608,20 @@ pub fn test_param_fuzz_polyphonic(library: &PluginLibrary, plugin_id: &str) -> R .info() .context("Could not fetch the plugin's parameters")?; - if param_infos - .values() - .all(|param| !param.poly_automatable() && !param.poly_modulatable()) - { - return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not have any poly-modulatable parameters.", - )), - }); - } - let mut note_event_rng = NoteGenerator::new(¬e_ports).with_params(¶m_infos); + let param_fuzzer = ParamFuzzer::new(¶m_infos); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports, BUFFER_SIZE); let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); - // TODO: mix in mono parameter changes as well? - run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); + + param_fuzzer.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); note_event_rng.fill_event_queue( &mut prng, &process_data.input_events, diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index b0bcd86..f254ec2 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -197,25 +197,23 @@ pub fn test_process_varying_sample_rates( .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => AudioPortConfig::default(), - }; - - let note_ports_config = match plugin.get_extension::() { - Some(note_ports) => Some( - note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - ), - None => None, - }; + let audio_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'audio-ports' IO configuration")? + .unwrap_or_default(); + + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut process_data = ProcessData::new( &mut audio_buffers, ProcessConfig { @@ -226,14 +224,11 @@ pub fn test_process_varying_sample_rates( run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); - - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); Ok(()) }) @@ -266,24 +261,22 @@ pub fn test_process_varying_block_sizes( .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => AudioPortConfig::default(), - }; + let audio_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'audio-ports' IO configuration")? + .unwrap_or_default(); - let note_ports_config = match plugin.get_extension::() { - Some(note_ports) => Some( - note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - ), - None => None, - }; + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); for &buffer_size in BLOCK_SIZES { - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -295,14 +288,11 @@ pub fn test_process_varying_block_sizes( num_iters as usize, |process_data| { process_data.buffers.randomize(&mut prng); - - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); Ok(()) }, @@ -334,23 +324,21 @@ pub fn test_process_random_block_sizes( .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => AudioPortConfig::default(), - }; + let audio_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'audio-ports' IO configuration")? + .unwrap_or_default(); - let note_ports_config = match plugin.get_extension::() { - Some(note_ports) => Some( - note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - ), - None => None, - }; + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); @@ -363,14 +351,11 @@ pub fn test_process_random_block_sizes( }; process_data.buffers.randomize(&mut prng); - - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); Ok(()) })?; @@ -492,23 +477,21 @@ pub fn test_process_audio_reset_determinism( .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, - None => AudioPortConfig::default(), - }; + let audio_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'audio-ports' IO configuration")? + .unwrap_or_default(); - let note_ports_config = match plugin.get_extension::() { - Some(note_ports) => Some( - note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - ), - None => None, - }; + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32( &audio_ports_config, BUFFER_SIZE * 8, /* we do it in one block to simplify the test */ @@ -521,13 +504,11 @@ pub fn test_process_audio_reset_determinism( let mut rng = new_prng(); process_data.buffers.randomize(&mut rng); - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut new_prng(), - &process_data.input_events, - process_data.block_size, - ); - } + note_event_rng.fill_event_queue( + &mut new_prng(), + &process_data.input_events, + process_data.block_size, + ); let result = match curr_iter { 0 => ProcessControlFlow::Reset, @@ -535,9 +516,7 @@ pub fn test_process_audio_reset_determinism( _ => { plugin.reset(); process_data.reset(); - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.reset(); - } + note_event_rng.reset(); ProcessControlFlow::Exit } @@ -607,14 +586,12 @@ pub fn test_process_audio_config( } }; - let note_ports_config = match plugin.get_extension::() { - Some(note_ports) => Some( - note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - ), - None => None, - }; + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); for config_audio_ports_config in audio_ports_config .enumerate() @@ -737,7 +714,7 @@ pub fn test_process_audio_config( // TODO: check info } - let mut note_event_rng = note_ports_config.as_ref().map(NoteGenerator::new); + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = if in_place { AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE) } else { @@ -747,14 +724,11 @@ pub fn test_process_audio_config( let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); run_simple(&plugin, &mut process_data, 5, |process_data| { process_data.buffers.randomize(&mut prng); - - if let Some(note_event_rng) = note_event_rng.as_mut() { - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - } + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); Ok(()) }) diff --git a/src/tests/rng.rs b/src/tests/rng.rs index c4b968e..8b78746 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -53,9 +53,15 @@ pub struct NoteGenerator<'a> { /// A helper to generate random parameter automation and modulation events in a couple different /// ways to stress test a plugin's parameter handling. pub struct ParamFuzzer<'a> { + /// The parameter info to generate random events for. params: &'a ParamInfo, - notes: NotePortConfig, + + /// Whether to snap generated parameter values to the parameter's minimum or maximum value. snap_to_bounds: bool, + + /// The range for the next event's timing relative to the previous event. + /// This will be capped to 0 when generating events + sample_offset_range: RangeInclusive, } /// The description of an active note in the [`NoteGenerator`]. @@ -579,7 +585,7 @@ impl<'a> NoteGenerator<'a> { port_index: note_port_idx as i16, channel: note.channel, key: note.key, - value: ParamFuzzer::random_value(param, prng), + value: ParamFuzzer::random_modulation(param, prng), })); } } @@ -661,23 +667,85 @@ impl<'a> ParamFuzzer<'a> { pub fn new(params: &'a ParamInfo) -> Self { ParamFuzzer { params, - notes: NotePortConfig::default(), snap_to_bounds: false, + sample_offset_range: -10..=20, } } - pub fn with_note_config(mut self, notes: NotePortConfig) -> Self { - self.notes = notes; - self - } - pub fn with_snap_to_bounds(mut self) -> Self { self.snap_to_bounds = true; self } - // TODO: Modulation and per-{key,channel,port,note_id} modulation - // TODO: Variants similar to `fill_event_queue` from `NoteGenerator` + /// Fill an event queue with random parameter change events for the next `num_samples` samples. + /// This does not clear the event queue. If the queue was not empty, then this will do a stable + /// sort after inserting _all_ events. + /// + /// Unlike [`ParamFuzzer::randomize_params_at`], this generates [`Event::ParamMod`] events as well as + /// generating events at random irregular unsynchronized (between different parameters) intervals. + pub fn fill_event_queue(&'a self, prng: &'a mut Pcg32, queue: &EventQueue, num_samples: u32) { + let mut events = vec![]; + let mut sample = prng.random_range(self.sample_offset_range.clone()).max(0) as u32; + while sample < num_samples { + let Some(event) = self.generate(prng) else { + return; + }; + + events.push(event); + sample += prng.random_range(self.sample_offset_range.clone()).max(0) as u32; + } + + queue.add_events(events); + } + + /// Generate a single random parameter change event for one of the plugin's parameters. + pub fn generate(&'a self, prng: &'a mut Pcg32) -> Option { + let (param_id, param_info) = self + .params + .iter() + .filter(|(_, info)| !info.readonly() && !info.hidden()) + .choose(prng)?; + + if !self.snap_to_bounds && param_info.modulatable() && prng.random_bool(0.5) { + Some(Event::ParamValue(clap_event_param_value { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: 0, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_PARAM_VALUE, + flags: 0, + }, + param_id: *param_id, + cookie: param_info.cookie, + note_id: -1, + port_index: -1, + channel: -1, + key: -1, + value: ParamFuzzer::random_modulation(param_info, prng), + })) + } else { + Some(Event::ParamValue(clap_event_param_value { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: 0, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_PARAM_VALUE, + flags: if param_info.automatable() { + 0 + } else { + CLAP_EVENT_IS_LIVE + }, + }, + param_id: *param_id, + cookie: param_info.cookie, + note_id: -1, + port_index: -1, + channel: -1, + key: -1, + value: ParamFuzzer::random_value(param_info, prng), + })) + } + } /// Randomize _all_ parameters at a certain sample index using **automation**, returning an /// iterator yielding automation events for all parameters. @@ -738,4 +806,14 @@ impl<'a> ParamFuzzer<'a> { prng.random_range(param.range.clone()) } } + + pub fn random_modulation(param: &Param, prng: &mut Pcg32) -> f64 { + let range = (param.range.end() - param.range.start()).abs() * 0.5; + + if param.stepped() { + prng.random_range(-range..=range).round() + } else { + prng.random_range(-range..=range) + } + } } From 346aa53fb0e6611b2736f031e7fe4f2ee6e65b20 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 14 Jan 2026 22:19:25 +0400 Subject: [PATCH 024/114] increase iteration count for 'param-conversions' test --- src/tests/plugin.rs | 2 +- src/tests/plugin/params.rs | 19 ++++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index b1b1f92..535fcb7 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -181,7 +181,7 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ParamFuzzModulation => String::from( "Sends parameter change events, including monophonic modulation and polyphonic \ automation/modulation events at random irregular unsynchronized intervals, and \ - has the plugin process them. The plugin passes the test if it doesn't produce \ + have the plugin process them. The plugin passes the test if it doesn't produce \ any infinite or NaN values, and doesn't crash.", ), PluginTestCase::ParamSetWrongNamespace => String::from( diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 10838b7..0211180 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -3,7 +3,6 @@ use anyhow::{Context, Result}; use clap_sys::events::CLAP_EVENT_PARAM_VALUE; use clap_sys::id::clap_id; -use rand::Rng; use serde::Serialize; use std::collections::BTreeMap; @@ -60,8 +59,6 @@ impl<'a> ParamValue<'a> { /// The test for `ProcessingTest::ParamConversions`. pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Result { - let mut prng = new_prng(); - let host = Host::new(); let plugin = library .create_plugin(plugin_id, host.clone()) @@ -98,18 +95,10 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul 'param_loop: for (param_id, param_info) in param_infos { let param_name = ¶m_info.name; - // For each parameter we'll test this for the minimum and maximum values - // (in case these values have special meanings), and four other random - // values - let values: [f64; VALUES_PER_PARAM] = [ - *param_info.range.start(), - *param_info.range.end(), - prng.random_range(param_info.range.clone()), - prng.random_range(param_info.range.clone()), - prng.random_range(param_info.range.clone()), - prng.random_range(param_info.range), - ]; - 'value_loop: for starting_value in values { + 'value_loop: for i in 0..=100 { + let starting_value = param_info.range.start() + + (param_info.range.end() - param_info.range.start()) * (i as f64 / 100.0); + // If the plugin rounds string representations then `value` may very // will not roundtrip correctly, so we'll start at the string // representation From 6055bba8ab9bf3c7358634d037b83b04245f5a1a Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 17 Jan 2026 22:17:10 +0400 Subject: [PATCH 025/114] fix clippy warnings --- Cargo.toml | 2 +- src/plugin/host.rs | 6 +++--- src/plugin/instance/process.rs | 14 +++++++------- src/plugin/preset_discovery/indexer.rs | 6 +++--- src/tests/plugin/processing.rs | 16 +++++++--------- src/tests/plugin/state.rs | 4 ++-- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8eb24b8..628081f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "clap-validator" version = "0.3.2" edition = "2021" license = "MIT" -rust-version = "1.64.0" # MSRV +rust-version = "1.87.0" # MSRV description = "A validator and automatic test suite for CLAP plugins" readme = "README.md" diff --git a/src/plugin/host.rs b/src/plugin/host.rs index 9e3f9c8..230703a 100644 --- a/src/plugin/host.rs +++ b/src/plugin/host.rs @@ -159,9 +159,9 @@ impl InstanceState { clap_version: CLAP_VERSION, // This is populated with a pointer to the `Arc`'s data after creating the Arc host_data: std::ptr::null_mut(), - name: b"clap-validator\0".as_ptr() as *const c_char, - vendor: b"Robbert van der Helm\0".as_ptr() as *const c_char, - url: b"https://github.com/free-audio/clap-validator\0".as_ptr() as *const c_char, + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), version: clap_validator_version.as_ptr(), get_extension: Some(Host::get_extension), request_restart: Some(Host::request_restart), diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 40d27cf..231a828 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -232,7 +232,7 @@ impl<'a> ProcessData<'a> { let process_data = clap_process { steady_time: self.sample_pos as i64, - frames_count: self.block_size as u32, + frames_count: self.block_size, transport: &self.transport_info, audio_inputs: if inputs.is_empty() { std::ptr::null() @@ -388,11 +388,11 @@ impl AudioBuffers { }; if let Some(input) = buffer.input() { - if clap_inputs.len() <= input as usize { - clap_inputs.resize(input as usize + 1, None); + if clap_inputs.len() <= input { + clap_inputs.resize(input + 1, None); } - clap_inputs[input as usize] = Some(clap_audio_buffer { + clap_inputs[input] = Some(clap_audio_buffer { data32: if buffer.is_64bit() { null_mut() } else { @@ -412,11 +412,11 @@ impl AudioBuffers { } if let Some(output) = buffer.output() { - if clap_outputs.len() <= output as usize { - clap_outputs.resize(output as usize + 1, None); + if clap_outputs.len() <= output { + clap_outputs.resize(output + 1, None); } - clap_outputs[output as usize] = Some(clap_audio_buffer { + clap_outputs[output] = Some(clap_audio_buffer { data32: if buffer.is_64bit() { null_mut() } else { diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index cfc9fa8..c256ed1 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -378,9 +378,9 @@ impl Indexer { clap_preset_discovery_indexer: Mutex::new(clap_preset_discovery_indexer { clap_version: CLAP_VERSION, - name: b"clap-validator\0".as_ptr() as *const c_char, - vendor: b"Robbert van der Helm\0".as_ptr() as *const c_char, - url: b"https://github.com/free-audio/clap-validator\0".as_ptr() as *const c_char, + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), version: clap_validator_version.as_ptr(), // This is filled with a pointer to this struct after the `Box` has been allocated indexer_data: std::ptr::null_mut(), diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index f254ec2..94afeaf 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -39,7 +39,7 @@ where ) })?; - original_buffers.clone_from(&process.buffers); + original_buffers.clone_from(process.buffers); plugin.process(process).with_context(|| { format!("Failed to process cycle {} out of {}", curr_iter, num_iters) @@ -409,7 +409,7 @@ pub fn test_process_audio_constant_mask( process.buffers.silence_all_inputs(); } - original_buffers.clone_from(&process.buffers); + original_buffers.clone_from(process.buffers); curr_iter += 1; plugin @@ -457,7 +457,7 @@ pub fn test_process_audio_constant_mask( if !has_received_constant_flag && has_received_constant_output { return Ok(TestStatus::Warning { - details: Some(format!( + details: Some(String::from( "The plugin does not seem to set the constant mask during processing.", )), }); @@ -531,8 +531,8 @@ pub fn test_process_audio_reset_determinism( if !audio_output[0].is_same(&audio_output[1]) { return Ok(TestStatus::Warning { - details: Some(format!( - "Plugin output does not seem to be deterministic after reactivation" + details: Some(String::from( + "Plugin output does not seem to be deterministic after reactivation", )), }); } @@ -614,15 +614,13 @@ pub fn test_process_audio_config( { let main_input_channels = config_audio_ports .inputs - .iter() - .next() + .first() .filter(|x| x.is_main) .map(|x| x.num_channels); let main_output_channels = config_audio_ports .outputs - .iter() - .next() + .first() .filter(|x| x.is_main) .map(|x| x.num_channels); diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index ec7e4bd..ddc3b52 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -48,9 +48,9 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res match state.load(&[]) { Ok(_) => Ok(TestStatus::Warning { - details: Some(format!( + details: Some(String::from( "The plugin returned true when 'clap_plugin_state::load()' was called when an \ - empty state, this is likely a bug." + empty state, this is likely a bug.", )), }), Err(_) => { From f2fc203bf5493c94449addf808eab0212a67400a Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 17 Jan 2026 23:26:34 +0400 Subject: [PATCH 026/114] add configurable-audio-ports layout test --- src/plugin/ext.rs | 1 + src/plugin/ext/configurable_audio_ports.rs | 109 +++++++ src/tests/plugin.rs | 36 ++- src/tests/plugin/layout.rs | 360 +++++++++++++++++++++ src/tests/plugin/processing.rs | 198 ------------ 5 files changed, 490 insertions(+), 214 deletions(-) create mode 100644 src/plugin/ext/configurable_audio_ports.rs create mode 100644 src/tests/plugin/layout.rs diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index e562f5d..425f286 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -7,6 +7,7 @@ use std::ptr::NonNull; pub mod audio_ports; pub mod audio_ports_config; +pub mod configurable_audio_ports; pub mod latency; pub mod note_ports; pub mod params; diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs new file mode 100644 index 0000000..710bdf6 --- /dev/null +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -0,0 +1,109 @@ +use crate::{ + plugin::{ + assert_plugin_state, + ext::Extension, + instance::{Plugin, PluginStatus}, + }, + util::unsafe_clap_call, +}; +use clap_sys::ext::{ + audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}, + configurable_audio_ports::{ + clap_audio_port_configuration_request, clap_plugin_configurable_audio_ports, + CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, + }, +}; +use std::{ + ffi::CStr, + ptr::{null, NonNull}, +}; + +/// TODO: surround/ambisonic extensions? +#[derive(Debug, Clone, Copy)] +pub struct AudioPortsRequest { + pub is_input: bool, + pub port_index: u32, + pub channel_count: u32, +} + +pub struct ConfigurableAudioPorts<'a> { + plugin: &'a Plugin<'a>, + configurable_audio_ports: NonNull, +} + +impl<'a> Extension<&'a Plugin<'a>> for ConfigurableAudioPorts<'a> { + const EXTENSION_ID: &'static CStr = CLAP_EXT_CONFIGURABLE_AUDIO_PORTS; + + type Struct = clap_plugin_configurable_audio_ports; + + fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + configurable_audio_ports: extension_struct, + } + } +} + +impl<'a> ConfigurableAudioPorts<'a> { + pub fn can_apply_configuration( + &self, + requests: impl IntoIterator, + ) -> bool { + assert_plugin_state!(self.plugin, state < PluginStatus::Activated); + + let requests = requests + .into_iter() + .map(|r| clap_audio_port_configuration_request { + is_input: r.is_input, + port_index: r.port_index, + channel_count: r.channel_count, + port_details: null(), + port_type: match r.channel_count { + 1 => CLAP_PORT_MONO.as_ptr(), + 2 => CLAP_PORT_STEREO.as_ptr(), + _ => null(), + }, + }) + .collect::>(); + + let plugin = self.plugin.as_ptr(); + let ext = self.configurable_audio_ports.as_ptr(); + + unsafe_clap_call! { ext=>can_apply_configuration( + plugin, + requests.as_ptr(), + requests.len() as u32 + )} + } + + pub fn apply_configuration( + &self, + requests: impl IntoIterator, + ) -> bool { + assert_plugin_state!(self.plugin, state < PluginStatus::Activated); + + let requests = requests + .into_iter() + .map(|r| clap_audio_port_configuration_request { + is_input: r.is_input, + port_index: r.port_index, + channel_count: r.channel_count, + port_details: null(), + port_type: match r.channel_count { + 1 => CLAP_PORT_MONO.as_ptr(), + 2 => CLAP_PORT_STEREO.as_ptr(), + _ => null(), + }, + }) + .collect::>(); + + let plugin = self.plugin.as_ptr(); + let ext = self.configurable_audio_ports.as_ptr(); + + unsafe_clap_call! { ext=>can_apply_configuration( + plugin, + requests.as_ptr(), + requests.len() as u32 + )} + } +} diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 535fcb7..20a3d3e 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -3,7 +3,10 @@ use super::TestCase; use crate::{ plugin::{ - ext::{audio_ports_config::AudioPortsConfig, Extension}, + ext::{ + audio_ports_config::AudioPortsConfig, configurable_audio_ports::ConfigurableAudioPorts, + Extension, + }, library::PluginLibrary, }, tests::TestStatus, @@ -13,6 +16,7 @@ use clap::ValueEnum; use std::process::Command; mod descriptor; +mod layout; mod params; pub mod processing; mod state; @@ -27,14 +31,14 @@ pub enum PluginTestCase { FeaturesCategories, #[strum(serialize = "features-duplicates")] FeaturesDuplicates, + #[strum(serialize = "layout-audio-ports-config")] + LayoutAudioPortsConfig, + #[strum(serialize = "layout-configurable-audio-ports")] + LayoutConfigurableAudioPorts, #[strum(serialize = "process-audio-out-of-place-basic")] ProcessAudioOutOfPlaceBasic, #[strum(serialize = "process-audio-in-place-basic")] ProcessAudioInPlaceBasic, - #[strum(serialize = "process-audio-out-of-place-layouts")] - ProcessAudioOutOfPlaceConfig, - #[strum(serialize = "process-audio-in-place-layouts")] - ProcessAudioInPlaceConfig, #[strum(serialize = "process-audio-constant-mask")] ProcessAudioConstantMask, #[strum(serialize = "process-audio-reset-determinism")] @@ -104,13 +108,13 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses in-place audio processing for buses that support it.", ), - PluginTestCase::ProcessAudioOutOfPlaceConfig => format!( - "Performs the same test as {}, but this time it tries all available port \ - configurations exposed via the '{}' extension.", + PluginTestCase::LayoutConfigurableAudioPorts => format!( + "Performs the same test as {}, but this time it tries random configurations \ + exposed via the '{}' extension.", PluginTestCase::ProcessAudioOutOfPlaceBasic, - AudioPortsConfig::EXTENSION_ID.to_str().unwrap() + ConfigurableAudioPorts::EXTENSION_ID.to_str().unwrap() ), - PluginTestCase::ProcessAudioInPlaceConfig => format!( + PluginTestCase::LayoutAudioPortsConfig => format!( "Performs the same test as {}, but this time it tries all available port \ configurations exposed via the '{}' extension.", PluginTestCase::ProcessAudioInPlaceBasic, @@ -255,18 +259,18 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::FeaturesDuplicates => { descriptor::test_features_duplicates(library, plugin_id) } + PluginTestCase::LayoutAudioPortsConfig => { + layout::test_layout_audio_ports_config(library, plugin_id) + } + PluginTestCase::LayoutConfigurableAudioPorts => { + layout::test_layout_configurable_audio_ports(library, plugin_id) + } PluginTestCase::ProcessAudioOutOfPlaceBasic => { processing::test_process_audio_basic(library, plugin_id, false) } PluginTestCase::ProcessAudioInPlaceBasic => { processing::test_process_audio_basic(library, plugin_id, true) } - PluginTestCase::ProcessAudioOutOfPlaceConfig => { - processing::test_process_audio_config(library, plugin_id, false) - } - PluginTestCase::ProcessAudioInPlaceConfig => { - processing::test_process_audio_config(library, plugin_id, true) - } PluginTestCase::ProcessAudioConstantMask => { processing::test_process_audio_constant_mask(library, plugin_id) } diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs new file mode 100644 index 0000000..505d100 --- /dev/null +++ b/src/tests/plugin/layout.rs @@ -0,0 +1,360 @@ +use crate::{ + plugin::{ + ext::{ + audio_ports::{AudioPortConfig, AudioPorts}, + audio_ports_config::{AudioPortsConfig, AudioPortsConfigInfo}, + configurable_audio_ports::{AudioPortsRequest, ConfigurableAudioPorts}, + note_ports::NotePorts, + Extension, + }, + host::Host, + instance::process::{AudioBuffers, ProcessConfig, ProcessData}, + library::PluginLibrary, + }, + tests::{ + plugin::processing::run_simple, + rng::{new_prng, NoteGenerator}, + TestStatus, + }, +}; +use anyhow::{Context, Result}; +use rand::{seq::SliceRandom, Rng}; +use rand_pcg::Pcg32; + +const BUFFER_SIZE: usize = 512; + +/// The test for `PluginTestCase::LayoutAudioPortsConfig`. +pub fn test_layout_audio_ports_config( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports = match plugin.get_extension::() { + Some(audio_ports) => audio_ports, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let audio_ports_config_info = plugin.get_extension::(); + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports_config) => audio_ports_config, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPortsConfig::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); + + for config_audio_ports_config in audio_ports_config + .enumerate() + .context("Could not enumerate audio port configurations")? + { + audio_ports_config + .select(config_audio_ports_config.id) + .with_context(|| { + format!( + "Could not select audio port configuration '{}' ({})", + config_audio_ports_config.name, config_audio_ports_config.id, + ) + })?; + + let config_audio_ports = audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?; + + // Check that the audio-ports-config info matches the actual audio-ports config + { + let main_input_channels = config_audio_ports + .inputs + .first() + .filter(|x| x.is_main) + .map(|x| x.num_channels); + + let main_output_channels = config_audio_ports + .outputs + .first() + .filter(|x| x.is_main) + .map(|x| x.num_channels); + + anyhow::ensure!( + config_audio_ports.inputs.len() as u32 + == config_audio_ports_config.input_port_count, + "The number of input audio ports for configuration '{}' ({}) does not match the \ + number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + config_audio_ports_config.input_port_count, + config_audio_ports.inputs.len() as u32, + ); + + anyhow::ensure!( + config_audio_ports.outputs.len() as u32 + == config_audio_ports_config.output_port_count, + "The number of output audio ports for configuration '{}' ({}) does not match the \ + number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + config_audio_ports_config.output_port_count, + config_audio_ports.outputs.len() as u32, + ); + + match ( + main_input_channels, + config_audio_ports_config.main_input_channel_count, + ) { + (None, None) => {} + (Some(a), Some(b)) => anyhow::ensure!( + a == b, + "The number of channels in the main input port for the '{}' configuration \ + info ({}) does not match the number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + b, + a, + ), + (None, Some(_)) => { + anyhow::bail!( + "The configuration '{}' reports that a main input port exists, but \ + 'audio-ports' does not.", + config_audio_ports_config.name, + ) + } + (Some(_), None) => anyhow::bail!( + "The configuration '{}' reports that main input port does not exist, but \ + according to 'audio-ports' it does.", + config_audio_ports_config.name, + ), + } + + match ( + main_output_channels, + config_audio_ports_config.main_output_channel_count, + ) { + (None, None) => {} + (Some(a), Some(b)) => anyhow::ensure!( + a == b, + "The number of channels in the main output port for the '{}' configuration \ + info ({}) does not match the number reported by 'audio-ports' ({})", + config_audio_ports_config.name, + b, + a, + ), + (None, Some(_)) => { + anyhow::bail!( + "The configuration '{}' reports that a main output port exists, but \ + 'audio-ports' does not.", + config_audio_ports_config.name, + ) + } + (Some(_), None) => anyhow::bail!( + "The configuration '{}' reports that main output port does not exist, but \ + according to 'audio-ports' it does.", + config_audio_ports_config.name, + ), + } + } + + // Check that the audio-ports-config-info matches the current config + if let Some(audio_ports_config_info) = &audio_ports_config_info { + anyhow::ensure!( + audio_ports_config_info.current() == config_audio_ports_config.id, + "The current configuration ID reported by 'audio-ports-config-info' ({}) does not \ + match the last selected configuration ID ({})", + audio_ports_config_info.current(), + config_audio_ports_config.id, + ); + + // TODO: check info + } + + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); + + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with IO configuration '{}' ({})", + config_audio_ports_config.name, config_audio_ports_config.id, + ) + })?; + } + + // The `Host` contains built-in thread safety checks + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) +} + +/// The test for `PluginTestCase::LayoutConfigurableAudioPorts`. +pub fn test_layout_configurable_audio_ports( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { + fn random_layout_requests( + prng: &mut Pcg32, + config: &AudioPortConfig, + ) -> Vec { + let mut requests = Vec::new(); + + for (i, _) in config.inputs.iter().enumerate() { + requests.push(AudioPortsRequest { + is_input: true, + port_index: i as u32, + channel_count: prng.random_range(0..=8), + }); + } + + for (i, _) in config.outputs.iter().enumerate() { + requests.push(AudioPortsRequest { + is_input: false, + port_index: i as u32, + channel_count: prng.random_range(0..=8), + }); + } + + requests.shuffle(prng); + requests + } + + fn print_layout_requests(requests: &[AudioPortsRequest]) -> String { + let mut result = Vec::new(); + + for request in requests { + result.push(format!( + "{} {}: {}ch", + if request.is_input { "in" } else { "out" }, + request.port_index, + request.channel_count, + )); + } + + result.join(" ") + } + + let mut prng = new_prng(); + + let host = Host::new(); + let plugin = library + .create_plugin(plugin_id, host.clone()) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports = match plugin.get_extension::() { + Some(audio_ports) => audio_ports, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + AudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let configurable_audio_ports = match plugin.get_extension::() { + Some(extension) => extension, + None => { + return Ok(TestStatus::Skipped { + details: Some(format!( + "The plugin does not implement the '{}' extension.", + ConfigurableAudioPorts::EXTENSION_ID.to_str().unwrap(), + )), + }); + } + }; + + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); + + let config_audio_ports = audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?; + + let mut checks_total = 0; + let mut checks_passed = 0; + + while checks_total < 100 && checks_passed < 10 { + let requests = random_layout_requests(&mut prng, &config_audio_ports); + let can_apply = configurable_audio_ports.can_apply_configuration(requests.iter().cloned()); + let has_applied = configurable_audio_ports.apply_configuration(requests.iter().cloned()); + + if can_apply != has_applied { + anyhow::bail!( + "The plugin returned conflicting results from 'can_apply_configuration' ({}) and \ + 'apply_configuration' ({}) for the following layout: {}", + can_apply, + has_applied, + print_layout_requests(&requests), + ); + } + + checks_total += 1; + if has_applied { + checks_passed += 1; + } + + let config_audio_ports = audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?; + + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); + note_event_rng.fill_event_queue( + &mut prng, + &process_data.input_events, + process_data.block_size, + ); + + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with the following configuration: {}", + print_layout_requests(&requests) + ) + })?; + } + + // The `Host` contains built-in thread safety checks + host.callback_error_check() + .context("An error occured during a host callback")?; + Ok(TestStatus::Success { details: None }) +} diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 94afeaf..5ab0ecf 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -1,7 +1,6 @@ //! Contains most of the boilerplate around testing audio processing. use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; -use crate::plugin::ext::audio_ports_config::{AudioPortsConfig, AudioPortsConfigInfo}; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::Extension; use crate::plugin::host::Host; @@ -547,203 +546,6 @@ pub fn test_process_audio_reset_determinism( Ok(TestStatus::Success { details: None }) } -/// The test for `PluginTestCase::ProcessAudioOutOfPlaceConfig` and `PluginTestCase::ProcessAudioInPlaceConfig`. -pub fn test_process_audio_config( - library: &PluginLibrary, - plugin_id: &str, - in_place: bool, -) -> Result { - let mut prng = new_prng(); - - let host = Host::new(); - let plugin = library - .create_plugin(plugin_id, host.clone()) - .context("Could not create the plugin instance")?; - plugin.init().context("Error during initialization")?; - - let audio_ports = match plugin.get_extension::() { - Some(audio_ports) => audio_ports, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - AudioPorts::EXTENSION_ID.to_str().unwrap(), - )), - }); - } - }; - - let audio_ports_config_info = plugin.get_extension::(); - let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports_config) => audio_ports_config, - None => { - return Ok(TestStatus::Skipped { - details: Some(format!( - "The plugin does not implement the '{}' extension.", - AudioPortsConfig::EXTENSION_ID.to_str().unwrap(), - )), - }); - } - }; - - let note_ports_config = plugin - .get_extension::() - .map(|x| x.config()) - .transpose() - .context("Error while querying 'note-ports' IO configuration")? - .unwrap_or_default(); - - for config_audio_ports_config in audio_ports_config - .enumerate() - .context("Could not enumerate audio port configurations")? - { - audio_ports_config - .select(config_audio_ports_config.id) - .with_context(|| { - format!( - "Could not select audio port configuration '{}' ({})", - config_audio_ports_config.name, config_audio_ports_config.id, - ) - })?; - - let config_audio_ports = audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?; - - // Check that the audio-ports-config info matches the actual audio-ports config - { - let main_input_channels = config_audio_ports - .inputs - .first() - .filter(|x| x.is_main) - .map(|x| x.num_channels); - - let main_output_channels = config_audio_ports - .outputs - .first() - .filter(|x| x.is_main) - .map(|x| x.num_channels); - - anyhow::ensure!( - config_audio_ports.inputs.len() as u32 - == config_audio_ports_config.input_port_count, - "The number of input audio ports for configuration '{}' ({}) does not match the \ - number reported by 'audio-ports' ({})", - config_audio_ports_config.name, - config_audio_ports_config.input_port_count, - config_audio_ports.inputs.len() as u32, - ); - - anyhow::ensure!( - config_audio_ports.outputs.len() as u32 - == config_audio_ports_config.output_port_count, - "The number of output audio ports for configuration '{}' ({}) does not match the \ - number reported by 'audio-ports' ({})", - config_audio_ports_config.name, - config_audio_ports_config.output_port_count, - config_audio_ports.outputs.len() as u32, - ); - - match ( - main_input_channels, - config_audio_ports_config.main_input_channel_count, - ) { - (None, None) => {} - (Some(a), Some(b)) => anyhow::ensure!( - a == b, - "The number of channels in the main input port for the '{}' configuration \ - info ({}) does not match the number reported by 'audio-ports' ({})", - config_audio_ports_config.name, - b, - a, - ), - (None, Some(_)) => { - anyhow::bail!( - "The configuration '{}' reports that a main input port exists, but \ - 'audio-ports' does not.", - config_audio_ports_config.name, - ) - } - (Some(_), None) => anyhow::bail!( - "The configuration '{}' reports that main input port does not exist, but \ - according to 'audio-ports' it does.", - config_audio_ports_config.name, - ), - } - - match ( - main_output_channels, - config_audio_ports_config.main_output_channel_count, - ) { - (None, None) => {} - (Some(a), Some(b)) => anyhow::ensure!( - a == b, - "The number of channels in the main output port for the '{}' configuration \ - info ({}) does not match the number reported by 'audio-ports' ({})", - config_audio_ports_config.name, - b, - a, - ), - (None, Some(_)) => { - anyhow::bail!( - "The configuration '{}' reports that a main output port exists, but \ - 'audio-ports' does not.", - config_audio_ports_config.name, - ) - } - (Some(_), None) => anyhow::bail!( - "The configuration '{}' reports that main output port does not exist, but \ - according to 'audio-ports' it does.", - config_audio_ports_config.name, - ), - } - } - - // Check that the audio-ports-config-info matches the current config - if let Some(audio_ports_config_info) = &audio_ports_config_info { - anyhow::ensure!( - audio_ports_config_info.current() == config_audio_ports_config.id, - "The current configuration ID reported by 'audio-ports-config-info' ({}) does not \ - match the last selected configuration ID ({})", - audio_ports_config_info.current(), - config_audio_ports_config.id, - ); - - // TODO: check info - } - - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = if in_place { - AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE) - } else { - AudioBuffers::new_out_of_place_f32(&config_audio_ports, BUFFER_SIZE) - }; - - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - - Ok(()) - }) - .with_context(|| { - format!( - "Error while processing audio with IO configuration '{}' ({})", - config_audio_ports_config.name, config_audio_ports_config.id, - ) - })?; - } - - // The `Host` contains built-in thread safety checks - host.callback_error_check() - .context("An error occured during a host callback")?; - Ok(TestStatus::Success { details: None }) -} - /// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, /// infinite, or denormal values, that the input buffers have not been modified by the plugin, and /// that the output event queue is monotonically ordered. From d316baba782d6c4ff77ccb017616c7845f3070cf Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sun, 18 Jan 2026 05:08:13 +0400 Subject: [PATCH 027/114] a lot of refactoring --- Cargo.lock | 35 +- Cargo.toml | 3 +- .rustfmt.toml => rustfmt.toml | 1 + src/commands/list.rs | 49 +- src/commands/validate.rs | 82 ++- src/main.rs | 41 +- src/plugin.rs | 52 -- src/plugin/ext/audio_ports.rs | 4 +- src/plugin/ext/audio_ports_config.rs | 22 +- src/plugin/ext/configurable_audio_ports.rs | 31 +- src/plugin/ext/latency.rs | 18 +- src/plugin/ext/note_ports.rs | 4 +- src/plugin/ext/params.rs | 9 +- src/plugin/ext/preset_load.rs | 2 +- src/plugin/ext/state.rs | 70 +-- src/plugin/host.rs | 24 +- src/plugin/instance.rs | 75 ++- src/plugin/instance/audio_thread.rs | 11 +- src/plugin/instance/process.rs | 110 ++-- src/plugin/library.rs | 4 +- src/plugin/preset_discovery/indexer.rs | 20 +- .../preset_discovery/metadata_receiver.rs | 27 +- src/tests.rs | 138 ++--- src/tests/plugin.rs | 40 +- src/tests/plugin/layout.rs | 33 +- src/tests/plugin/params.rs | 18 +- src/tests/plugin/processing.rs | 6 +- src/tests/plugin/state.rs | 22 +- src/tests/plugin_library.rs | 22 +- src/tests/plugin_library/factories.rs | 4 +- src/tests/plugin_library/preset_discovery.rs | 8 +- src/tests/rng.rs | 10 +- src/util.rs | 109 ++-- src/validator.rs | 486 +++++++----------- 34 files changed, 732 insertions(+), 858 deletions(-) rename .rustfmt.toml => rustfmt.toml (53%) diff --git a/Cargo.lock b/Cargo.lock index 829ec54..0f735a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -171,7 +171,6 @@ dependencies = [ "parking_lot", "rand", "rand_pcg", - "rayon", "regex", "serde", "serde_json", @@ -528,16 +527,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.6" @@ -650,28 +639,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - [[package]] name = "redox_syscall" version = "0.3.5" diff --git a/Cargo.toml b/Cargo.toml index 628081f..d2f762c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap-validator" version = "0.3.2" -edition = "2021" +edition = "2024" license = "MIT" rust-version = "1.87.0" # MSRV @@ -26,7 +26,6 @@ midi-consts = "0.1.0" parking_lot = "0.12.1" rand = "0.9.2" rand_pcg = "0.9.0" -rayon = "1.6.1" regex = "1.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/.rustfmt.toml b/rustfmt.toml similarity index 53% rename from .rustfmt.toml rename to rustfmt.toml index a5a6806..b953752 100644 --- a/.rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ format_strings = true +comment_width = 100 \ No newline at end of file diff --git a/src/commands/list.rs b/src/commands/list.rs index d4bf907..d921091 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,19 +1,54 @@ //! Commands for listing information about the validator or installed plugins. +use super::{TextWrapper, println_wrapped, println_wrapped_no_indent}; +use crate::index::PresetIndexResult; +use crate::plugin::preset_discovery::PresetFile; use anyhow::{Context, Result}; +use clap::Subcommand; use colored::Colorize; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::ExitCode; -use super::{println_wrapped, println_wrapped_no_indent, TextWrapper}; -use crate::index::PresetIndexResult; -use crate::plugin::preset_discovery::PresetFile; +/// Commands for listing tests and data realted to the installed plugins. +#[derive(Subcommand)] +pub enum ListCommand { + /// Lists basic information about all installed CLAP plugins. + Plugins { + /// Print JSON instead of a human readable format. + #[arg(short, long)] + json: bool, + }, + /// Lists the available presets for one, more, or all installed CLAP plugins. + Presets { + /// Print JSON instead of a human readable format. + #[arg(short, long)] + json: bool, + /// Paths to one or more plugins that should be indexed for presets, optional. + /// + /// All installed plugins are crawled if this value is missing. + paths: Option>, + }, + /// Lists all available test cases. + Tests { + /// Print JSON instead of a human readable format. + #[arg(short, long)] + json: bool, + }, +} + +pub fn list(command: &ListCommand) -> Result { + match command { + ListCommand::Plugins { json } => list_plugins(*json), + ListCommand::Presets { json, paths } => list_presets(*json, paths.as_deref()), + ListCommand::Tests { json } => list_tests(*json), + } +} // TODO: The indexing here always happens in the same process. We should move this over to out of // process scanning at some point. /// Lists basic information about all installed CLAP plugins. -pub fn plugins(json: bool) -> Result { +fn list_plugins(json: bool) -> Result { let plugin_index = crate::index::index(); if json { @@ -79,7 +114,7 @@ pub fn plugins(json: bool) -> Result { } /// Lists presets for one, more, or all plugins. -pub fn presets

(json: bool, plugin_paths: Option<&[P]>) -> Result +fn list_presets

(json: bool, plugin_paths: Option<&[P]>) -> Result where P: AsRef, { @@ -341,7 +376,7 @@ where } /// Lists all available test cases. -pub fn tests(json: bool) -> Result { +fn list_tests(json: bool) -> Result { let list = crate::tests::TestList::default(); if json { diff --git a/src/commands/validate.rs b/src/commands/validate.rs index da880bd..561a5cc 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -1,14 +1,77 @@ //! Commands for validating plugins. -use std::process::ExitCode; - +use super::{TextWrapper, println_wrapped}; +use crate::tests::TestStatus; +use crate::{Verbosity, validator}; use anyhow::{Context, Result}; +use clap::Args; use colored::Colorize; +use std::path::PathBuf; +use std::process::ExitCode; -use super::{println_wrapped, TextWrapper}; -use crate::tests::TestStatus; -use crate::validator::{self, SingleTestSettings, ValidatorSettings}; -use crate::Verbosity; +/// Options for the validator. +#[derive(Debug, Args)] +pub struct ValidatorSettings { + /// Paths to one or more plugins that should be validated. + #[arg(required = true)] + pub paths: Vec, + /// Only validate plugins with this ID. + /// + /// If the plugin library contains multiple plugins, then you can pass a single plugin's ID + /// to this option to only validate that plugin. Otherwise all plugins in the library are + /// validated. + #[arg(short = 'i', long)] + pub plugin_id: Option, + /// Print the test output as JSON instead of human readable text. + #[arg(long)] + pub json: bool, + /// Only run the tests that match this case-insensitive regular expression. + #[arg(short = 'f', long)] + pub test_filter: Option, + /// Changes the behavior of -f/--test-filter to skip matching tests instead. + #[arg(short = 'v', long)] + pub invert_filter: bool, + /// When running the validation out-of-process, hide the plugin's output. + /// + /// This can be useful for validating noisy plugins. + #[arg(long)] + pub hide_output: bool, + /// Only show failed tests. + /// + /// This affects both the human readable and the JSON output. + #[arg(long)] + pub only_failed: bool, + /// Run the tests within this process. + /// + /// Tests are normally run in separate processes in case the plugin crashes. Another benefit + /// of the out-of-process validation is that the test always starts from a clean state. + /// Using this option will remove those protections, but in turn the tests may run faster. + #[arg(long)] + pub in_process: bool, + /// Don't run tests in parallel. + /// + /// This will cause the out-of-process tests to be run sequentially. Implied when the + /// --in-process option is used. Can be useful for keeping plugin output in the correct order. + #[arg(long, conflicts_with = "in_process")] + pub no_parallel: bool, +} + +/// Options for running a single test. This is used for the out-of-process testing method. This +/// option is hidden from the CLI as it's merely an implementation detail. +#[derive(Debug, Args)] +pub struct SingleTestSettings { + /// The type of test (plugin library or plugin) to run. + pub test_type: String, + /// The name of the test to run. + pub test_name: String, + /// The serialized test data as JSON. + pub test_data: String, + + /// The name of the file to write the test's JSON result to. This is not done through STDIO + /// because the hosted plugin may also write things there. + #[arg(long)] + pub output_file: PathBuf, +} /// The main validator command. This will validate one or more plugins and print the results. pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { @@ -141,9 +204,6 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result

{ /// /// # Safety /// The extension struct pointer must be a valid pointer to the correct extension struct for - /// the plugin instance and given EXTENSION_ID. + /// the plugin instance and given `IDS`. unsafe fn new(plugin: P, extension_struct: NonNull) -> Self; } diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index db5330f..679aec3 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -20,7 +20,6 @@ use crate::util::unsafe_clap_call; use super::Extension; /// Abstraction for the `audio-ports` extension covering the main thread functionality. -#[derive(Debug)] pub struct AudioPorts<'a> { plugin: &'a Plugin<'a>, audio_ports: NonNull, diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index e867cd3..d50a086 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -12,13 +12,11 @@ use std::ffi::CStr; use std::mem::zeroed; use std::ptr::NonNull; -#[derive(Debug)] pub struct AudioPortsConfig<'a> { plugin: &'a Plugin<'a>, audio_ports_config: NonNull, } -#[derive(Debug)] pub struct AudioPortsConfigInfo<'a> { plugin: &'a Plugin<'a>, audio_ports_config_info: NonNull, diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index b070a01..a51904d 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -1,5 +1,5 @@ use crate::plugin::ext::Extension; -use crate::plugin::instance::{Plugin, PluginStatus}; +use crate::plugin::instance::Plugin; use crate::util::unsafe_clap_call; use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_plugin_latency}; use std::ffi::CStr; @@ -27,8 +27,6 @@ impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { impl<'a> Latency<'a> { #[allow(unused)] pub fn get(&self) -> u32 { - self.plugin.status().assert_is(PluginStatus::Activating); - let latency = self.latency.as_ptr(); let plugin = self.plugin.as_ptr(); unsafe_clap_call! { latency=>get(plugin) } diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index ea1c142..a0ae99b 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -14,7 +14,6 @@ use std::mem; use std::ptr::NonNull; /// Abstraction for the `note-ports` extension covering the main thread functionality. -#[derive(Debug)] pub struct NotePorts<'a> { plugin: &'a Plugin<'a>, note_ports: NonNull, diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 0e448ed..c26ad93 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -26,7 +26,6 @@ use crate::util::{self, c_char_slice_to_string, unsafe_clap_call}; pub type ParamInfo = BTreeMap; /// Abstraction for the `params` extension covering the main thread functionality. -#[derive(Debug)] pub struct Params<'a> { plugin: &'a Plugin<'a>, params: NonNull, diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 45c6673..cf93ce4 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -12,7 +12,6 @@ use crate::util::unsafe_clap_call; use super::Extension; /// Abstraction for the `preset-load` extension covering the main thread functionality. -#[derive(Debug)] pub struct PresetLoad<'a> { plugin: &'a Plugin<'a>, preset_load: NonNull, diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 10c94c2..6a0799a 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -14,7 +14,6 @@ use crate::plugin::instance::Plugin; use crate::util::{check_null_ptr, unsafe_clap_call}; /// Abstraction for the `state` extension covering the main thread functionality. -#[derive(Debug)] pub struct State<'a> { plugin: &'a Plugin<'a>, state: NonNull, diff --git a/src/plugin/host.rs b/src/plugin/host.rs deleted file mode 100644 index c2776d7..0000000 --- a/src/plugin/host.rs +++ /dev/null @@ -1,837 +0,0 @@ -//! Data structures and utilities for hosting plugins. - -use anyhow::{Context, Result}; -use clap_sys::ext::audio_ports::{ - CLAP_AUDIO_PORTS_RESCAN_NAMES, CLAP_EXT_AUDIO_PORTS, clap_host_audio_ports, -}; -use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_host_latency}; -use clap_sys::ext::note_ports::{ - CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, - CLAP_NOTE_DIALECT_MIDI_MPE, CLAP_NOTE_PORTS_RESCAN_ALL, CLAP_NOTE_PORTS_RESCAN_NAMES, - clap_host_note_ports, clap_note_dialect, -}; -use clap_sys::ext::params::{ - CLAP_EXT_PARAMS, CLAP_PARAM_RESCAN_ALL, CLAP_PARAM_RESCAN_INFO, CLAP_PARAM_RESCAN_TEXT, - CLAP_PARAM_RESCAN_VALUES, clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, -}; -use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; -use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; -use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_host_tail}; -use clap_sys::ext::thread_check::{CLAP_EXT_THREAD_CHECK, clap_host_thread_check}; -use clap_sys::ext::voice_info::{CLAP_EXT_VOICE_INFO, clap_host_voice_info}; -use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; -use clap_sys::host::clap_host; -use clap_sys::id::clap_id; -use clap_sys::plugin::clap_plugin; -use clap_sys::version::CLAP_VERSION; -use crossbeam::atomic::AtomicCell; -use crossbeam::channel; -use crossbeam::queue::SegQueue; -use parking_lot::Mutex; -use std::cell::RefCell; -use std::collections::HashMap; -use std::ffi::{CStr, CString, c_void}; -use std::os::raw::c_char; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::thread::ThreadId; - -use crate::plugin::instance::{PluginHandle, PluginStatus}; -use crate::plugin::preset_discovery::LocationValue; -use crate::util::{self, check_null_ptr, unsafe_clap_call}; - -/// An abstraction for a CLAP plugin host. -/// -/// - It handles callback requests made by the plugin, and it checks whether the calling thread -/// matches up when any of its functions are called by the plugin. A `Result` indicating the first -/// failure, of any, can be retrieved by calling the -/// [`callback_error_check()`][Self::callback_error_check()] method. -/// - In order for those calblacks to be handled correctly every CLAP function call where the plugin -/// potentially requests a main thread callback [`Host::handle_callbacks_once()`] needs to be -/// called. Alternatively [`Host::handle_callbacks_blocking()`] can be called on the main thread -/// while other audio threads are doing their thing. -/// - Multiple plugins can share this host instance. Because of that, we can't just cast the `*const -/// clap_host` directly to a `*const Host`, as that would make it impossible to figure out which -/// `*const clap_host` belongs to which plugin instance. Instead, every registered plugin instance -/// gets their own `InstanceState` which provides a `clap_host` struct unique to that plugin -/// instance. This can be linked back to both the plugin instance and the shared `Host`. -#[derive(Debug)] -pub struct Host { - /// The ID of the main thread. - main_thread_id: ThreadId, - /// A description of the first error encountered during a callback by this `Host`, if any. This - /// is primarily used to check that the plugin called all host callbacks from the correct thread - /// after the rest of the test has succeeded. - callback_error: RefCell>, - - /// These are the plugin instances taht were registered on this host. They're added here when - /// the `Plugin` object is created, and they're removed when the object is dropped. This is used - /// to keep track of audio threads and pending callbacks. - instances: RefCell>>>, - - /// Allows waking up the main thread for callbacks while running - /// [`handle_callbacks_blocking()`][Self::handle_callbacks_blocking()]. Other threads can also - /// use this to cause the function to return. - pub callback_task_sender: channel::Sender, - /// Used for handling callbacks on the main thread during - /// [`handle_callbacks_blocking()`][Self::handle_callbacks_blocking()]. - callback_task_receiver: channel::Receiver, - - // These are the vtables for the extensions supported by the host - clap_host_audio_ports: clap_host_audio_ports, - clap_host_note_ports: clap_host_note_ports, - clap_host_params: clap_host_params, - clap_host_preset_load: clap_host_preset_load, - clap_host_state: clap_host_state, - clap_host_thread_check: clap_host_thread_check, - clap_host_latency: clap_host_latency, - clap_host_tail: clap_host_tail, - clap_host_voice_info: clap_host_voice_info, -} - -/// Runtime information about a plugin instance. This keeps track of pending callbacks and things -/// like audio threads. It also contains the plugin's unique `clap_host` struct so host callbacks -/// can be linked back to this specific plugin instance. -#[derive(Debug)] -pub struct InstanceState { - /// The plugin this `InstanceState` is associated with. This is the same as they key in the - /// `Host::instances` hash map, but it also needs to be stored here to make it possible to - /// know what plugin instance a `*const clap_host` refers to. - /// - /// This is an `Option` because the plugin handle is only known after the plugin has been - /// created, and the factory's `create_plugin()` function requires a pointer to the `clap_host`. - pub plugin: AtomicCell>, - /// The host this `InstanceState` belongs to. This is needed to get back to the `Host` - /// instance from a `*const clap_host`, which we can cast to this struct to access the pointer. - host: Rc, - - /// The `clap-validator` version. Read at compile time, but it has to be stored here since it - /// needs to be null terminated. - _clap_validator_version: CString, - /// The vtable that's passed to the plugin. The `host_data` field is populated with a pointer to - /// this object. - clap_host: Mutex, - - pub callback_events: SegQueue, - - /// The plugin's current state in terms of activation and processing status. - pub status: AtomicCell, - - /// The plugin instance's audio thread, if it has one. Used for the audio thread checks. - pub audio_thread: AtomicCell>, - /// Whether the plugin has called `clap_host::request_callback()` and expects - /// `clap_plugin::on_main_thread()` to be called on the main thread. - pub requested_callback: AtomicBool, - /// Whether the plugin has called `clap_host::request_restart()` and expects the plugin to be - /// deactivated and subsequently reactivated. - /// - /// This flag is reset at the start of the `ProcessingTest::run*` functions, and it will cause - /// the multi-loop - /// [`ProcessingTest::run`][crate::testa::plugin::processing::ProcessingTest::run] function to - /// deactivate and reactivate. - pub requested_restart: AtomicBool, -} - -/// When the host is handling callbacks in a blocking fashion, other threads can send tasks over the -/// channel to either wake up the main thread to make it check for outstanding work, or to have it -/// return and stop blocking. -pub enum CallbackTask { - /// Check the registered plugin instances for outstanding callbacks and perform them as needed. - /// The combined use of polling and channels may seem a bit odd, but this is done to have a - /// thread-safe way to avoid multiple sequential callback requests from stacking up. If the - /// plugin calls `clap_host::request_callback()` ten times in a row, then we only need to call - /// `clap_plugin::on_main()` once. - Poll, - /// Stop blocking and return from [`Host::handle_callbacks_blocking()`]. - Stop, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum CallbackEvent { - RequestProcess, - RequestFlush, - RescanParamsValues, - RescanParamsText, - RescanParamsInfo, - RescanParamsAll, - RescanAudioPortsNames, - RescanAudioPortsAll, - RescanNotePortsNames, - RescanNotePortsAll, - ChangedLatency, - ChangedTail, - ChangedVoiceInfo, - ChangedState, -} - -impl InstanceState { - /// Construct a new plugin instance object. The [`InstanceState::plugin`] field must be set - /// later because the `clap_host` struct needs to be passed to `clap_factory::create_plugin()`, - /// and the plugin instance pointer is only known after that point. This contains the - /// `clap_host` vtable for this plugin instance, and keeps track of things like the instance's - /// audio thread and pending callbacks. The `Pin` is necessary to prevent moving the object out - /// of the `Arc`, since that would break pointers to the `InstanceState`. - pub fn new(host: Rc) -> Pin> { - let clap_validator_version = - CString::new(env!("CARGO_PKG_VERSION")).expect("Invalid bytes in crate version"); - let instance = Arc::pin(Self { - plugin: AtomicCell::new(None), - host, - - clap_host: Mutex::new(clap_host { - clap_version: CLAP_VERSION, - // This is populated with a pointer to the `Arc`'s data after creating the Arc - host_data: std::ptr::null_mut(), - name: c"clap-validator".as_ptr(), - vendor: c"Robbert van der Helm".as_ptr(), - url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: clap_validator_version.as_ptr(), - get_extension: Some(Host::get_extension), - request_restart: Some(Host::request_restart), - request_process: Some(Host::request_process), - request_callback: Some(Host::request_callback), - }), - _clap_validator_version: clap_validator_version, - - callback_events: SegQueue::new(), - status: AtomicCell::new(PluginStatus::default()), - - audio_thread: AtomicCell::new(None), - requested_callback: AtomicBool::new(false), - requested_restart: AtomicBool::new(false), - }); - - // We need to get the pointer to the pinned `InstanceState` into the `clap_host::host_data` - // field - instance.clap_host.lock().host_data = &*instance as *const Self as *mut c_void; - instance - } - - /// Get the `InstanceState` and the host from a valid `clap_host` pointer. - pub unsafe fn from_clap_host_ptr<'a>(ptr: *const clap_host) -> (&'a InstanceState, &'a Host) { - unsafe { - // This should have already been asserted before calling this function, but this is a - // validator and you can never be too sure - assert!(!ptr.is_null() && !(*ptr).host_data.is_null()); - - let this = &*((*ptr).host_data as *const Self); - (this, &*this.host) - } - } - - /// Get the host instance if this is called from the main thread. Returns `None` if this is not - /// the case. - pub fn host(&self) -> Option<&Host> { - if std::thread::current().id() == self.host.main_thread_id { - Some(&*self.host) - } else { - None - } - } - - /// Get a pointer to the `clap_host` struct for this instance. This uniquely identifies the - /// instance. - pub fn clap_host_ptr(self: &Pin>) -> *const clap_host { - // The value will not move, so this is safe - self.clap_host.data_ptr() - } - - /// Get a pointer to the `clap_plugin` struct for this instance. - /// - /// # Panics - /// - /// If the `plugin field has not yet been set. - pub fn plugin_ptr(&self) -> *const clap_plugin { - self.plugin - .load() - .expect("The 'plugin' field has not yet been set on this 'InstanceState'") - .0 - .as_ptr() - } -} - -impl Drop for Host { - fn drop(&mut self) { - if let Some(error) = self.callback_error.borrow_mut().take() { - // not an error because this can happen on a failing test - log::trace!( - "The validator's host has detected a callback error but this error has not been \ - used as part of the test result. This could be a clap-validator bug. The error \ - message is: {error}" - ) - } - } -} - -impl Host { - /// Initialize a CLAP host. The thread this object is created on will be designated as the main - /// thread for the purposes of the thread safety checks. - pub fn new() -> Rc { - // Normally you'd of course use bounded channel to avoid unnecessary allocations, but since - // we're a validator it's probably better to not have to deal with the possibility that a - // queue is full. These are used for handling callbacks on the main thread while the audio - // thread is active. - let (callback_task_sender, callback_task_receiver) = channel::unbounded(); - - Rc::new(Host { - main_thread_id: std::thread::current().id(), - // If the plugin never makes callbacks from the wrong thread, then this will remain an - // None`. Otherwise this will be replaced by the first error. - callback_error: RefCell::new(None), - - instances: RefCell::new(HashMap::new()), - callback_task_sender, - callback_task_receiver, - - clap_host_audio_ports: clap_host_audio_ports { - is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), - rescan: Some(Self::ext_audio_ports_rescan), - }, - clap_host_note_ports: clap_host_note_ports { - supported_dialects: Some(Self::ext_note_ports_supported_dialects), - rescan: Some(Self::ext_note_ports_rescan), - }, - clap_host_preset_load: clap_host_preset_load { - on_error: Some(Self::ext_preset_load_on_error), - loaded: Some(Self::ext_preset_load_loaded), - }, - clap_host_params: clap_host_params { - rescan: Some(Self::ext_params_rescan), - clear: Some(Self::ext_params_clear), - request_flush: Some(Self::ext_params_request_flush), - }, - clap_host_state: clap_host_state { - mark_dirty: Some(Self::ext_state_mark_dirty), - }, - clap_host_thread_check: clap_host_thread_check { - is_main_thread: Some(Self::ext_thread_check_is_main_thread), - is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), - }, - clap_host_latency: clap_host_latency { - changed: Some(Self::ext_latency_changed), - }, - clap_host_tail: clap_host_tail { - changed: Some(Self::ext_tail_changed), - }, - clap_host_voice_info: clap_host_voice_info { - changed: Some(Self::ext_voice_info_changed), - }, - }) - } - - /// Register a plugin instance with the host. This is used to keep track of things like audio - /// thread IDs and pending callbacks. This also contains the `*const clap_host` that should be - /// paased to the plugin when its created. - /// - /// The plugin should be unregistered using - /// [`unregister_instance()`][Self::unregister_instance()] when it gets destroyed. - /// - /// # Panics - /// - /// Panics if `instance.plugin` is `None`, or if the instance has already been registered. - pub fn register_instance(&self, instance: Pin>) { - let previous_instance = self.instances.borrow_mut().insert( - instance.plugin.load().expect( - "'InstanceState::plugin' should contain the plugin's handle when registering it \ - with the host", - ), - instance.clone(), - ); - assert!( - previous_instance.is_none(), - "The plugin instance has already been registered" - ); - } - - /// Remove a plugin from the list of registered plugins. - pub fn unregister_instance(&self, instance: Pin>) { - let removed_instance = self - .instances - .borrow_mut() - .remove(&instance.plugin.load().expect( - "'InstanceState::plugin' should contain the plugin's handle when unregistering it \ - with the host", - )) - .expect( - "Tried unregistering a plugin instance that has not been registered with the host", - ); - - if removed_instance.requested_callback.load(Ordering::SeqCst) { - log::warn!( - "A plugin still had unhandled callbacks when it was removed. This is a \ - clap-validator bug." - ) - } - } - - /// Handle main thread callbacks until [`CallbackTask::Stop`] is send to - /// [`Host::callback_task_sender`] from another thread. - pub fn handle_callbacks_blocking(&self) { - let mut should_stop = false; - loop { - if should_stop { - break; - } - - let task = self.callback_task_receiver.recv().unwrap(); - if matches!(task, CallbackTask::Stop) { - should_stop = true; - } - - // Flush all poll messages, if the plugin rapid fired a bunch of callbacks at us. We - // only keep track of a single request per callback type to avoid these things from - // unnecessarily stacking up. - while let Ok(callback) = self.callback_task_receiver.try_recv() { - match callback { - CallbackTask::Poll => (), - CallbackTask::Stop => should_stop = true, - } - } - - // This function will handle up to ten recursive callback requests. We'll do this even - // if the handler should be stopped to make sure we did not miss any outstanding events. - self.handle_callbacks_once(); - } - } - - /// Handle pending main thread callbacks. If a callback results in another callback, this is - /// allowed to loop up to ten times. - pub fn handle_callbacks_once(&self) { - let instances = self.instances.borrow(); - for i in 0..10 { - let mut handled_callback = false; - for instance in instances.values() { - let plugin_ptr = instance.plugin_ptr(); - if instance - .requested_callback - .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) - .is_ok() - { - log::trace!( - "Calling 'clap_plugin::on_main_thread()' in response to a call to \ - 'clap_host::request_restart()'", - ); - unsafe_clap_call! { plugin_ptr=>on_main_thread(plugin_ptr) }; - handled_callback = true; - } - } - - if !handled_callback { - if i > 1 { - log::trace!( - "The plugin recursively requested callbacks {} times in a row", - i - ) - } - - return; - } - } - - log::warn!( - "The plugin recursively called 'clap_host::on_main_thread()'. Aborted after ten \ - iterations." - ) - } - - /// Check if any of the host's callbacks were called from the wrong thread. Returns the first - /// error if this happened. If there were errors and this function is not called before the - /// object is destroyed, an error will be logged. - pub fn callback_error_check(&self) -> Result<()> { - match self.callback_error.borrow_mut().take() { - Some(err) => anyhow::bail!(err), - None => Ok(()), - } - } - - /// Set the callback error field if it does not already contain a value. Earlier errors are not - /// overwritten. - fn set_callback_error(&self, error: impl Into) { - let mut callback_error = self.callback_error.borrow_mut(); - if callback_error.is_none() { - *callback_error = Some(error.into()); - } - } - - /// Checks whether this is the main thread. If it is not, then an error indicating this can be - /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - fn assert_main_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if current_thread_id != self.main_thread_id { - self.set_callback_error(format!( - "'{}' may only be called from the main thread (thread {:?}), but it was called \ - from thread {:?}.", - function_name, self.main_thread_id, current_thread_id - )); - } - } - - /// Checks whether this is the audio thread. If it is not, then an error indicating this can be - /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - #[allow(unused)] - fn assert_audio_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if !self.is_audio_thread(current_thread_id) { - if current_thread_id == self.main_thread_id { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called \ - from the main thread." - )); - } else { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called \ - from an unknown thread." - )); - } - } - } - - /// Checks whether this is **not** the audio thread. If it is, then an error indicating this can - /// be retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - fn assert_not_audio_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if self.is_audio_thread(current_thread_id) { - self.set_callback_error(format!( - "'{function_name}' was called from an audio thread, this is not allowed.", - )); - } - } - - /// Returns whether the thread ID is one of the registered audio threads. - fn is_audio_thread(&self, thread_id: ThreadId) -> bool { - self.instances - .borrow() - .values() - .any(|instance| instance.audio_thread.load() == Some(thread_id)) - } - - unsafe extern "C" fn get_extension( - host: *const clap_host, - extension_id: *const c_char, - ) -> *const c_void { - check_null_ptr!(host, (*host).host_data, extension_id); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - // Right now there's no way to have the host only expose certain extensions. We can always - // add that when test cases need it. - let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; - if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { - &this.clap_host_audio_ports as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { - &this.clap_host_note_ports as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { - &this.clap_host_preset_load as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PARAMS { - &this.clap_host_params as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_STATE { - &this.clap_host_state as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { - &this.clap_host_thread_check as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_LATENCY { - &this.clap_host_latency as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_TAIL { - &this.clap_host_tail as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { - &this.clap_host_voice_info as *const _ as *const c_void - } else { - std::ptr::null() - } - } - - unsafe extern "C" fn request_restart(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, _) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and - // in the multi-iteration run function it will trigger a deactivate->reactivate cycle - log::trace!("'clap_host::request_restart()' was called by the plugin, setting the flag"); - instance.requested_restart.store(true, Ordering::SeqCst); - } - - unsafe extern "C" fn request_process(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, _) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - // Handling this within the context of the validator would be a bit messy. Do plugins use - // this? - log::trace!("'clap_host::request_process()' was called by the plugin"); - instance.callback_events.push(CallbackEvent::RequestProcess); - } - - unsafe extern "C" fn request_callback(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - // This this is either handled by `handle_callbacks_blocking()` while the audio thread is - // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the - // callback is not handled before the plugin is destroyed. - log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); - instance.requested_callback.store(true, Ordering::SeqCst); - this.callback_task_sender.send(CallbackTask::Poll).unwrap(); - } - - unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported( - host: *const clap_host, - _flag: u32, - ) -> bool { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); - log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); - true - } - - unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_audio_ports::rescan()"); - log::trace!("'clap_host_audio_ports::rescan()' was called"); - - if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - instance - .callback_events - .push(CallbackEvent::RescanAudioPortsNames); - } - - if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - if instance.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_audio_ports::rescan()' was called while the plugin was activated", - ); - } - - instance - .callback_events - .push(CallbackEvent::RescanAudioPortsAll); - } - } - - unsafe extern "C" fn ext_note_ports_supported_dialects( - host: *const clap_host, - ) -> clap_note_dialect { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_note_ports::supported_dialects()"); - log::trace!("'clap_host_note_ports::supported_dialects()' was called"); - - CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE - } - - unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_note_ports::rescan()"); - log::trace!("'clap_host_note_ports::rescan()' was called"); - - if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { - instance - .callback_events - .push(CallbackEvent::RescanNotePortsNames); - } - - if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { - if instance.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while \ - the plugin was activated", - ); - } - - instance - .callback_events - .push(CallbackEvent::RescanNotePortsAll); - } - } - - unsafe extern "C" fn ext_preset_load_on_error( - host: *const clap_host, - location_kind: clap_preset_discovery_location_kind, - location: *const c_char, - load_key: *const c_char, - os_error: i32, - msg: *const c_char, - ) { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_preset_load::on_error()"); - - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) }.context( - "'clap_host_preset_load::on_error()' called with an invalid load_key parameter", - ); - let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } - .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); - match (location, load_key, msg) { - (Ok(location), Ok(Some(load_key)), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with load key \ - {load_key}, OS error code {os_error}, and the following error message: {msg}" - )); - } - (Ok(location), Ok(None), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with no load key, \ - OS error code {os_error}, and the following error message: {msg}" - )); - } - (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - } - } - } - - unsafe extern "C" fn ext_preset_load_loaded( - host: *const clap_host, - location_kind: clap_preset_discovery_location_kind, - location: *const c_char, - load_key: *const c_char, - ) { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_preset_load::loaded()"); - - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } - .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); - match (location, load_key) { - (Ok(_location), Ok(_load_key)) => { - log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); - } - (Err(err), _) | (_, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - } - } - } - - unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_params::rescan()"); - log::trace!("'clap_host_params::rescan()' was called"); - - if flags & CLAP_PARAM_RESCAN_VALUES != 0 { - instance - .callback_events - .push(CallbackEvent::RescanParamsValues); - } - - if flags & CLAP_PARAM_RESCAN_TEXT != 0 { - instance - .callback_events - .push(CallbackEvent::RescanParamsText); - } - - if flags & CLAP_PARAM_RESCAN_INFO != 0 { - instance - .callback_events - .push(CallbackEvent::RescanParamsInfo); - } - - if flags & CLAP_PARAM_RESCAN_ALL != 0 { - if instance.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the \ - plugin is activated", - ); - } - - instance - .callback_events - .push(CallbackEvent::RescanParamsAll); - } - } - - unsafe extern "C" fn ext_params_clear( - host: *const clap_host, - _param_id: clap_id, - _flags: clap_param_clear_flags, - ) { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_params::clear()"); - log::debug!("TODO: Handle 'clap_host_params::clear()'"); - } - - unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_not_audio_thread("clap_host_params::request_flush()"); - log::trace!("'clap_host_params::request_flush()' was called"); - instance.callback_events.push(CallbackEvent::RequestFlush); - } - - unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_state::mark_dirty()"); - log::trace!("'clap_host_state::mark_dirty()' was called"); - instance.callback_events.push(CallbackEvent::ChangedState); - } - - unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - std::thread::current().id() == this.main_thread_id - } - - unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); - let (_, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.is_audio_thread(std::thread::current().id()) - } - - unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - if instance.status.load() != PluginStatus::Activating { - this.set_callback_error( - "'clap_host_latency::changed()' must only be called within \ - 'clap_plugin::activate()'", - ); - } - - this.assert_main_thread("clap_host_latency::changed()"); - log::trace!("'clap_host_latency::changed()' was called"); - instance.callback_events.push(CallbackEvent::ChangedLatency); - } - - unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_audio_thread("clap_host_tail::changed()"); - log::trace!("'clap_host_tail::changed()' was called"); - instance.callback_events.push(CallbackEvent::ChangedTail); - } - - unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let (instance, this) = unsafe { InstanceState::from_clap_host_ptr(host) }; - - this.assert_main_thread("clap_host_voice_info::changed()"); - log::trace!("'clap_host_voice_info::changed()' was called"); - instance - .callback_events - .push(CallbackEvent::ChangedVoiceInfo); - } -} diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 0a73441..c9d787c 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -1,56 +1,64 @@ //! Abstractions for single CLAP plugin instances for main thread interactions. -use anyhow::Result; +use super::ext::Extension; +use super::library::{PluginLibrary, PluginMetadata}; +use crate::plugin::preset_discovery::LocationValue; +use crate::util::{self, check_null_ptr, unsafe_clap_call}; +use anyhow::{Context, Result}; +use audio_thread::PluginAudioThread; +use clap_sys::ext::audio_ports::{ + CLAP_AUDIO_PORTS_RESCAN_NAMES, CLAP_EXT_AUDIO_PORTS, clap_host_audio_ports, +}; +use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_host_latency}; +use clap_sys::ext::note_ports::{ + CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, + CLAP_NOTE_DIALECT_MIDI_MPE, CLAP_NOTE_PORTS_RESCAN_ALL, CLAP_NOTE_PORTS_RESCAN_NAMES, + clap_host_note_ports, clap_note_dialect, +}; +use clap_sys::ext::params::{ + CLAP_EXT_PARAMS, CLAP_PARAM_RESCAN_ALL, CLAP_PARAM_RESCAN_INFO, CLAP_PARAM_RESCAN_TEXT, + CLAP_PARAM_RESCAN_VALUES, clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, +}; +use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; +use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; +use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_host_tail}; +use clap_sys::ext::thread_check::{CLAP_EXT_THREAD_CHECK, clap_host_thread_check}; +use clap_sys::ext::voice_info::{CLAP_EXT_VOICE_INFO, clap_host_voice_info}; use clap_sys::factory::plugin_factory::clap_plugin_factory; +use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; +use clap_sys::host::clap_host; +use clap_sys::id::clap_id; use clap_sys::plugin::clap_plugin; -use std::ffi::CStr; +use clap_sys::version::CLAP_VERSION; +use crossbeam::atomic::AtomicCell; +use crossbeam::queue::SegQueue; +use std::ffi::{CStr, c_char, c_void}; use std::marker::PhantomData; -use std::ops::Deref; -use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; +use std::panic::resume_unwind; use std::pin::Pin; use std::ptr::NonNull; -use std::rc::Rc; use std::sync::Arc; - -use super::ext::Extension; -use super::library::{PluginLibrary, PluginMetadata}; -use crate::plugin::host::{CallbackTask, Host, InstanceState}; -use crate::util::unsafe_clap_call; -use audio_thread::PluginAudioThread; +use std::thread::ThreadId; pub mod audio_thread; pub mod process; -/// A `Send+Sync` wrapper around `*const clap_plugin`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct PluginHandle(pub NonNull); - -unsafe impl Send for PluginHandle {} -unsafe impl Sync for PluginHandle {} - -/// A CLAP plugin instance. The plugin will be deinitialized when this object is dropped. All -/// functions here are callable only from the main thread. Use the -/// [`on_audio_thread()`][Self::on_audio_thread()] method to spawn an audio thread. -/// -/// All functions on `Plugin` and the objects created from it will panic if the plugin is not in the -/// correct state. -#[derive(Debug)] -pub struct Plugin<'lib> { - handle: PluginHandle, - /// Information about this plugin instance stored on the host. This keeps track of things like - /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - state: Pin>, - - /// The CLAP plugin library this plugin instance was created from. This field is not used - /// directly, but keeping a reference to the library here prevents the plugin instance from - /// outliving the library. - _library: &'lib PluginLibrary, - /// To honor CLAP's thread safety guidelines, the thread this object was created from is - /// designated the 'main thread', and this object cannot be shared with other threads. The - /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call - /// the plugin's audio thread functions. - _send_sync_marker: PhantomData<*const ()>, +pub enum CallbackEvent { + RequestProcess, + RequestFlush, + RescanParamsValues, + RescanParamsText, + RescanParamsInfo, + RescanParamsAll, + RescanAudioPortsNames, + RescanAudioPortsAll, + RescanNotePortsNames, + RescanNotePortsAll, + ChangedLatency, + ChangedTail, + ChangedVoiceInfo, + ChangedState, } /// The plugin's current lifecycle state. This is checked extensively to ensure that the plugin is @@ -113,24 +121,41 @@ impl PluginStatus { } } -/// An unsafe `Send` wrapper around [`Plugin`], needed to create the audio thread abstraction since -/// we artifically imposed `!Send`+`!Sync` on `Plugin` using the phantomdata marker. -struct PluginSendWrapper<'lib>(*const Plugin<'lib>); +/// A CLAP plugin instance. The plugin will be deinitialized when this object is dropped. All +/// functions here are callable only from the main thread. Use the +/// [`on_audio_thread()`][Self::on_audio_thread()] method to spawn an audio thread. +/// +/// All functions on `Plugin` and the objects created from it will panic if the plugin is not in the +/// correct state. +pub struct Plugin<'lib> { + handle: NonNull, -unsafe impl<'lib> Send for PluginSendWrapper<'lib> {} + /// Information about this plugin instance stored on the host. This keeps track of things like + /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. + state: Pin>, -/// This `Deref` wrapper works around the !Sync check check we would interwise run into if we -/// accessed the struct's value directly. -impl<'lib> Deref for PluginSendWrapper<'lib> { - type Target = *const Plugin<'lib>; + /// The CLAP plugin library this plugin instance was created from. This field is not used + /// directly, but keeping a reference to the library here prevents the plugin instance from + /// outliving the library. + _library: &'lib PluginLibrary, - fn deref(&self) -> &Self::Target { - &self.0 - } + /// To honor CLAP's thread safety guidelines, the thread this object was created from is + /// designated the 'main thread', and this object cannot be shared with other threads. The + /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call + /// the plugin's audio thread functions. + _send_sync_marker: PhantomData<*const ()>, } impl Drop for Plugin<'_> { fn drop(&mut self) { + if let Some(error) = self.state.callback_error.take() { + log::warn!( + "The validator's host has detected a callback error but this error has not been \ + used as part of the test result. This could be a clap-validator bug. The error \ + message is: {error}" + ) + } + // Make sure the plugin is in the correct state before it gets destroyed match self.status() { PluginStatus::Uninitialized | PluginStatus::Deactivated => (), @@ -145,8 +170,6 @@ impl Drop for Plugin<'_> { // plugin really shouldn't be making callbacks in deactivate() let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>destroy(plugin) }; - - self.host().unregister_instance(self.state.clone()); } } @@ -156,30 +179,22 @@ impl<'lib> Plugin<'lib> { /// unregistered when this object is dropped again. pub fn new( library: &'lib PluginLibrary, - host: Rc, factory: &clap_plugin_factory, plugin_id: &CStr, ) -> Result { - // The host can use this to keep track of things like audio threads and pending callbacks. - // The instance is remvoed again when this object is dropped. - let state = InstanceState::new(host.clone()); + let state = InstanceState::new(); let plugin = unsafe_clap_call! { factory=>create_plugin(factory, state.clap_host_ptr(), plugin_id.as_ptr()) }; + if plugin.is_null() { anyhow::bail!( "'clap_plugin_factory::create_plugin({plugin_id:?})' returned a null pointer." ); } - // We can only register the plugin instance with the host now because we did not have a - // plugin pointer before this. - let handle = PluginHandle(NonNull::new(plugin as *mut clap_plugin).unwrap()); - state.plugin.store(Some(handle)); - host.register_instance(state.clone()); - Ok(Plugin { - handle, + handle: NonNull::new(plugin as *mut clap_plugin).unwrap(), state, _library: library, @@ -189,7 +204,7 @@ impl<'lib> Plugin<'lib> { /// Get the raw pointer to the `clap_plugin` instance. pub fn as_ptr(&self) -> *const clap_plugin { - self.handle.0.as_ptr() + self.handle.as_ptr() } /// Get this plugin's metadata descriptor. In theory this should be the same as the one @@ -204,12 +219,9 @@ impl<'lib> Plugin<'lib> { PluginMetadata::from_descriptor(unsafe { &*descriptor }) } - /// Get the host for this plugin instance. - pub fn host(&self) -> &Host { - // `Plugin` can only be used from the main thread - self.state - .host() - .expect("Tried to get the host instance from a thread that isn't the main thread") + /// Get the reference to a thread-safe object containing information about this plugin instance. + pub fn state(&self) -> &InstanceState { + &self.state } /// The plugin's current initialization status. @@ -217,6 +229,21 @@ impl<'lib> Plugin<'lib> { self.state.status.load() } + /// Handle any pending main-thread callbacks for this plugin. + /// Returns an error if there is a callback error pending. + pub fn handle_callback(&self) -> Result<()> { + if self.state.requested_callback.swap(false) { + let plugin = self.as_ptr(); + unsafe_clap_call! { plugin=>on_main_thread(plugin) }; + } + + if let Some(error) = self.state.callback_error.take() { + anyhow::bail!(error); + } + + Ok(()) + } + /// Get the _main thread_ extension abstraction for the extension `T`, if the plugin supports /// this extension. Returns `None` if it does not. The plugin needs to be initialized using /// [`init()`][Self::init()] before this may be called. @@ -250,38 +277,63 @@ impl<'lib> Plugin<'lib> { &'a self, f: F, ) -> T { + struct SendWrapper<'lib>(&'lib Plugin<'lib>); + + // SAFETY: We artificially impose `!Send`+`!Sync` requirements on `Plugin` and + // `PluginAudioThread` to prevent them from being shared with other + // threads. But we'll need to temporarily lift that restriction in order + // to create this `PluginAudioThread`. + unsafe impl<'lib> Send for SendWrapper<'lib> {} + unsafe impl<'lib> Sync for SendWrapper<'lib> {} + + impl<'lib> SendWrapper<'lib> { + fn get(&self) -> &'lib Plugin<'lib> { + self.0 + } + } + self.status().assert_is(PluginStatus::Activated); - crossbeam::scope(|s| { - let unsafe_self_wrapper = PluginSendWrapper(self); - let callback_task_sender = self.host().callback_task_sender.clone(); + let is_running = AtomicCell::new(true); + let send_wrapper = SendWrapper(self); + crossbeam::scope(|s| { let audio_thread = s .builder() .name(String::from("audio-thread")) - .spawn(move |_| { - // SAFETY: We artificially impose `!Send`+`!Sync` requirements on `Plugin` and - // `PluginAudioThread` to prevent them from being shared with other - // threads. But we'll need to temporarily lift that restriction in order - // to create this `PluginAudioThread`. - let this = unsafe { &**unsafe_self_wrapper }; - - // The host may use this to assert that calls are run from an audio thread - this.state - .audio_thread - .store(Some(std::thread::current().id())); + .spawn(|_| { + struct SetFalseOnDrop<'a>(&'a AtomicCell); + impl<'a> Drop for SetFalseOnDrop<'a> { + fn drop(&mut self) { + self.0.store(false); + } + } - let result = catch_unwind(AssertUnwindSafe(|| f(PluginAudioThread::new(this)))); + let this = send_wrapper.get(); - this.state.audio_thread.store(None); - callback_task_sender.send(CallbackTask::Stop).unwrap(); + // So we know when to stop handling callbacks on the main thread + // even if the audio thread panics + let _guard = SetFalseOnDrop(&is_running); + + // This is used to check that calls are run from an audio thread + this.state + .audio_thread_id + .store(Some(std::thread::current().id())); - result.unwrap_or_else(|panic_info| resume_unwind(panic_info)) + f(PluginAudioThread::new(this)) }) .expect("Unable to spawn an audio thread"); // Handle callbacks requests on the main thread while the audio thread is running - self.host().handle_callbacks_blocking(); + while is_running.load() { + if self.state.requested_callback.swap(false) { + let plugin = self.as_ptr(); + unsafe_clap_call! { plugin=>on_main_thread(plugin) }; + } + + std::thread::sleep(std::time::Duration::from_millis(1)); + } + audio_thread .join() .unwrap_or_else(|panic_info| resume_unwind(panic_info)) @@ -350,3 +402,486 @@ impl<'lib> Plugin<'lib> { self.state.status.store(PluginStatus::Deactivated); } } + +/// Runtime information about a plugin instance. This keeps track of pending callbacks and things +/// like audio threads. It also contains the plugin's unique `clap_host` struct so host callbacks +/// can be linked back to this specific plugin instance. +pub struct InstanceState { + pub callback_events: SegQueue, + pub callback_error: AtomicCell>, + + /// The plugin's current state in terms of activation and processing status. + pub status: AtomicCell, + + /// The plugin instance's main thread. Used for the main thread checks. + pub main_thread_id: ThreadId, + + /// The plugin instance's audio thread, if it has one. Used for the audio thread checks. + pub audio_thread_id: AtomicCell>, + + /// Whether the plugin has called `clap_host::request_callback()` and expects + /// `clap_plugin::on_main_thread()` to be called on the main thread. + pub requested_callback: AtomicCell, + + /// Whether the plugin has called `clap_host::request_restart()` and expects the plugin to be + /// deactivated and subsequently reactivated. + pub requested_restart: AtomicCell, + + clap_host: clap_host, + clap_host_audio_ports: clap_host_audio_ports, + clap_host_note_ports: clap_host_note_ports, + clap_host_params: clap_host_params, + clap_host_preset_load: clap_host_preset_load, + clap_host_state: clap_host_state, + clap_host_thread_check: clap_host_thread_check, + clap_host_latency: clap_host_latency, + clap_host_tail: clap_host_tail, + clap_host_voice_info: clap_host_voice_info, +} + +impl InstanceState { + pub fn new() -> Pin> { + let main_thread = std::thread::current().id(); + let instance = Arc::pin(InstanceState { + callback_events: SegQueue::new(), + callback_error: AtomicCell::new(None), + + status: AtomicCell::new(PluginStatus::Uninitialized), + main_thread_id: main_thread, + audio_thread_id: AtomicCell::new(None), + requested_callback: AtomicCell::new(false), + requested_restart: AtomicCell::new(false), + + clap_host: clap_host { + clap_version: CLAP_VERSION, + // This is populated with a pointer to the `Arc`'s data after creating the Arc + host_data: std::ptr::null_mut(), + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), + version: c"0.1.0".as_ptr(), //TODO: use crate version + get_extension: Some(Self::get_extension), + request_restart: Some(Self::request_restart), + request_process: Some(Self::request_process), + request_callback: Some(Self::request_callback), + }, + + clap_host_audio_ports: clap_host_audio_ports { + is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), + rescan: Some(Self::ext_audio_ports_rescan), + }, + clap_host_note_ports: clap_host_note_ports { + supported_dialects: Some(Self::ext_note_ports_supported_dialects), + rescan: Some(Self::ext_note_ports_rescan), + }, + clap_host_preset_load: clap_host_preset_load { + on_error: Some(Self::ext_preset_load_on_error), + loaded: Some(Self::ext_preset_load_loaded), + }, + clap_host_params: clap_host_params { + rescan: Some(Self::ext_params_rescan), + clear: Some(Self::ext_params_clear), + request_flush: Some(Self::ext_params_request_flush), + }, + clap_host_state: clap_host_state { + mark_dirty: Some(Self::ext_state_mark_dirty), + }, + clap_host_thread_check: clap_host_thread_check { + is_main_thread: Some(Self::ext_thread_check_is_main_thread), + is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), + }, + clap_host_latency: clap_host_latency { + changed: Some(Self::ext_latency_changed), + }, + clap_host_tail: clap_host_tail { + changed: Some(Self::ext_tail_changed), + }, + clap_host_voice_info: clap_host_voice_info { + changed: Some(Self::ext_voice_info_changed), + }, + }); + + // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct + // so it can be retrieved in host callbacks + unsafe { + (&raw const instance.clap_host.host_data) + .cast_mut() + .write(&*instance as *const _ as *mut std::ffi::c_void); + } + + instance + } + + pub fn clap_host_ptr(&self) -> *const clap_host { + &self.clap_host as *const clap_host + } + + #[track_caller] + pub unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { + unsafe { + let state = (*host).host_data as *const InstanceState; + &*state + } + } + + /// Set the callback error field if it does not already contain a value. Earlier errors are not + /// overwritten. + fn set_callback_error(&self, error: impl Into) { + if let Some(old_error) = self.callback_error.swap(Some(error.into())) { + self.callback_error.store(Some(old_error)); + } + } + + /// Checks whether this is the main thread. If it is not, then an error indicating this can be + /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_main_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if current_thread_id != self.main_thread_id { + self.set_callback_error(format!( + "'{}' may only be called from the main thread (thread {:?}), but it was called \ + from thread {:?}.", + function_name, self.main_thread_id, current_thread_id + )); + } + } + + /// Checks whether this is the audio thread. If it is not, then an error indicating this can be + /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_audio_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if self.audio_thread_id.load() != Some(current_thread_id) { + if current_thread_id == self.main_thread_id { + self.set_callback_error(format!( + "'{function_name}' may only be called from an audio thread, but it was called \ + from the main thread." + )); + } else { + self.set_callback_error(format!( + "'{function_name}' may only be called from an audio thread, but it was called \ + from an unknown thread." + )); + } + } + } + + /// Checks whether this is **not** the audio thread. If it is, then an error indicating this can + /// be retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_not_audio_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if self.audio_thread_id.load() == Some(current_thread_id) { + self.set_callback_error(format!( + "'{function_name}' was called from an audio thread, this is not allowed.", + )); + } + } + + unsafe extern "C" fn get_extension( + host: *const clap_host, + extension_id: *const c_char, + ) -> *const c_void { + //check_null_ptr!(host, (*host).host_data, extension_id); + let this = unsafe { InstanceState::from_clap_host(host) }; + + // Right now there's no way to have the host only expose certain extensions. We can always + // add that when test cases need it. + let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; + if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { + &this.clap_host_audio_ports as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { + &this.clap_host_note_ports as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { + &this.clap_host_preset_load as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PARAMS { + &this.clap_host_params as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_STATE { + &this.clap_host_state as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { + &this.clap_host_thread_check as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_LATENCY { + &this.clap_host_latency as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_TAIL { + &this.clap_host_tail as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { + &this.clap_host_voice_info as *const _ as *const c_void + } else { + std::ptr::null() + } + } + + unsafe extern "C" fn request_restart(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and + // in the multi-iteration run function it will trigger a deactivate->reactivate cycle + log::trace!("'clap_host::request_restart()' was called by the plugin, setting the flag"); + this.requested_restart.store(true); + } + + unsafe extern "C" fn request_process(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + // Handling this within the context of the validator would be a bit messy. Do plugins use + // this? + log::trace!("'clap_host::request_process()' was called by the plugin"); + this.callback_events.push(CallbackEvent::RequestProcess); + } + + unsafe extern "C" fn request_callback(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + // This this is either handled by `handle_callbacks_blocking()` while the audio thread is + // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the + // callback is not handled before the plugin is destroyed. + log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); + this.requested_callback.store(true); + } + + unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported( + host: *const clap_host, + _flag: u32, + ) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); + log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); + true + } + + unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_audio_ports::rescan()"); + log::trace!("'clap_host_audio_ports::rescan()' was called"); + + if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + this.callback_events + .push(CallbackEvent::RescanAudioPortsNames); + } + + if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error( + "'clap_host_audio_ports::rescan()' was called while the plugin was activated", + ); + } + + this.callback_events + .push(CallbackEvent::RescanAudioPortsAll); + } + } + + unsafe extern "C" fn ext_note_ports_supported_dialects( + host: *const clap_host, + ) -> clap_note_dialect { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_note_ports::supported_dialects()"); + log::trace!("'clap_host_note_ports::supported_dialects()' was called"); + + CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE + } + + unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_note_ports::rescan()"); + log::trace!("'clap_host_note_ports::rescan()' was called"); + + if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { + this.callback_events + .push(CallbackEvent::RescanNotePortsNames); + } + + if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error( + "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while \ + the plugin was activated", + ); + } + + this.callback_events.push(CallbackEvent::RescanNotePortsAll); + } + } + + unsafe extern "C" fn ext_preset_load_on_error( + host: *const clap_host, + location_kind: clap_preset_discovery_location_kind, + location: *const c_char, + load_key: *const c_char, + os_error: i32, + msg: *const c_char, + ) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_preset_load::on_error()"); + + let location = unsafe { LocationValue::new(location_kind, location) } + .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) }.context( + "'clap_host_preset_load::on_error()' called with an invalid load_key parameter", + ); + let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } + .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); + match (location, load_key, msg) { + (Ok(location), Ok(Some(load_key)), Ok(msg)) => { + this.set_callback_error(format!( + "'clap_host_preset_load::on_error()' called for {location} with load key \ + {load_key}, OS error code {os_error}, and the following error message: {msg}" + )); + } + (Ok(location), Ok(None), Ok(msg)) => { + this.set_callback_error(format!( + "'clap_host_preset_load::on_error()' called for {location} with no load key, \ + OS error code {os_error}, and the following error message: {msg}" + )); + } + (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { + this.set_callback_error(format!("{err:#}")); + } + } + } + + unsafe extern "C" fn ext_preset_load_loaded( + host: *const clap_host, + location_kind: clap_preset_discovery_location_kind, + location: *const c_char, + load_key: *const c_char, + ) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_preset_load::loaded()"); + + let location = unsafe { LocationValue::new(location_kind, location) } + .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); + match (location, load_key) { + (Ok(_location), Ok(_load_key)) => { + log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); + } + (Err(err), _) | (_, Err(err)) => { + this.set_callback_error(format!("{err:#}")); + } + } + } + + unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_params::rescan()"); + log::trace!("'clap_host_params::rescan()' was called"); + + if flags & CLAP_PARAM_RESCAN_VALUES != 0 { + this.callback_events.push(CallbackEvent::RescanParamsValues); + } + + if flags & CLAP_PARAM_RESCAN_TEXT != 0 { + this.callback_events.push(CallbackEvent::RescanParamsText); + } + + if flags & CLAP_PARAM_RESCAN_INFO != 0 { + this.callback_events.push(CallbackEvent::RescanParamsInfo); + } + + if flags & CLAP_PARAM_RESCAN_ALL != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error( + "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the \ + plugin is activated", + ); + } + + this.callback_events.push(CallbackEvent::RescanParamsAll); + } + } + + unsafe extern "C" fn ext_params_clear( + host: *const clap_host, + _param_id: clap_id, + _flags: clap_param_clear_flags, + ) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_params::clear()"); + log::debug!("TODO: Handle 'clap_host_params::clear()'"); + } + + unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_not_audio_thread("clap_host_params::request_flush()"); + log::trace!("'clap_host_params::request_flush()' was called"); + this.callback_events.push(CallbackEvent::RequestFlush); + } + + unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_state::mark_dirty()"); + log::trace!("'clap_host_state::mark_dirty()' was called"); + this.callback_events.push(CallbackEvent::ChangedState); + } + + unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + this.main_thread_id == std::thread::current().id() + } + + unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + this.audio_thread_id.load() == Some(std::thread::current().id()) + } + + unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + if this.status.load() != PluginStatus::Activating { + this.set_callback_error( + "'clap_host_latency::changed()' must only be called within \ + 'clap_plugin::activate()'", + ); + } + + this.assert_main_thread("clap_host_latency::changed()"); + log::trace!("'clap_host_latency::changed()' was called"); + this.callback_events.push(CallbackEvent::ChangedLatency); + } + + unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_audio_thread("clap_host_tail::changed()"); + log::trace!("'clap_host_tail::changed()' was called"); + this.callback_events.push(CallbackEvent::ChangedTail); + } + + unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceState::from_clap_host(host) }; + + this.assert_main_thread("clap_host_voice_info::changed()"); + log::trace!("'clap_host_voice_info::changed()' was called"); + this.callback_events.push(CallbackEvent::ChangedVoiceInfo); + } +} diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 77a3e89..14f343e 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -7,19 +7,16 @@ use clap_sys::process::{ CLAP_PROCESS_SLEEP, CLAP_PROCESS_TAIL, }; use std::marker::PhantomData; -use std::pin::Pin; use std::ptr::NonNull; -use std::sync::Arc; use super::process::ProcessData; use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; -use crate::plugin::host::InstanceState; +use crate::plugin::instance::InstanceState; use crate::util::unsafe_clap_call; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. -#[derive(Debug)] pub struct PluginAudioThread<'a> { /// The plugin instance this audio thread belongs to. This is needed to ensure that the audio /// thread instance cannot outlive the plugin instance (which cannot outlive the plugin @@ -42,7 +39,7 @@ pub enum ProcessStatus { impl Drop for PluginAudioThread<'_> { fn drop(&mut self) { - match self.state().status.load() { + match self.status() { PluginStatus::Processing => self.stop_processing(), PluginStatus::Activated => (), state => panic!( @@ -66,14 +63,13 @@ impl<'a> PluginAudioThread<'a> { self.plugin.as_ptr() } - /// Get the underlying `Plugin`'s [`InstanceState`] object. - pub fn state(&self) -> &Pin> { - &self.plugin.state + pub fn state(&self) -> &InstanceState { + self.plugin.state() } /// Get the plugin's current initialization status. pub fn status(&self) -> PluginStatus { - self.state().status.load() + self.plugin.status() } /// Get the _audio thread_ extension abstraction for the extension `T`, if the plugin supports @@ -110,7 +106,7 @@ impl<'a> PluginAudioThread<'a> { let plugin = self.as_ptr(); if unsafe_clap_call! { plugin=>start_processing(plugin) } { - self.state().status.store(PluginStatus::Processing); + self.plugin.state().status.store(PluginStatus::Processing); Ok(()) } else { anyhow::bail!("'clap_plugin::start_processing()' returned false.") @@ -161,6 +157,6 @@ impl<'a> PluginAudioThread<'a> { let plugin = self.as_ptr(); unsafe_clap_call! { plugin=>stop_processing(plugin) }; - self.state().status.store(PluginStatus::Activated); + self.plugin.state.status.store(PluginStatus::Activated); } } diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index 6394698..b26c487 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -1,5 +1,9 @@ //! Data structures and functions surrounding audio processing. +use crate::plugin::ext::audio_ports::AudioPortConfig; +use crate::plugin::instance::Plugin; +use crate::plugin::instance::audio_thread::PluginAudioThread; +use crate::util::check_null_ptr; use anyhow::Result; use clap_sys::audio_buffer::clap_audio_buffer; use clap_sys::events::{ @@ -21,12 +25,6 @@ use std::ffi::c_void; use std::fmt::Debug; use std::pin::Pin; use std::ptr::null_mut; -use std::sync::atomic::Ordering; - -use crate::plugin::ext::audio_ports::AudioPortConfig; -use crate::plugin::instance::Plugin; -use crate::plugin::instance::audio_thread::PluginAudioThread; -use crate::util::check_null_ptr; /// The input and output data for a call to `clap_plugin::process()`. pub struct ProcessData<'a> { @@ -289,7 +287,8 @@ impl<'a> ProcessData<'a> { let mut running = true; while running { plugin.activate(self.config.sample_rate, 1, self.buffers.len())?; - plugin.host().handle_callbacks_once(); + plugin.handle_callback()?; + self.reset(); plugin.on_audio_thread(|plugin| -> Result<()> { @@ -306,7 +305,7 @@ impl<'a> ProcessData<'a> { if plugin .state() .requested_restart - .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + .compare_exchange(true, false) .is_ok() { log::trace!( @@ -330,7 +329,7 @@ impl<'a> ProcessData<'a> { } // Handle callbacks the plugin may have made during deactivate - plugin.host().handle_callbacks_once(); + plugin.handle_callback()?; Ok(()) } diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 086e7fc..77233fc 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -1,5 +1,8 @@ //! Interactions with CLAP plugin libraries, which may contain multiple plugins. +use super::instance::Plugin; +use super::preset_discovery::PresetDiscoveryFactory; +use crate::util::{self, unsafe_clap_call}; use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory}; @@ -13,12 +16,6 @@ use std::collections::HashSet; use std::ffi::CString; use std::path::{Path, PathBuf}; use std::ptr::NonNull; -use std::rc::Rc; - -use super::instance::Plugin; -use super::preset_discovery::PresetDiscoveryFactory; -use crate::plugin::host::Host; -use crate::util::{self, unsafe_clap_call}; /// A CLAP plugin library built from a CLAP plugin's entry point. This can be used to iterate over /// all plugins exposed by the library and to initialize plugins. @@ -166,14 +163,6 @@ impl PluginLibrary { .expect("A Plugin was constructed for a plugin with no entry point"); let plugin_factory = unsafe_clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } as *const clap_plugin_factory; - // TODO: Should we log anything here? In theory not supporting the plugin factory is - // perfectly legal, but it's a bit weird - if plugin_factory.is_null() { - anyhow::bail!( - "The plugin does not support the '{}' factory.", - CLAP_PLUGIN_FACTORY_ID.to_str().unwrap() - ); - } let mut metadata = PluginLibraryMetadata { version: ( @@ -183,6 +172,11 @@ impl PluginLibrary { ), plugins: Vec::new(), }; + + if plugin_factory.is_null() { + return Ok(metadata); + } + let num_plugins = unsafe_clap_call! { plugin_factory=>get_plugin_count(plugin_factory) }; for i in 0..num_plugins { let descriptor = @@ -231,7 +225,7 @@ impl PluginLibrary { /// IDs supported by this plugin library can be found by calling /// [`metadata()`][Self::metadata()]. The returned plugin has not yet been initialized, and /// `destroy()` will be called automatically when the object is dropped. - pub fn create_plugin(&self, id: &str, host: Rc) -> Result> { + pub fn create_plugin(&self, id: &str) -> Result> { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); let plugin_factory = unsafe_clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } @@ -244,7 +238,7 @@ impl PluginLibrary { } let id_cstring = CString::new(id).context("Plugin ID contained null bytes")?; - Plugin::new(self, host, unsafe { &*plugin_factory }, &id_cstring) + Plugin::new(self, unsafe { &*plugin_factory }, &id_cstring) } /// Returns the plugin's preset discovery factory, if it has one. diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index 3153a3a..ecae821 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -1,5 +1,7 @@ //! Tests surrounding plugin features. +use crate::plugin::library::PluginLibrary; +use crate::tests::TestStatus; use anyhow::{Context, Result}; use clap_sys::plugin_features::{ CLAP_PLUGIN_FEATURE_ANALYZER, CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, CLAP_PLUGIN_FEATURE_INSTRUMENT, @@ -7,10 +9,6 @@ use clap_sys::plugin_features::{ }; use std::collections::HashSet; -use crate::plugin::host::Host; -use crate::plugin::library::PluginLibrary; -use crate::tests::TestStatus; - /// Verifies that the descriptor stored in the factory and the descriptor stored on the plugin /// object are equivalent. pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result { @@ -26,9 +24,8 @@ pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -199,9 +197,10 @@ pub fn test_layout_audio_ports_config( })?; } - // The `Host` contains built-in thread safety checks - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + Ok(TestStatus::Success { details: None }) } @@ -252,9 +251,8 @@ pub fn test_layout_configurable_audio_ports( } let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -343,8 +341,9 @@ pub fn test_layout_configurable_audio_ports( })?; } - // The `Host` contains built-in thread safety checks - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index d90317f..ee929fb 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -4,7 +4,6 @@ use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::ext::params::{ParamInfo, Params}; -use crate::plugin::host::Host; use crate::plugin::instance::process::{AudioBuffers, Event, ProcessData}; use crate::plugin::library::PluginLibrary; use crate::tests::plugin::processing::run_simple; @@ -57,9 +56,8 @@ impl<'a> ParamValue<'a> { /// The test for `ProcessingTest::ParamConversions`. pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Result { - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -73,7 +71,10 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let param_infos = params .info() @@ -157,7 +158,20 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } } - if !(num_supported_value_to_text == 0 || num_supported_value_to_text == expected_conversions) { + plugin + .handle_callback() + .context("An error occured during a callback")?; + + if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin's parameters need to support both value to text and text to value \ + conversions for this test.", + )), + }); + } + + if num_supported_value_to_text != expected_conversions { anyhow::bail!( "'clap_plugin_params::value_to_text()' returned true for \ {num_supported_value_to_text} out of {expected_conversions} calls. This function is \ @@ -165,7 +179,8 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul Examples of failing conversions were: {failed_value_to_text_calls:#?}" ); } - if !(num_supported_text_to_value == 0 || num_supported_text_to_value == expected_conversions) { + + if num_supported_text_to_value != expected_conversions { anyhow::bail!( "'clap_plugin_params::text_to_value()' returned true for \ {num_supported_text_to_value} out of {expected_conversions} calls. This function is \ @@ -174,27 +189,14 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul ); } - host.callback_error_check() - .context("An error occured during a host callback")?; - if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { - Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin's parameters need to support both value to text and text to value \ - conversions for this test.", - )), - }) - } else { - Ok(TestStatus::Success { details: None }) - } + Ok(TestStatus::Success { details: None }) } /// The test for `ProcessingTest::ParamFuzzBasic`. pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); - - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -211,7 +213,10 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -304,9 +309,9 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result std::mem::swap(&mut previous_events, &mut current_events); } - // `ProcessingTest::run()` already handled callbacks for us - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -315,9 +320,8 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -334,7 +338,10 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -430,9 +437,9 @@ pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Resul std::mem::swap(&mut previous_events, &mut current_events); } - // `ProcessingTest::run()` already handled callbacks for us - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -445,10 +452,8 @@ pub fn test_param_fuzz_sample_accurate( const INTERVALS: &[u32] = &[1000, 100, 1]; let mut prng = new_prng(); - - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -465,7 +470,10 @@ pub fn test_param_fuzz_sample_accurate( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -549,9 +557,9 @@ pub fn test_param_fuzz_sample_accurate( } } - // `ProcessingTest::run()` already handled callbacks for us - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -559,10 +567,8 @@ pub fn test_param_fuzz_sample_accurate( /// The test for `ProcessingTest::ParamFuzzModulation`. pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); - - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -617,7 +623,9 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R Ok(()) })?; - host.handle_callbacks_once(); + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -628,10 +636,8 @@ pub fn test_param_set_wrong_namespace( plugin_id: &str, ) -> Result { let mut prng = new_prng(); - - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -651,7 +657,10 @@ pub fn test_param_set_wrong_namespace( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let param_infos = params .info() @@ -696,8 +705,10 @@ pub fn test_param_set_wrong_namespace( .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + if actual_param_values == initial_param_values { Ok(TestStatus::Success { details: None }) } else { @@ -714,9 +725,8 @@ pub fn test_param_set_wrong_namespace( /// The test for `ProcessingTest::ParamDefaultValues`. pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Result { - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -730,7 +740,10 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let param_infos = params .info() @@ -752,8 +765,9 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } } - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 4e10afe..6a62052 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -2,7 +2,6 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; -use crate::plugin::host::Host; use crate::plugin::instance::Plugin; use crate::plugin::instance::process::{ AudioBuffers, ProcessConfig, ProcessControlFlow, ProcessData, @@ -66,9 +65,8 @@ pub fn test_process_audio_basic( ) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -85,6 +83,10 @@ pub fn test_process_audio_basic( } }; + plugin + .handle_callback() + .context("An error occured during a callback")?; + let mut audio_buffers = if in_place { AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE) } else { @@ -97,9 +99,10 @@ pub fn test_process_audio_basic( Ok(()) })?; - // The `Host` contains built-in thread safety checks - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + Ok(TestStatus::Success { details: None }) } @@ -113,9 +116,8 @@ pub fn test_process_note_out_of_place( ) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -149,6 +151,10 @@ pub fn test_process_note_out_of_place( }); } + plugin + .handle_callback() + .context("An error occured during a callback")?; + // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports let mut note_event_rng = NoteGenerator::new(¬e_ports_config); @@ -169,8 +175,10 @@ pub fn test_process_note_out_of_place( Ok(()) })?; - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + Ok(TestStatus::Success { details: None }) } @@ -186,9 +194,8 @@ pub fn test_process_varying_sample_rates( let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -206,6 +213,10 @@ pub fn test_process_varying_sample_rates( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); + plugin + .handle_callback() + .context("An error occured during a callback")?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { let mut note_event_rng = NoteGenerator::new(¬e_ports_config); @@ -233,8 +244,9 @@ pub fn test_process_varying_sample_rates( ))?; } - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -250,9 +262,8 @@ pub fn test_process_varying_block_sizes( let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -277,6 +288,10 @@ pub fn test_process_varying_block_sizes( let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); let num_iters = (32768 / buffer_size).min(5); + plugin + .handle_callback() + .context("An error occured during a callback")?; + run_simple( &plugin, &mut process_data, @@ -298,8 +313,9 @@ pub fn test_process_varying_block_sizes( ))?; } - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -313,9 +329,8 @@ pub fn test_process_random_block_sizes( let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -333,6 +348,10 @@ pub fn test_process_random_block_sizes( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); + plugin + .handle_callback() + .context("An error occured during a callback")?; + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); @@ -355,8 +374,9 @@ pub fn test_process_random_block_sizes( Ok(()) })?; - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -368,9 +388,8 @@ pub fn test_process_audio_constant_mask( ) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -396,6 +415,10 @@ pub fn test_process_audio_constant_mask( let mut has_received_constant_output = false; let mut has_received_constant_flag = false; + plugin + .handle_callback() + .context("An error occured during a callback")?; + process_data.run(&plugin, |plugin, process| { process.buffers.randomize(&mut prng); @@ -446,8 +469,9 @@ pub fn test_process_audio_constant_mask( } })?; - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; if !has_received_constant_flag && has_received_constant_output { return Ok(TestStatus::Warning { @@ -465,9 +489,8 @@ pub fn test_process_audio_reset_determinism( library: &PluginLibrary, plugin_id: &str, ) -> Result { - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -485,6 +508,10 @@ pub fn test_process_audio_reset_determinism( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); + plugin + .handle_callback() + .context("An error occured during a callback")?; + let mut note_event_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = AudioBuffers::new_out_of_place_f32( &audio_ports_config, @@ -535,8 +562,9 @@ pub fn test_process_audio_reset_determinism( anyhow::bail!("Plugin output differs after reset"); } - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 803776c..b1f766a 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -10,7 +10,6 @@ use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::params::Params; use crate::plugin::ext::state::State; -use crate::plugin::host::Host; use crate::plugin::instance::process::{ AudioBuffers, Event, EventQueue, ProcessConfig, ProcessData, }; @@ -28,9 +27,8 @@ const PARAM_DIFF_FILE_NAME: &str = "param-diff.csv"; /// The test for `PluginTestCase::StateInvalidEmpty`. pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Result { - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -44,22 +42,21 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res }); } }; - host.handle_callbacks_once(); - match state.load(&[]) { + let result = state.load(&[]); + + plugin + .handle_callback() + .context("An error occured during a callback")?; + + match result { Ok(_) => Ok(TestStatus::Warning { details: Some(String::from( "The plugin returned true when 'clap_plugin_state::load()' was called when an \ empty state, this is likely a bug.", )), }), - Err(_) => { - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; - - Ok(TestStatus::Success { details: None }) - } + Err(_) => Ok(TestStatus::Success { details: None }), } } @@ -67,9 +64,8 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; @@ -85,7 +81,9 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re } }; - host.handle_callbacks_once(); + plugin + .handle_callback() + .context("An error occured during a callback")?; let mut random_data = vec![0u8; 1024 * 1024]; let mut succeeded = false; @@ -95,9 +93,9 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re succeeded |= state.load(&random_data).is_ok(); } - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; match succeeded { false => Ok(TestStatus::Success { details: None }), @@ -124,9 +122,8 @@ pub fn test_state_reproducibility_basic( ) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; // We'll drop and reinitialize the plugin later @@ -139,6 +136,7 @@ pub fn test_state_reproducibility_basic( .context("Error while querying 'audio-ports' IO configuration")?, None => AudioPortConfig::default(), }; + let params = match plugin.get_extension::() { Some(params) => params, None => { @@ -159,7 +157,10 @@ pub fn test_state_reproducibility_basic( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let param_infos = params .info() @@ -206,7 +207,10 @@ pub fn test_state_reproducibility_basic( .collect::>>()?; let expected_state = state.save()?; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -218,7 +222,7 @@ pub fn test_state_reproducibility_basic( drop(plugin); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance a second time")?; plugin .init() @@ -235,6 +239,7 @@ pub fn test_state_reproducibility_basic( }); } }; + let state = match plugin.get_extension::() { Some(state) => state, None => { @@ -245,10 +250,16 @@ pub fn test_state_reproducibility_basic( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; state.load(&expected_state)?; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -276,10 +287,11 @@ pub fn test_state_reproducibility_basic( // Now for the moment of truth let actual_state = state.save()?; - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { @@ -309,9 +321,8 @@ pub fn test_state_reproducibility_flush( ) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; // We'll drop and reinitialize the plugin later. This first pass sets the values using the flush @@ -341,7 +352,10 @@ pub fn test_state_reproducibility_flush( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; let param_infos = params .info() @@ -365,7 +379,10 @@ pub fn test_state_reproducibility_flush( input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; // We'll compare against these values in that second pass let expected_param_values: BTreeMap = param_infos @@ -373,7 +390,10 @@ pub fn test_state_reproducibility_flush( .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; let expected_state = state.save()?; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; // Plugins with no parameters at all should of course not trigger this error if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { @@ -395,7 +415,7 @@ pub fn test_state_reproducibility_flush( drop(plugin); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance a second time")?; plugin .init() @@ -407,6 +427,7 @@ pub fn test_state_reproducibility_flush( .context("Error while querying 'audio-ports' IO configuration")?, None => AudioPortConfig::default(), }; + let params = match plugin.get_extension::() { Some(params) => params, None => { @@ -418,6 +439,7 @@ pub fn test_state_reproducibility_flush( }); } }; + let state = match plugin.get_extension::() { Some(state) => state, None => { @@ -428,7 +450,10 @@ pub fn test_state_reproducibility_flush( }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; // NOTE: We can reuse random parameter set events, except that the cookie pointers may be // different if the plugin uses those. So we need to update these cookies first. @@ -487,10 +512,11 @@ pub fn test_state_reproducibility_flush( } let actual_state = state.save()?; - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; + plugin + .handle_callback() + .context("An error occured during a callback")?; + if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { @@ -519,9 +545,8 @@ pub fn test_state_reproducibility_flush( pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); - let host = Host::new(); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance")?; let (expected_state, expected_param_values) = { @@ -577,10 +602,13 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> .collect::>>()?; // This state file is saved without buffered writes. It's expected that the plugin - // implementsq this correctly, so we can check if it handles buffered streams correctly by + // implements this correctly, so we can check if it handles buffered streams correctly by // treating this as the ground truth. let expected_state = state.save()?; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -590,11 +618,12 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> drop(plugin); let plugin = library - .create_plugin(plugin_id, host.clone()) + .create_plugin(plugin_id) .context("Could not create the plugin instance a second time")?; plugin .init() .context("Error while initializing the second plugin instance")?; + let params = match plugin.get_extension::() { Some(params) => params, None => { @@ -605,6 +634,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> }); } }; + let state = match plugin.get_extension::() { Some(state) => state, None => { @@ -615,12 +645,17 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> }); } }; - host.handle_callbacks_once(); + + plugin + .handle_callback() + .context("An error occured during a callback")?; // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; - host.handle_callbacks_once(); + plugin + .handle_callback() + .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -645,9 +680,10 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> // Because we're mean, we'll use a different prime number for the saving const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; - host.handle_callbacks_once(); - host.callback_error_check() - .context("An error occured during a host callback")?; + + plugin + .handle_callback() + .context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) diff --git a/src/tests/plugin_library/factories.rs b/src/tests/plugin_library/factories.rs index 45acbff..8ad0f91 100644 --- a/src/tests/plugin_library/factories.rs +++ b/src/tests/plugin_library/factories.rs @@ -1,13 +1,11 @@ //! Tests interacting with the plugin's factories. +use crate::plugin::library::PluginLibrary; +use crate::tests::TestStatus; use anyhow::{Context, Result}; use clap_sys::version::clap_version_is_compatible; use std::path::Path; -use crate::plugin::host::Host; -use crate::plugin::library::PluginLibrary; -use crate::tests::TestStatus; - /// The test for `PluginLibraryTestCase::QueryNonexistentFactory`. pub fn test_query_nonexistent_factory(library_path: &Path) -> Result { let library = PluginLibrary::load(library_path) @@ -86,7 +84,7 @@ pub fn test_create_id_with_trailing_garbage(library_path: &Path) -> Result Result // With everything indexed, we can try loading these presets. We'll reuse one plugin // instance per plugin. for (plugin_id, presets) in loadable_presets_by_plugin_id { - let host = Host::new(); let plugin = library - .create_plugin(&plugin_id, host.clone()) + .create_plugin(&plugin_id) .with_context(|| format!("Could not create a plugin instance for '{plugin_id}'"))?; plugin .init() @@ -131,7 +129,9 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // We'll try to run some audio through the plugin to make sure the preset change was // successful, but it doesn't matter if the plugin doesn't have any audio ports let audio_ports = plugin.get_extension::(); - host.handle_callbacks_once(); + plugin + .handle_callback() + .context("An error occured during a host callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -162,8 +162,7 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // In case the plugin uses `clap_host_preset_load::on_error()` to report an error, // we will check that first before making sure the preset loaded correctly. This // might otherwise mask the error message. - host.handle_callbacks_once(); - host.callback_error_check().with_context(|| { + plugin.handle_callback().with_context(|| { format!( "An error occurred while loading the preset '{}' for plugin '{}'", preset.name, plugin_id @@ -186,14 +185,12 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result ) })?; - host.handle_callbacks_once(); - host.callback_error_check().with_context(|| { + plugin.handle_callback().with_context(|| { format!("An error occured during a host callback made by '{plugin_id}'") })?; } - host.handle_callbacks_once(); - host.callback_error_check().with_context(|| { + plugin.handle_callback().with_context(|| { format!("An error occured during a host callback made by '{plugin_id}'") })?; } From 577d652d9bb4a2a917308225cc7c18ad1e26d1c4 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 21 Jan 2026 13:20:04 +0400 Subject: [PATCH 037/114] refactor --- src/plugin/ext/audio_ports.rs | 50 ++++++------- src/plugin/ext/audio_ports_config.rs | 16 +++- src/plugin/ext/configurable_audio_ports.rs | 26 ++++--- src/plugin/ext/latency.rs | 6 +- src/plugin/ext/note_ports.rs | 18 +++-- src/plugin/ext/params.rs | 79 ++++++++++++-------- src/plugin/ext/preset_load.rs | 28 +++---- src/plugin/ext/state.rs | 26 +++++-- src/plugin/instance.rs | 85 ++++++++++++++-------- src/plugin/instance/audio_thread.rs | 40 +++++----- src/plugin/instance/process.rs | 3 +- src/plugin/library.rs | 47 ++++++++---- src/plugin/preset_discovery.rs | 11 ++- src/plugin/preset_discovery/provider.rs | 40 ++++++---- src/tests/plugin/state.rs | 4 +- src/util.rs | 11 +-- 16 files changed, 296 insertions(+), 194 deletions(-) diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 679aec3..b413c3b 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -1,5 +1,10 @@ //! Abstractions for interacting with the `audio-ports` extension. +use super::Extension; +use crate::plugin::ext::ambisonic::Ambisonic; +use crate::plugin::ext::surround::Surround; +use crate::plugin::instance::Plugin; +use crate::util::clap_call; use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::{ @@ -12,13 +17,6 @@ use std::collections::HashMap; use std::ffi::CStr; use std::ptr::NonNull; -use crate::plugin::ext::ambisonic::Ambisonic; -use crate::plugin::ext::surround::Surround; -use crate::plugin::instance::Plugin; -use crate::util::unsafe_clap_call; - -use super::Extension; - /// Abstraction for the `audio-ports` extension covering the main thread functionality. pub struct AudioPorts<'a> { plugin: &'a Plugin<'a>, @@ -72,8 +70,12 @@ impl AudioPorts<'_> { // TODO: Refactor this to reduce the duplication a little without hurting the human readable error messages let audio_ports = self.audio_ports.as_ptr(); let plugin = self.plugin.as_ptr(); - let num_inputs = unsafe_clap_call! { audio_ports=>count(plugin, true) }; - let num_outputs = unsafe_clap_call! { audio_ports=>count(plugin, false) }; + let num_inputs = unsafe { + clap_call! { audio_ports=>count(plugin, true) } + }; + let num_outputs = unsafe { + clap_call! { audio_ports=>count(plugin, false) } + }; // Audio ports have a stable ID attribute that can be used to connect input and output ports // so the host can do in-place processing. This uses stable IDs rather than the indices in @@ -86,7 +88,10 @@ impl AudioPorts<'_> { for i in 0..num_inputs { let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { audio_ports=>get(plugin, i, true, &mut info) }; + let success = unsafe { + clap_call! { audio_ports=>get(plugin, i, true, &mut info) } + }; + if !success { anyhow::bail!( "Plugin returned an error when querying input audio port {i} ({num_inputs} \ @@ -94,14 +99,8 @@ impl AudioPorts<'_> { ); } - is_audio_port_type_consistent(&info, has_ambisonic, has_surround).with_context( - || { - format!( - "Inconsistent channel count for output port {i} ({num_outputs} total \ - output ports)" - ) - }, - )?; + is_audio_port_type_consistent(&info, has_ambisonic, has_surround) + .with_context(|| format!("Inconsistent type for output port {i}"))?; // We'll convert these stable IDs to vector indices later if input_stable_index_pairs.contains_key(&info.id) { @@ -133,7 +132,10 @@ impl AudioPorts<'_> { for i in 0..num_outputs { let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { audio_ports=>get(plugin, i, false, &mut info) }; + let success = unsafe { + clap_call! { audio_ports=>get(plugin, i, false, &mut info) } + }; + if !success { anyhow::bail!( "Plugin returned an error when querying output audio port {i} ({num_outputs} \ @@ -141,14 +143,8 @@ impl AudioPorts<'_> { ); } - is_audio_port_type_consistent(&info, has_ambisonic, has_surround).with_context( - || { - format!( - "Inconsistent channel count for output port {i} ({num_outputs} total \ - output ports)" - ) - }, - )?; + is_audio_port_type_consistent(&info, has_ambisonic, has_surround) + .with_context(|| format!("Inconsistent channel count for output port {i}"))?; if output_stable_index_pairs.contains_key(&info.id) { anyhow::bail!( diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index d50a086..4e490d0 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; -use crate::util::{c_char_slice_to_string, clap_call, unsafe_clap_call}; +use crate::util::{c_char_slice_to_string, clap_call}; use anyhow::Result; use clap_sys::ext::audio_ports_config::{ CLAP_EXT_AUDIO_PORTS_CONFIG, CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, @@ -68,7 +68,9 @@ impl AudioPortsConfig<'_> { pub fn enumerate(&self) -> Result> { let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); - let count = unsafe_clap_call! { audio_ports_config=>count(plugin) }; + let count = unsafe { + clap_call! { audio_ports_config=>count(plugin) } + }; (0..count) .map(|i| unsafe { @@ -97,7 +99,10 @@ impl AudioPortsConfig<'_> { pub fn select(&self, config_id: clap_id) -> Result<()> { let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); - let result = unsafe_clap_call! { audio_ports_config=>select(plugin, config_id) }; + let result = unsafe { + clap_call! { audio_ports_config=>select(plugin, config_id) } + }; + if !result { anyhow::bail!("audio_ports_config::select() returned false"); } @@ -110,7 +115,10 @@ impl AudioPortsConfigInfo<'_> { pub fn current(&self) -> clap_id { let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); let plugin = self.plugin.as_ptr(); - unsafe_clap_call! { audio_ports_config_info=>current_config(plugin) } + + unsafe { + clap_call! { audio_ports_config_info=>current_config(plugin) } + } } // TODO: diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index b36f75e..ec1fc58 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; -use crate::util::unsafe_clap_call; +use crate::util::clap_call; use clap_sys::ext::audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}; use clap_sys::ext::configurable_audio_ports::{ CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, @@ -63,11 +63,13 @@ impl<'a> ConfigurableAudioPorts<'a> { let plugin = self.plugin.as_ptr(); let ext = self.configurable_audio_ports.as_ptr(); - unsafe_clap_call! { ext=>can_apply_configuration( - plugin, - requests.as_ptr(), - requests.len() as u32 - )} + unsafe { + clap_call! { ext=>can_apply_configuration( + plugin, + requests.as_ptr(), + requests.len() as u32 + )} + } } pub fn apply_configuration( @@ -94,10 +96,12 @@ impl<'a> ConfigurableAudioPorts<'a> { let plugin = self.plugin.as_ptr(); let ext = self.configurable_audio_ports.as_ptr(); - unsafe_clap_call! { ext=>apply_configuration( - plugin, - requests.as_ptr(), - requests.len() as u32 - )} + unsafe { + clap_call! { ext=>apply_configuration( + plugin, + requests.as_ptr(), + requests.len() as u32 + )} + } } } diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index a51904d..7643f02 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; -use crate::util::unsafe_clap_call; +use crate::util::clap_call; use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_plugin_latency}; use std::ffi::CStr; use std::ptr::NonNull; @@ -29,6 +29,8 @@ impl<'a> Latency<'a> { pub fn get(&self) -> u32 { let latency = self.latency.as_ptr(); let plugin = self.plugin.as_ptr(); - unsafe_clap_call! { latency=>get(plugin) } + unsafe { + clap_call! { latency=>get(plugin) } + } } } diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index a0ae99b..dd1a584 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -2,7 +2,7 @@ use super::Extension; use crate::plugin::instance::Plugin; -use crate::util::unsafe_clap_call; +use crate::util::clap_call; use anyhow::Result; use clap_sys::ext::note_ports::{ CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, @@ -60,8 +60,12 @@ impl NotePorts<'_> { let note_ports = self.note_ports.as_ptr(); let plugin = self.plugin.as_ptr(); - let num_inputs = unsafe_clap_call! { note_ports=>count(plugin, true) }; - let num_outputs = unsafe_clap_call! { note_ports=>count(plugin, false) }; + let num_inputs = unsafe { + clap_call! { note_ports=>count(plugin, true) } + }; + let num_outputs = unsafe { + clap_call! { note_ports=>count(plugin, false) } + }; // We don't need the port's stable IDs, but we'll still verify that they're unique let mut input_stable_indices: HashSet = HashSet::new(); @@ -69,7 +73,9 @@ impl NotePorts<'_> { for i in 0..num_inputs { let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { note_ports=>get(plugin, i, true, &mut info) }; + let success = unsafe { + clap_call! { note_ports=>get(plugin, i, true, &mut info) } + }; if !success { anyhow::bail!( "Plugin returned an error when querying input note port {i} ({num_inputs} \ @@ -111,7 +117,9 @@ impl NotePorts<'_> { for i in 0..num_outputs { let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { note_ports=>get(plugin, i, false, &mut info) }; + let success = unsafe { + clap_call! { note_ports=>get(plugin, i, false, &mut info) } + }; if !success { anyhow::bail!( "Plugin returned an error when querying output note port {i} ({num_outputs} \ diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index c26ad93..2cb41bd 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -21,7 +21,7 @@ use std::ptr::NonNull; use super::Extension; use crate::plugin::instance::process::EventQueue; use crate::plugin::instance::{Plugin, PluginStatus}; -use crate::util::{self, c_char_slice_to_string, unsafe_clap_call}; +use crate::util::{self, c_char_slice_to_string, clap_call}; pub type ParamInfo = BTreeMap; @@ -73,7 +73,11 @@ impl Params<'_> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); let mut value = 0.0f64; - if unsafe_clap_call! { params=>get_value(plugin, param_id, &mut value) } { + let result = unsafe { + clap_call! { params=>get_value(plugin, param_id, &mut value) } + }; + + if result { Ok(value) } else { anyhow::bail!( @@ -89,15 +93,19 @@ impl Params<'_> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); let mut string_buffer = [0; CLAP_NAME_SIZE]; - if unsafe_clap_call! { - params=>value_to_text( - plugin, - param_id, - value, - string_buffer.as_mut_ptr(), - string_buffer.len() as u32, - ) - } { + let result = unsafe { + clap_call! { + params=>value_to_text( + plugin, + param_id, + value, + string_buffer.as_mut_ptr(), + string_buffer.len() as u32, + ) + } + }; + + if result { c_char_slice_to_string(&string_buffer) .map(Some) .with_context(|| { @@ -119,18 +127,18 @@ impl Params<'_> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); let mut value = 0.0f64; - if unsafe_clap_call! { - params=>text_to_value( - plugin, - param_id, - text_cstring.as_ptr(), - &mut value, - ) - } { - Ok(Some(value)) - } else { - Ok(None) - } + let result = unsafe { + clap_call! { + params=>text_to_value( + plugin, + param_id, + text_cstring.as_ptr(), + &mut value, + ) + } + }; + + if result { Ok(Some(value)) } else { Ok(None) } } /// Get information about all of the plugin's parameters. Returns an error if the plugin's @@ -142,13 +150,18 @@ impl Params<'_> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); - let num_params = unsafe_clap_call! { params=>count(plugin) }; + let num_params = unsafe { + clap_call! { params=>count(plugin) } + }; // Right now this is only used to make sure the plugin doesn't have multiple bypass parameters let mut bypass_parameter_id = None; for i in 0..num_params { let mut info: clap_param_info = unsafe { std::mem::zeroed() }; - let success = unsafe_clap_call! { params=>get_info(plugin, i, &mut info) }; + let success = unsafe { + clap_call! { params=>get_info(plugin, i, &mut info) } + }; + if !success { anyhow::bail!( "Plugin returned an error when querying parameter {i} ({num_params} total \ @@ -334,13 +347,15 @@ impl Params<'_> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); - unsafe_clap_call! { - params=>flush( - plugin, - input_events.vtable_input(), - output_events.vtable_output(), - ) - }; + unsafe { + clap_call! { + params=>flush( + plugin, + input_events.vtable_input(), + output_events.vtable_output(), + ) + }; + } } } diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index cf93ce4..558adce 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -5,11 +5,10 @@ use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_plugin_preset_load}; use std::ffi::{CStr, CString}; use std::ptr::NonNull; +use super::Extension; use crate::plugin::instance::Plugin; use crate::plugin::preset_discovery::LocationValue; -use crate::util::unsafe_clap_call; - -use super::Extension; +use crate::util::clap_call; /// Abstraction for the `preset-load` extension covering the main thread functionality. pub struct PresetLoad<'a> { @@ -47,17 +46,20 @@ impl PresetLoad<'_> { let preset_load = self.preset_load.as_ptr(); let plugin = self.plugin.as_ptr(); - let success = unsafe_clap_call! { - preset_load=>from_location( - plugin, - location_kind, - location_ptr, - match load_key_cstring.as_ref() { - Some(load_key_cstring) => load_key_cstring.as_ptr(), - None => std::ptr::null(), - } - ) + let success = unsafe { + clap_call! { + preset_load=>from_location( + plugin, + location_kind, + location_ptr, + match load_key_cstring.as_ref() { + Some(load_key_cstring) => load_key_cstring.as_ptr(), + None => std::ptr::null(), + } + ) + } }; + if success { Ok(()) } else { diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 6a0799a..f5fca15 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use super::Extension; use crate::plugin::instance::Plugin; -use crate::util::{check_null_ptr, unsafe_clap_call}; +use crate::util::{check_null_ptr, clap_call}; /// Abstraction for the `state` extension covering the main thread functionality. pub struct State<'a> { @@ -71,7 +71,11 @@ impl State<'_> { let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); - if unsafe_clap_call! { state=>save(plugin, &stream.vtable) } { + let result = unsafe { + clap_call! { state=>save(plugin, &stream.vtable) } + }; + + if result { Ok(stream.into_vec()) } else { anyhow::bail!("'clap_plugin_state::save()' returned false."); @@ -85,7 +89,11 @@ impl State<'_> { let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); - if unsafe_clap_call! { state=>save(plugin, stream.vtable()) } { + let result = unsafe { + clap_call! { state=>save(plugin, stream.vtable()) } + }; + + if result { Ok(stream.into_vec()) } else { anyhow::bail!( @@ -101,7 +109,11 @@ impl State<'_> { let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); - if unsafe_clap_call! { state=>load(plugin, stream.vtable()) } { + let result = unsafe { + clap_call! { state=>load(plugin, stream.vtable()) } + }; + + if result { Ok(()) } else { anyhow::bail!("'clap_plugin_state::load()' returned false."); @@ -115,7 +127,11 @@ impl State<'_> { let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); - if unsafe_clap_call! { state=>load(plugin, &stream.vtable) } { + let result = unsafe { + clap_call! { state=>load(plugin, &stream.vtable) } + }; + + if result { Ok(()) } else { anyhow::bail!( diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index c9d787c..559acb5 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -3,7 +3,7 @@ use super::ext::Extension; use super::library::{PluginLibrary, PluginMetadata}; use crate::plugin::preset_discovery::LocationValue; -use crate::util::{self, check_null_ptr, unsafe_clap_call}; +use crate::util::{self, check_null_ptr, clap_call}; use anyhow::{Context, Result}; use audio_thread::PluginAudioThread; use clap_sys::ext::audio_ports::{ @@ -32,12 +32,12 @@ use clap_sys::plugin::clap_plugin; use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; use crossbeam::queue::SegQueue; -use std::ffi::{CStr, c_char, c_void}; +use std::ffi::{CStr, CString, c_char, c_void}; use std::marker::PhantomData; use std::panic::resume_unwind; use std::pin::Pin; use std::ptr::NonNull; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::thread::ThreadId; pub mod audio_thread; @@ -166,10 +166,12 @@ impl Drop for Plugin<'_> { ), } - // TODO: We can't handle host callbacks that happen in between these two functions, but the - // plugin really shouldn't be making callbacks in deactivate() + self.handle_callback_unchecked(); + let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>destroy(plugin) }; + unsafe { + clap_call! { plugin=>destroy(plugin) } + } } } @@ -177,14 +179,16 @@ impl<'lib> Plugin<'lib> { /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the /// plugin could not be created. The plugin instance will be registered with the host, and /// unregistered when this object is dropped again. - pub fn new( + pub(crate) fn new( library: &'lib PluginLibrary, factory: &clap_plugin_factory, plugin_id: &CStr, ) -> Result { let state = InstanceState::new(); - let plugin = unsafe_clap_call! { - factory=>create_plugin(factory, state.clap_host_ptr(), plugin_id.as_ptr()) + let plugin = unsafe { + clap_call! { + factory=>create_plugin(factory, state.clap_host_ptr(), plugin_id.as_ptr()) + } }; if plugin.is_null() { @@ -219,11 +223,6 @@ impl<'lib> Plugin<'lib> { PluginMetadata::from_descriptor(unsafe { &*descriptor }) } - /// Get the reference to a thread-safe object containing information about this plugin instance. - pub fn state(&self) -> &InstanceState { - &self.state - } - /// The plugin's current initialization status. pub fn status(&self) -> PluginStatus { self.state.status.load() @@ -232,15 +231,16 @@ impl<'lib> Plugin<'lib> { /// Handle any pending main-thread callbacks for this plugin. /// Returns an error if there is a callback error pending. pub fn handle_callback(&self) -> Result<()> { - if self.state.requested_callback.swap(false) { - let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>on_main_thread(plugin) }; - } + self.handle_callback_unchecked(); if let Some(error) = self.state.callback_error.take() { anyhow::bail!(error); } + while let Some(event) = self.state.callback_events.pop() { + println!("{:?}", event); + } + Ok(()) } @@ -252,7 +252,9 @@ impl<'lib> Plugin<'lib> { let plugin = self.as_ptr(); for id in T::IDS { - let extension_ptr = unsafe_clap_call! { plugin=>get_extension(plugin, id.as_ptr()) }; + let extension_ptr = unsafe { + clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } + }; if !extension_ptr.is_null() { return unsafe { @@ -326,14 +328,12 @@ impl<'lib> Plugin<'lib> { // Handle callbacks requests on the main thread while the audio thread is running while is_running.load() { - if self.state.requested_callback.swap(false) { - let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>on_main_thread(plugin) }; - } - - std::thread::sleep(std::time::Duration::from_millis(1)); + self.handle_callback_unchecked(); + std::thread::sleep(std::time::Duration::from_millis(5)); } + self.handle_callback_unchecked(); + audio_thread .join() .unwrap_or_else(|panic_info| resume_unwind(panic_info)) @@ -346,7 +346,11 @@ impl<'lib> Plugin<'lib> { self.status().assert_is(PluginStatus::Uninitialized); let plugin = self.as_ptr(); - if unsafe_clap_call! { plugin=>init(plugin) } { + let result = unsafe { + clap_call! { plugin=>init(plugin) } + }; + + if result { // If the plugin never calls `request_callback`, the validator won't catch this anyhow::ensure!( unsafe { (*plugin).on_main_thread.is_some() }, @@ -379,9 +383,11 @@ impl<'lib> Plugin<'lib> { self.state.status.store(PluginStatus::Activating); let plugin = self.as_ptr(); - if unsafe_clap_call! { - plugin=>activate(plugin, sample_rate, min_buffer_size as u32, max_buffer_size as u32) - } { + let result = unsafe { + clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size as u32, max_buffer_size as u32) } + }; + + if result { self.state.status.store(PluginStatus::Activated); Ok(()) } else { @@ -397,16 +403,27 @@ impl<'lib> Plugin<'lib> { self.status().assert_is(PluginStatus::Activated); let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>deactivate(plugin) }; + unsafe { + clap_call! { plugin=>deactivate(plugin) } + } self.state.status.store(PluginStatus::Deactivated); } + + fn handle_callback_unchecked(&self) { + if self.state.requested_callback.swap(false) { + let plugin = self.as_ptr(); + unsafe { + clap_call! { plugin=>on_main_thread(plugin) } + }; + } + } } /// Runtime information about a plugin instance. This keeps track of pending callbacks and things /// like audio threads. It also contains the plugin's unique `clap_host` struct so host callbacks /// can be linked back to this specific plugin instance. -pub struct InstanceState { +struct InstanceState { pub callback_events: SegQueue, pub callback_error: AtomicCell>, @@ -441,6 +458,8 @@ pub struct InstanceState { impl InstanceState { pub fn new() -> Pin> { + static VERSION: OnceLock = OnceLock::new(); + let main_thread = std::thread::current().id(); let instance = Arc::pin(InstanceState { callback_events: SegQueue::new(), @@ -459,7 +478,9 @@ impl InstanceState { name: c"clap-validator".as_ptr(), vendor: c"Robbert van der Helm".as_ptr(), url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: c"0.1.0".as_ptr(), //TODO: use crate version + version: VERSION + .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).unwrap()) + .as_ptr(), get_extension: Some(Self::get_extension), request_restart: Some(Self::request_restart), request_process: Some(Self::request_process), diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 14f343e..dc5b3f1 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -1,5 +1,9 @@ //! Abstractions for single CLAP plugin instances for audio thread interactions. +use super::process::ProcessData; +use super::{Plugin, PluginStatus}; +use crate::plugin::ext::Extension; +use crate::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; use clap_sys::process::{ @@ -9,19 +13,13 @@ use clap_sys::process::{ use std::marker::PhantomData; use std::ptr::NonNull; -use super::process::ProcessData; -use super::{Plugin, PluginStatus}; -use crate::plugin::ext::Extension; -use crate::plugin::instance::InstanceState; -use crate::util::unsafe_clap_call; - /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. pub struct PluginAudioThread<'a> { /// The plugin instance this audio thread belongs to. This is needed to ensure that the audio /// thread instance cannot outlive the plugin instance (which cannot outlive the plugin /// library). This `Plugin` also contains a reference to the plugin instance's state. - plugin: &'a Plugin<'a>, + pub(super) plugin: &'a Plugin<'a>, /// To honor CLAP's thread safety guidelines, this audio thread abstraction cannot be shared /// with or sent to other threads. _send_sync_marker: PhantomData<*const ()>, @@ -63,10 +61,6 @@ impl<'a> PluginAudioThread<'a> { self.plugin.as_ptr() } - pub fn state(&self) -> &InstanceState { - self.plugin.state() - } - /// Get the plugin's current initialization status. pub fn status(&self) -> PluginStatus { self.plugin.status() @@ -83,7 +77,9 @@ impl<'a> PluginAudioThread<'a> { let plugin = self.as_ptr(); for id in T::IDS { - let extension_ptr = unsafe_clap_call! { plugin=>get_extension(plugin, id.as_ptr()) }; + let extension_ptr = unsafe { + clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } + }; if !extension_ptr.is_null() { return unsafe { @@ -105,8 +101,12 @@ impl<'a> PluginAudioThread<'a> { self.status().assert_is(PluginStatus::Activated); let plugin = self.as_ptr(); - if unsafe_clap_call! { plugin=>start_processing(plugin) } { - self.plugin.state().status.store(PluginStatus::Processing); + let result = unsafe { + clap_call! { plugin=>start_processing(plugin) } + }; + + if result { + self.plugin.state.status.store(PluginStatus::Processing); Ok(()) } else { anyhow::bail!("'clap_plugin::start_processing()' returned false.") @@ -121,8 +121,8 @@ impl<'a> PluginAudioThread<'a> { self.status().assert_is(PluginStatus::Processing); let plugin = self.as_ptr(); - let result = process_data.with_clap_process_data(|clap_process_data| { - unsafe_clap_call! { plugin=>process(plugin, &clap_process_data) } + let result = process_data.with_clap_process_data(|clap_process_data| unsafe { + clap_call! { plugin=>process(plugin, &clap_process_data) } }); match result { @@ -145,7 +145,9 @@ impl<'a> PluginAudioThread<'a> { self.status().assert_active(); let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>reset(plugin) }; + unsafe { + clap_call! { plugin=>reset(plugin) } + }; } /// Stop processing audio. See @@ -155,7 +157,9 @@ impl<'a> PluginAudioThread<'a> { self.status().assert_is(PluginStatus::Processing); let plugin = self.as_ptr(); - unsafe_clap_call! { plugin=>stop_processing(plugin) }; + unsafe { + clap_call! { plugin=>stop_processing(plugin) } + }; self.plugin.state.status.store(PluginStatus::Activated); } diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index b26c487..be1928e 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -303,7 +303,8 @@ impl<'a> ProcessData<'a> { // Restart processing as necessary if plugin - .state() + .plugin + .state .requested_restart .compare_exchange(true, false) .is_ok() diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 77233fc..1a90da3 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -2,7 +2,7 @@ use super::instance::Plugin; use super::preset_discovery::PresetDiscoveryFactory; -use crate::util::{self, unsafe_clap_call}; +use crate::util::{self, clap_call}; use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory}; @@ -81,7 +81,10 @@ impl Drop for PluginLibrary { // plugin here let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); - unsafe_clap_call! { entry_point=>deinit() }; + + unsafe { + clap_call! { entry_point=>deinit() }; + } } } @@ -142,7 +145,11 @@ impl PluginLibrary { // The entry point needs to be initialized before it can be used. It will be deinitialized // when the `Plugin` object is dropped. let entry_point = get_clap_entry_point(&library)?; - if !unsafe_clap_call! { entry_point=>init(path_cstring.as_ptr()) } { + let result = unsafe { + clap_call! { entry_point=>init(path_cstring.as_ptr()) } + }; + + if !result { anyhow::bail!("'clap_plugin_entry::init({path_cstring:?})' returned false."); } @@ -161,8 +168,9 @@ impl PluginLibrary { pub fn metadata(&self) -> Result { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); - let plugin_factory = unsafe_clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } - as *const clap_plugin_factory; + let plugin_factory = unsafe { + clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } + } as *const clap_plugin_factory; let mut metadata = PluginLibraryMetadata { version: ( @@ -177,10 +185,15 @@ impl PluginLibrary { return Ok(metadata); } - let num_plugins = unsafe_clap_call! { plugin_factory=>get_plugin_count(plugin_factory) }; + let num_plugins = unsafe { + clap_call! { plugin_factory=>get_plugin_count(plugin_factory) } + }; + for i in 0..num_plugins { - let descriptor = - unsafe_clap_call! { plugin_factory=>get_plugin_descriptor(plugin_factory, i) }; + let descriptor = unsafe { + clap_call! { plugin_factory=>get_plugin_descriptor(plugin_factory, i) } + }; + if descriptor.is_null() { anyhow::bail!( "The plugin returned a null plugin descriptor for plugin index {i} (expected \ @@ -215,8 +228,9 @@ impl PluginLibrary { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); - let factory_pointer = - unsafe_clap_call! { entry_point=>get_factory(factory_id_cstring.as_ptr()) }; + let factory_pointer = unsafe { + clap_call! { entry_point=>get_factory(factory_id_cstring.as_ptr()) } + }; !factory_pointer.is_null() } @@ -228,8 +242,11 @@ impl PluginLibrary { pub fn create_plugin(&self, id: &str) -> Result> { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); - let plugin_factory = unsafe_clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } - as *const clap_plugin_factory; + + let plugin_factory = unsafe { + clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } + } as *const clap_plugin_factory; + if plugin_factory.is_null() { anyhow::bail!( "The plugin does not support the '{}' factory.", @@ -245,8 +262,10 @@ impl PluginLibrary { pub fn preset_discovery_factory(&self) -> Result> { let entry_point = get_clap_entry_point(&self.library) .expect("A Plugin was constructed for a plugin with no entry point"); - let preset_discovery_factory = unsafe_clap_call! { - entry_point=>get_factory(CLAP_PRESET_DISCOVERY_FACTORY_ID.as_ptr()) + let preset_discovery_factory = unsafe { + clap_call! { + entry_point=>get_factory(CLAP_PRESET_DISCOVERY_FACTORY_ID.as_ptr()) + } } as *mut clap_preset_discovery_factory; match NonNull::new(preset_discovery_factory) { diff --git a/src/plugin/preset_discovery.rs b/src/plugin/preset_discovery.rs index fae37ba..4d2511a 100644 --- a/src/plugin/preset_discovery.rs +++ b/src/plugin/preset_discovery.rs @@ -9,7 +9,7 @@ use std::collections::HashSet; use std::ptr::NonNull; use super::library::PluginLibrary; -use crate::util::{self, unsafe_clap_call}; +use crate::util::{self, clap_call}; mod indexer; mod metadata_receiver; @@ -101,11 +101,16 @@ impl<'lib> PresetDiscoveryFactory<'lib> { /// [`create()`][Self::create()]. pub fn metadata(&self) -> Result> { let factory = self.as_ptr(); - let num_providers = unsafe_clap_call! { factory=>count(factory) }; + let num_providers = unsafe { + clap_call! { factory=>count(factory) } + }; let mut metadata = Vec::with_capacity(num_providers as usize); for i in 0..num_providers { - let descriptor = unsafe_clap_call! { factory=>get_descriptor(factory, i) }; + let descriptor = unsafe { + clap_call! { factory=>get_descriptor(factory, i) } + }; + if descriptor.is_null() { anyhow::bail!( "The preset discovery factory returned a null pointer for the descriptor at \ diff --git a/src/plugin/preset_discovery/provider.rs b/src/plugin/preset_discovery/provider.rs index 7565d39..d573061 100644 --- a/src/plugin/preset_discovery/provider.rs +++ b/src/plugin/preset_discovery/provider.rs @@ -12,7 +12,7 @@ use walkdir::WalkDir; use super::indexer::{Indexer, IndexerResults}; use super::metadata_receiver::{MetadataReceiver, PresetFile}; use super::{Location, LocationValue, PresetDiscoveryFactory, ProviderMetadata}; -use crate::util::unsafe_clap_call; +use crate::util::clap_call; /// A preset discovery provider created from a preset discovery factory. The provider is initialized /// and the declared contents are read when the object is created, and the provider is destroyed @@ -50,13 +50,16 @@ impl<'a> Provider<'a> { CString::new(provider_id).expect("The provider ID contained internal null bytes"); let provider = { let factory = factory.as_ptr(); - let provider = unsafe_clap_call! { - factory=>create( - factory, - indexer.clap_preset_discovery_indexer_ptr(), - provider_id_cstring.as_ptr() - ) + let provider = unsafe { + clap_call! { + factory=>create( + factory, + indexer.clap_preset_discovery_indexer_ptr(), + provider_id_cstring.as_ptr() + ) + } }; + match NonNull::new(provider as *mut clap_preset_discovery_provider) { Some(provider) => provider, None => anyhow::bail!( @@ -68,7 +71,11 @@ impl<'a> Provider<'a> { let declared_data = { let provider = provider.as_ptr(); - if !unsafe_clap_call! { provider=>init(provider) } { + let result = unsafe { + clap_call! { provider=>init(provider) } + }; + + if !result { anyhow::bail!( "'clap_preset_discovery_factory::init()' returned false for the provider with \ ID '{provider_id}'." @@ -146,14 +153,17 @@ impl<'a> Provider<'a> { MetadataReceiver::new(&mut result, &location, location_flags); let provider = self.as_ptr(); - let success = unsafe_clap_call! { - provider=>get_metadata( - provider, - location_kind, - location_ptr, - metadata_receiver.clap_preset_discovery_metadata_receiver_ptr() - ) + let success = unsafe { + clap_call! { + provider=>get_metadata( + provider, + location_kind, + location_ptr, + metadata_receiver.clap_preset_discovery_metadata_receiver_ptr() + ) + } }; + if !success { // TODO: Is the plugin allowed to return false here? If it doesn't have any // presets it should just not declare any, right? diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index b1f766a..6fc211e 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -732,12 +732,12 @@ fn generate_param_diff( .value_to_text(param_id, actual_value) .ok() .flatten() - .unwrap_or("".to_string()); + .unwrap_or("".to_string()); let string_expected = params .value_to_text(param_id, expected_value) .ok() .flatten() - .unwrap_or("".to_string()); + .unwrap_or("".to_string()); Some(format!( "{}, {:?}, {:?}, {:.4}, {:?}, {:.4}", diff --git a/src/util.rs b/src/util.rs index 8b27d19..1815ed8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -8,8 +8,6 @@ use std::ffi::CStr; use std::os::raw::c_char; use std::path::PathBuf; -// TODO: Remove these attributes once we start implementing host interfaces - /// Assert that the specified pointers are non-null. Panics if this is not the case. macro_rules! check_null_ptr { ($ptr:expr) => { @@ -41,14 +39,7 @@ macro_rules! clap_call { } } -/// [`clap_call!()`], wrapped in an unsafe block. -macro_rules! unsafe_clap_call { - { $($args:tt)* } => { - unsafe { $crate::util::clap_call! { $($args)* } } - } -} - -pub(crate) use {check_null_ptr, clap_call, unsafe_clap_call}; +pub(crate) use {check_null_ptr, clap_call}; /// Similar to, [`std::any::type_name_of_val()`], but on stable Rust, and stripping away the pointer /// part. From 662c83d395ab8cc583ab31ec880a0566ddd01f7d Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 21 Jan 2026 14:14:29 +0400 Subject: [PATCH 038/114] latest macos runners --- .github/workflows/build.yml | 6 +++--- src/plugin/ext/audio_ports.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 578d8ff..fc9cc6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,9 @@ jobs: matrix: include: - { name: ubuntu-22.04, os: ubuntu-22.04, cross-target: '' } - - { name: macos-12-x86_64, os: macos-12, cross-target: '' } - - { name: macos-12-aarch64, os: macos-12, cross-target: aarch64-apple-darwin } - { name: windows, os: windows-latest, cross-target: '' } + - { name: macos-15-aarch64, os: macos-15, cross-target: '' } + - { name: macos-15-x86_64, os: macos-15, cross-target: x86_64-apple-darwin } name: Build binary runs-on: ${{ matrix.os }} steps: @@ -92,7 +92,7 @@ jobs: universal-binary: name: Build a universal macOS binary - runs-on: macos-12 + runs-on: macos-15 needs: package steps: - uses: actions/checkout@v4 diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index b413c3b..dc1ec23 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -295,6 +295,14 @@ fn is_audio_port_type_consistent( ); } + if info.channel_count.isqrt().pow(2) != info.channel_count { + anyhow::bail!( + "Expected a perfect square (1, 4, 9, ...) number of channels for ambisonic audio \ + port, but the audio port has {} channels.", + info.channel_count + ); + } + Ok(()) } else { log::warn!("Unknown audio port type '{port_type:?}'"); From b81e6e1f8d5f4d2701b0f62502cba4249335ff31 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 21 Jan 2026 14:17:09 +0400 Subject: [PATCH 039/114] latest macos runners --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc9cc6e..1dbd390 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,8 +101,8 @@ jobs: - name: Determine the previously build archive names run: | - echo "X86_64_ARCHIVE_NAME=clap-validator-$(git describe --always)-macos-12-x86_64" >> "$GITHUB_ENV" - echo "AARCH64_ARCHIVE_NAME=clap-validator-$(git describe --always)-macos-12-aarch64" >> "$GITHUB_ENV" + echo "X86_64_ARCHIVE_NAME=clap-validator-$(git describe --always)-macos-15-x86_64" >> "$GITHUB_ENV" + echo "AARCH64_ARCHIVE_NAME=clap-validator-$(git describe --always)-macos-15-aarch64" >> "$GITHUB_ENV" - name: Determine archive name for the universal binary run: | From 11f0efcff258d1aafd24fd38e9c229e6a4f937b7 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 24 Jan 2026 16:16:56 +0400 Subject: [PATCH 040/114] warn if none of applied layouts was accepted (configurable-audio-ports test) --- src/tests/plugin/layout.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 9ee96a5..9002278 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -292,7 +292,7 @@ pub fn test_layout_configurable_audio_ports( let mut checks_total = 0; let mut checks_passed = 0; - while checks_total < 100 && checks_passed < 10 { + while checks_total < 200 && checks_passed < 20 { let requests = random_layout_requests(&mut prng, &config_audio_ports); let can_apply = configurable_audio_ports.can_apply_configuration(requests.iter().cloned()); let has_applied = configurable_audio_ports.apply_configuration(requests.iter().cloned()); @@ -345,5 +345,13 @@ pub fn test_layout_configurable_audio_ports( .handle_callback() .context("An error occured during a callback")?; + if checks_passed == 0 { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "Tried 200 random audio port layouts, but none was accepted.", + )), + }); + } + Ok(TestStatus::Success { details: None }) } From 9f93fd3d88a9fbfd977c3e36f44bcbfc3fed3ec4 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sun, 25 Jan 2026 19:22:29 +0400 Subject: [PATCH 041/114] 64-bit out of place processing test --- src/plugin/ext/audio_ports.rs | 75 ++++++++++++++++++++++++++++- src/plugin/instance/process.rs | 87 ++++++++++++++++++++++++++++++---- src/tests/plugin.rs | 10 ++++ src/tests/plugin/layout.rs | 2 +- src/tests/plugin/processing.rs | 49 +++++++++++++++++++ 5 files changed, 211 insertions(+), 12 deletions(-) diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index dc1ec23..68deb81 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -8,8 +8,10 @@ use crate::util::clap_call; use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::{ - CLAP_AUDIO_PORT_IS_MAIN, CLAP_EXT_AUDIO_PORTS, CLAP_PORT_MONO, CLAP_PORT_STEREO, - clap_audio_port_info, clap_plugin_audio_ports, + CLAP_AUDIO_PORT_IS_MAIN, CLAP_AUDIO_PORT_PREFERS_64BITS, + CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE, CLAP_AUDIO_PORT_SUPPORTS_64BITS, + CLAP_EXT_AUDIO_PORTS, CLAP_PORT_MONO, CLAP_PORT_STEREO, clap_audio_port_info, + clap_plugin_audio_ports, }; use clap_sys::ext::surround::CLAP_PORT_SURROUND; use clap_sys::id::CLAP_INVALID_ID; @@ -40,9 +42,17 @@ pub struct AudioPort { /// The number of channels for an audio port. pub num_channels: u32, + /// The index if the output/input port this input/output port should be connected to. This is /// the index in the other **port list**, not a stable ID (which have already been translated). pub in_place_pair_idx: Option, + + /// Supports 64 bit processing + pub supports_double_sample_size: bool, + + /// All ports with this flag require common sample size + #[allow(unused)] // TODO: use for future mixed precision processing tests + pub requires_common_sample_size: bool, } impl<'a> Extension<&'a Plugin<'a>> for AudioPorts<'a> { @@ -86,6 +96,9 @@ impl AudioPorts<'_> { let mut input_stable_index_pairs: HashMap = HashMap::new(); let mut output_stable_index_pairs: HashMap = HashMap::new(); + let mut has_single_precision_requires_common_port = false; + let mut has_double_precision_requires_common_port = false; + for i in 0..num_inputs { let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; let success = unsafe { @@ -121,12 +134,36 @@ impl AudioPorts<'_> { ); } + let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; + let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; + let requires_common_sample_size = + (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + + if prefers_double_sample_size && !supports_double_sample_size { + anyhow::bail!( + "Input audio port {i} (id={}) prefers 64-bit sample size, but does not \ + support it.", + info.id + ); + } + + if requires_common_sample_size { + if supports_double_sample_size { + has_double_precision_requires_common_port = true; + } else { + has_single_precision_requires_common_port = true; + } + } + config.inputs.push(AudioPort { is_main, num_channels: info.channel_count, // These are reconstructed from `input_stable_index_pairs` and // `output_stable_index_pairs` later in_place_pair_idx: None, + + supports_double_sample_size, + requires_common_sample_size, }); } @@ -163,13 +200,46 @@ impl AudioPorts<'_> { ); } + let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; + let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; + let requires_common_sample_size = + (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + + if prefers_double_sample_size && !supports_double_sample_size { + anyhow::bail!( + "Output audio port {i} (id={}) prefers 64-bit sample size, but does not \ + support it.", + info.id + ); + } + + if requires_common_sample_size { + if supports_double_sample_size { + has_double_precision_requires_common_port = true; + } else { + has_single_precision_requires_common_port = true; + } + } + config.outputs.push(AudioPort { is_main, num_channels: info.channel_count, in_place_pair_idx: None, + + supports_double_sample_size, + requires_common_sample_size, }); } + // this implies that the common sample size requirement is useless (i.e. every port can only support + // 32bit sample size) and nullifies the 64 bit support of the other ports + if has_single_precision_requires_common_port && has_double_precision_requires_common_port { + anyhow::bail!( + "The plugin has audio ports that require common sample size, but some of these \ + ports only support 32-bit sample size while others support 64-bit sample size." + ); + } + // Now we need to convert the stable in-place pair indices to vector indices for (input_stable_id, (input_port_idx, pair_stable_id)) in input_stable_index_pairs .iter() @@ -295,6 +365,7 @@ fn is_audio_port_type_consistent( ); } + // ambisonic audio requires (N^2) channels where N is the ambisonics order if info.channel_count.isqrt().pow(2) != info.channel_count { anyhow::bail!( "Expected a perfect square (1, 4, 9, ...) number of channels for ambisonic audio \ diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs index be1928e..1946a46 100644 --- a/src/plugin/instance/process.rs +++ b/src/plugin/instance/process.rs @@ -461,17 +461,23 @@ impl AudioBuffers { .inputs .iter() .enumerate() - .map(|(index, port)| AudioBuffer::Float32 { - input: Some(index), - output: None, - data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], + .map(|(index, port)| { + AudioBuffer::new_out_of_place( + index, + true, + false, + port.num_channels as usize, + num_samples, + ) }) .chain(config.outputs.iter().enumerate().map(|(index, port)| { - AudioBuffer::Float32 { - input: None, - output: Some(index), - data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], - } + AudioBuffer::new_out_of_place( + index, + false, + false, + port.num_channels as usize, + num_samples, + ) })) .collect(), num_samples, @@ -512,6 +518,44 @@ impl AudioBuffers { Self::new(buffers, num_samples) } + pub fn new_out_of_place_f64(config: &AudioPortConfig, num_samples: usize) -> Option { + if !config + .inputs + .iter() + .chain(config.outputs.iter()) + .any(|port| port.supports_double_sample_size) + { + return None; + } + + Some(Self::new( + config + .inputs + .iter() + .enumerate() + .map(|(index, port)| { + AudioBuffer::new_out_of_place( + index, + true, + port.supports_double_sample_size, + port.num_channels as usize, + num_samples, + ) + }) + .chain(config.outputs.iter().enumerate().map(|(index, port)| { + AudioBuffer::new_out_of_place( + index, + false, + port.supports_double_sample_size, + port.num_channels as usize, + num_samples, + ) + })) + .collect(), + num_samples, + )) + } + /// The number of samples in the buffer. pub fn len(&self) -> usize { self.num_samples @@ -671,6 +715,31 @@ impl AudioBuffers { } impl AudioBuffer { + pub fn new_out_of_place( + port_index: usize, + is_input: bool, + is_double: bool, + num_channels: usize, + num_samples: usize, + ) -> Self { + let input = is_input.then_some(port_index); + let output = (!is_input).then_some(port_index); + + if is_double { + AudioBuffer::Float64 { + input, + output, + data: vec![vec![0.0f64; num_samples]; num_channels], + } + } else { + AudioBuffer::Float32 { + input, + output, + data: vec![vec![0.0f32; num_samples]; num_channels], + } + } + } + /// Get the index of the input bus for this buffer. pub fn input(&self) -> Option { match self { diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index c8d11ce..3c55467 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -30,6 +30,8 @@ pub enum PluginTestCase { ProcessAudioOutOfPlaceBasic, #[strum(serialize = "process-audio-in-place-basic")] ProcessAudioInPlaceBasic, + #[strum(serialize = "process-audio-out-of-place-double")] + ProcessAudioOutOfPlaceDouble, #[strum(serialize = "process-audio-constant-mask")] ProcessAudioConstantMask, #[strum(serialize = "process-audio-reset-determinism")] @@ -99,6 +101,11 @@ impl<'a> TestCase<'a> for PluginTestCase { tests whether the output does not contain any non-finite or subnormal values. \ Uses in-place audio processing for buses that support it.", ), + PluginTestCase::ProcessAudioOutOfPlaceDouble => format!( + "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones \ + for ports that support it.", + PluginTestCase::ProcessAudioOutOfPlaceBasic, + ), PluginTestCase::LayoutConfigurableAudioPorts => format!( "Performs the same test as {}, but this time it tries random configurations \ exposed via the 'configurable-audio-ports' extension.", @@ -248,6 +255,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessAudioInPlaceBasic => { processing::test_process_audio_basic(library, plugin_id, true) } + PluginTestCase::ProcessAudioOutOfPlaceDouble => { + processing::test_process_audio_double(library, plugin_id) + } PluginTestCase::ProcessAudioConstantMask => { processing::test_process_audio_constant_mask(library, plugin_id) } diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 9002278..5491cb7 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -346,7 +346,7 @@ pub fn test_layout_configurable_audio_ports( .context("An error occured during a callback")?; if checks_passed == 0 { - return Ok(TestStatus::Skipped { + return Ok(TestStatus::Warning { details: Some(String::from( "Tried 200 random audio port layouts, but none was accepted.", )), diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 6a62052..7292708 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -106,6 +106,55 @@ pub fn test_process_audio_basic( Ok(TestStatus::Success { details: None }) } +// The test for `PluginTestCase::ProcessAudioOutOfPlaceDouble`. +pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not implement the 'audio-ports' extension.", + )), + }); + } + }; + + plugin + .handle_callback() + .context("An error occured during a callback")?; + + let Some(mut audio_buffers) = + AudioBuffers::new_out_of_place_f64(&audio_ports_config, BUFFER_SIZE) + else { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not support 64-bit floating point audio.", + )), + }); + }; + + let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + run_simple(&plugin, &mut process_data, 5, |process_data| { + process_data.buffers.randomize(&mut prng); + Ok(()) + })?; + + plugin + .handle_callback() + .context("An error occured during a callback")?; + + Ok(TestStatus::Success { details: None }) +} + /// The test for `PluginTestCase::ProcessNoteOutOfPlaceBasic` and `PluginTestCase::ProcessNoteInconsistent`. This test is very similar to /// `ProcessAudioOutOfPlaceBasic`, but it requires the `note-ports` extension, sends notes and/or /// MIDI to the plugin, and doesn't require the `audio-ports` extension. From 2f3a1d625c96e1a61f6d4692d6d0cfa4e60b6817 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sun, 25 Jan 2026 19:28:41 +0400 Subject: [PATCH 042/114] fix process audio constant mask warning when no input ports available --- src/tests/plugin/processing.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 7292708..e288bec 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -455,6 +455,14 @@ pub fn test_process_audio_constant_mask( } }; + if audio_ports_config.inputs.is_empty() { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not have any audio input ports.", + )), + }); + } + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut original_buffers = audio_buffers.clone(); let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); From 3b5af4a294dab47b2581033b031c0aed0752c017 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 29 Jan 2026 03:13:49 +0400 Subject: [PATCH 043/114] big refactor --- Cargo.lock | 83 +- Cargo.toml | 3 +- rustfmt.toml | 3 +- src/main.rs | 4 + src/plugin.rs | 1 + src/plugin/ext/params.rs | 19 +- src/plugin/ext/state.rs | 4 +- src/plugin/instance.rs | 412 ++++---- src/plugin/instance/audio_thread.rs | 114 +- src/plugin/instance/process.rs | 983 ------------------ src/plugin/library.rs | 88 +- src/plugin/preset_discovery/indexer.rs | 38 +- .../preset_discovery/metadata_receiver.rs | 43 +- src/plugin/process.rs | 251 +++++ src/plugin/process/buffer.rs | 497 +++++++++ src/plugin/process/events.rs | 190 ++++ src/plugin/process/transport.rs | 99 ++ src/tests/plugin.rs | 224 ++-- src/tests/plugin/layout.rs | 87 +- src/tests/plugin/params.rs | 287 ++--- src/tests/plugin/processing.rs | 628 ++++------- src/tests/plugin/state.rs | 227 ++-- src/tests/plugin_library/preset_discovery.rs | 10 +- src/tests/rng.rs | 121 ++- src/util.rs | 28 +- 25 files changed, 2050 insertions(+), 2394 deletions(-) delete mode 100644 src/plugin/instance/process.rs create mode 100644 src/plugin/process.rs create mode 100644 src/plugin/process/buffer.rs create mode 100644 src/plugin/process/events.rs create mode 100644 src/plugin/process/transport.rs diff --git a/Cargo.lock b/Cargo.lock index 815720c..a97f731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,13 +205,12 @@ dependencies = [ "clap-sys 0.5.0 (git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156)", "colored", "core-foundation", - "crossbeam", + "crossbeam-utils", "either", "libloading", "log", "log-panics", "midi-consts", - "parking_lot", "rand", "rand_pcg", "rayon", @@ -288,30 +287,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "crossbeam" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" -dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -336,24 +311,11 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" @@ -510,16 +472,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.19" @@ -586,29 +538,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.48.1", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -856,12 +785,6 @@ dependencies = [ "time 0.3.36", ] -[[package]] -name = "smallvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" - [[package]] name = "smawk" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index cb18314..2e2cde1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,11 @@ clap = { version = "4.1.8", features = ["derive", "wrap_help"] } # For CLAP 1.2.2 support clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } colored = "3.0.0" -crossbeam = "0.8.1" +crossbeam-utils = "0.8.21" libloading = "0.9.0" log = "0.4" log-panics = "2.0" midi-consts = "0.1.0" -parking_lot = "0.12.1" rand = "0.9.2" rand_pcg = "0.9.0" rayon = "1.6.1" diff --git a/rustfmt.toml b/rustfmt.toml index b953752..b8ffc7c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,3 @@ format_strings = true -comment_width = 100 \ No newline at end of file +comment_width = 120 +max_width = 120 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fd4ecbf..7086492 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ use clap::{Parser, Subcommand, ValueEnum}; use std::process::ExitCode; +use crate::plugin::library::mark_current_thread_as_os_main_thread; + mod commands; mod index; mod plugin; @@ -51,6 +53,8 @@ enum Command { } fn main() -> ExitCode { + mark_current_thread_as_os_main_thread(); + let cli = Cli::parse(); // For now logging everything to the terminal is fine. In the future it may be useful to have diff --git a/src/plugin.rs b/src/plugin.rs index 7e1d8e7..fd42604 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -4,3 +4,4 @@ pub mod ext; pub mod instance; pub mod library; pub mod preset_discovery; +pub mod process; diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 2cb41bd..910ff30 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -1,15 +1,11 @@ //! Abstractions for interacting with the `params` extension. +use super::Extension; +use crate::plugin::instance::{Plugin, PluginStatus}; +use crate::plugin::process::EventQueue; +use crate::util::{self, c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; -use clap_sys::ext::params::{ - CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL, - CLAP_PARAM_IS_AUTOMATABLE_PER_KEY, CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID, - CLAP_PARAM_IS_AUTOMATABLE_PER_PORT, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_HIDDEN, - CLAP_PARAM_IS_MODULATABLE, CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL, - CLAP_PARAM_IS_MODULATABLE_PER_KEY, CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID, - CLAP_PARAM_IS_MODULATABLE_PER_PORT, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, - clap_param_info, clap_param_info_flags, clap_plugin_params, -}; +use clap_sys::ext::params::*; use clap_sys::id::clap_id; use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::collections::BTreeMap; @@ -18,11 +14,6 @@ use std::ops::RangeInclusive; use std::pin::Pin; use std::ptr::NonNull; -use super::Extension; -use crate::plugin::instance::process::EventQueue; -use crate::plugin::instance::{Plugin, PluginStatus}; -use crate::util::{self, c_char_slice_to_string, clap_call}; - pub type ParamInfo = BTreeMap; /// Abstraction for the `params` extension covering the main thread functionality. diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index f5fca15..8da15ef 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -3,10 +3,10 @@ use anyhow::Result; use clap_sys::ext::state::{CLAP_EXT_STATE, clap_plugin_state}; use clap_sys::stream::{clap_istream, clap_ostream}; -use parking_lot::Mutex; use std::ffi::{CStr, c_void}; use std::pin::Pin; use std::ptr::NonNull; +use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; use super::Extension; @@ -234,6 +234,7 @@ impl OutputStream { unsafe { Pin::into_inner_unchecked(self) } .buffer .into_inner() + .unwrap() } unsafe extern "C" fn write( @@ -253,6 +254,7 @@ impl OutputStream { this.buffer .lock() + .unwrap() .extend_from_slice(std::slice::from_raw_parts( buffer as *const u8, size as usize, diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 559acb5..d1798e6 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -3,21 +3,17 @@ use super::ext::Extension; use super::library::{PluginLibrary, PluginMetadata}; use crate::plugin::preset_discovery::LocationValue; -use crate::util::{self, check_null_ptr, clap_call}; +use crate::util::{self, AssertSendSync, check_null_ptr, clap_call, validator_version}; use anyhow::{Context, Result}; -use audio_thread::PluginAudioThread; -use clap_sys::ext::audio_ports::{ - CLAP_AUDIO_PORTS_RESCAN_NAMES, CLAP_EXT_AUDIO_PORTS, clap_host_audio_ports, -}; +use clap_sys::ext::audio_ports::{CLAP_AUDIO_PORTS_RESCAN_NAMES, CLAP_EXT_AUDIO_PORTS, clap_host_audio_ports}; use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_host_latency}; use clap_sys::ext::note_ports::{ - CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, - CLAP_NOTE_DIALECT_MIDI_MPE, CLAP_NOTE_PORTS_RESCAN_ALL, CLAP_NOTE_PORTS_RESCAN_NAMES, - clap_host_note_ports, clap_note_dialect, + CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, + CLAP_NOTE_PORTS_RESCAN_ALL, CLAP_NOTE_PORTS_RESCAN_NAMES, clap_host_note_ports, clap_note_dialect, }; use clap_sys::ext::params::{ - CLAP_EXT_PARAMS, CLAP_PARAM_RESCAN_ALL, CLAP_PARAM_RESCAN_INFO, CLAP_PARAM_RESCAN_TEXT, - CLAP_PARAM_RESCAN_VALUES, clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, + CLAP_EXT_PARAMS, CLAP_PARAM_RESCAN_ALL, CLAP_PARAM_RESCAN_INFO, CLAP_PARAM_RESCAN_TEXT, CLAP_PARAM_RESCAN_VALUES, + clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, }; use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; @@ -30,35 +26,38 @@ use clap_sys::host::clap_host; use clap_sys::id::clap_id; use clap_sys::plugin::clap_plugin; use clap_sys::version::CLAP_VERSION; -use crossbeam::atomic::AtomicCell; -use crossbeam::queue::SegQueue; -use std::ffi::{CStr, CString, c_char, c_void}; +use crossbeam_utils::atomic::AtomicCell; +use std::ffi::{CStr, c_char, c_void}; use std::marker::PhantomData; use std::panic::resume_unwind; use std::pin::Pin; use std::ptr::NonNull; -use std::sync::{Arc, OnceLock}; +use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::{Arc, Mutex}; use std::thread::ThreadId; -pub mod audio_thread; -pub mod process; +mod audio_thread; +pub use audio_thread::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CallbackEvent { RequestProcess, RequestFlush, - RescanParamsValues, - RescanParamsText, - RescanParamsInfo, - RescanParamsAll, - RescanAudioPortsNames, - RescanAudioPortsAll, - RescanNotePortsNames, - RescanNotePortsAll, - ChangedLatency, - ChangedTail, - ChangedVoiceInfo, - ChangedState, + + ParamsRescanValues, + ParamsRescanText, + ParamsRescanInfo, + ParamsRescanAll, + + AudioPortsRescanNames, + AudioPortsRescanAll, + NotePortsRescanNames, + NotePortsRescanAll, + + LatencyChanged, + TailChanged, + VoiceInfoChanged, + StateMarkDirty, } /// The plugin's current lifecycle state. This is checked extensively to ensure that the plugin is @@ -80,8 +79,8 @@ impl PluginStatus { pub fn assert_is(&self, expected: PluginStatus) { if *self != expected { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, \ - must be {:?}). This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be {:?}). This is \ + a bug in the validator.", self, expected ) } @@ -91,8 +90,8 @@ impl PluginStatus { pub fn assert_is_not(&self, unexpected: PluginStatus) { if *self == unexpected { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, \ - must not be {:?}). This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must not be {:?}). \ + This is a bug in the validator.", self, unexpected ) } @@ -102,8 +101,8 @@ impl PluginStatus { pub fn assert_active(&self) { if *self < PluginStatus::Activated { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, \ - must be activated). This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be activated). \ + This is a bug in the validator.", self ) } @@ -113,8 +112,8 @@ impl PluginStatus { pub fn assert_inactive(&self) { if *self >= PluginStatus::Activated { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, \ - must be deactivated). This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be deactivated). \ + This is a bug in the validator.", self ) } @@ -132,27 +131,28 @@ pub struct Plugin<'lib> { /// Information about this plugin instance stored on the host. This keeps track of things like /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - state: Pin>, + shared: Pin>, + + main: InstanceMainThread, /// The CLAP plugin library this plugin instance was created from. This field is not used /// directly, but keeping a reference to the library here prevents the plugin instance from /// outliving the library. - _library: &'lib PluginLibrary, + _library: PhantomData<&'lib PluginLibrary>, /// To honor CLAP's thread safety guidelines, the thread this object was created from is /// designated the 'main thread', and this object cannot be shared with other threads. The /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call /// the plugin's audio thread functions. - _send_sync_marker: PhantomData<*const ()>, + _thread: PhantomData<*const ()>, } impl Drop for Plugin<'_> { fn drop(&mut self) { - if let Some(error) = self.state.callback_error.take() { + if let Some(error) = self.shared.callback_error.lock().unwrap().take() { log::warn!( - "The validator's host has detected a callback error but this error has not been \ - used as part of the test result. This could be a clap-validator bug. The error \ - message is: {error}" + "The validator's host has detected a callback error but this error has not been used as part of the \ + test result. This could be a clap-validator bug. The error message is: {error}" ) } @@ -161,8 +161,8 @@ impl Drop for Plugin<'_> { PluginStatus::Uninitialized | PluginStatus::Deactivated => (), PluginStatus::Activated => self.deactivate(), status => panic!( - "The plugin was in an invalid state '{status:?}' when the instance got dropped, \ - this is a clap-validator bug" + "The plugin was in an invalid state '{status:?}' when the instance got dropped, this is a \ + clap-validator bug" ), } @@ -179,30 +179,28 @@ impl<'lib> Plugin<'lib> { /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the /// plugin could not be created. The plugin instance will be registered with the host, and /// unregistered when this object is dropped again. - pub(crate) fn new( - library: &'lib PluginLibrary, - factory: &clap_plugin_factory, - plugin_id: &CStr, - ) -> Result { - let state = InstanceState::new(); + /// + /// # Safety + /// This MUST be called on the OS main thread (if applicable). + pub(crate) unsafe fn new(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result { + let (shared, main) = InstanceShared::new(); let plugin = unsafe { clap_call! { - factory=>create_plugin(factory, state.clap_host_ptr(), plugin_id.as_ptr()) + factory=>create_plugin(factory, shared.clap_host_ptr(), plugin_id.as_ptr()) } }; if plugin.is_null() { - anyhow::bail!( - "'clap_plugin_factory::create_plugin({plugin_id:?})' returned a null pointer." - ); + anyhow::bail!("'clap_plugin_factory::create_plugin({plugin_id:?})' returned a null pointer."); } Ok(Plugin { handle: NonNull::new(plugin as *mut clap_plugin).unwrap(), - state, + shared, + main, - _library: library, - _send_sync_marker: PhantomData, + _library: PhantomData, + _thread: PhantomData, }) } @@ -225,7 +223,11 @@ impl<'lib> Plugin<'lib> { /// The plugin's current initialization status. pub fn status(&self) -> PluginStatus { - self.state.status.load() + self.shared.status.load() + } + + pub fn shared(&self) -> &Pin> { + &self.shared } /// Handle any pending main-thread callbacks for this plugin. @@ -233,13 +235,14 @@ impl<'lib> Plugin<'lib> { pub fn handle_callback(&self) -> Result<()> { self.handle_callback_unchecked(); - if let Some(error) = self.state.callback_error.take() { + if let Some(error) = self.shared.callback_error.lock().unwrap().take() { anyhow::bail!(error); } - while let Some(event) = self.state.callback_events.pop() { - println!("{:?}", event); - } + // TODO: + // while let Ok(event) = self.shared.callback_receiver.lock().unwrap().recv() { + // println!("{:?}", event); + // } Ok(()) } @@ -257,12 +260,7 @@ impl<'lib> Plugin<'lib> { }; if !extension_ptr.is_null() { - return unsafe { - Some(T::new( - self, - NonNull::new(extension_ptr as *mut T::Struct).unwrap(), - )) - }; + return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; } } @@ -275,70 +273,26 @@ impl<'lib> Plugin<'lib> { /// /// If whatever happens on the audio thread caused main-thread callback requests to be emited, /// then those will be handled concurrently. - pub fn on_audio_thread<'a, T: Send, F: FnOnce(PluginAudioThread<'a>) -> T + Send>( - &'a self, - f: F, - ) -> T { - struct SendWrapper<'lib>(&'lib Plugin<'lib>); - - // SAFETY: We artificially impose `!Send`+`!Sync` requirements on `Plugin` and - // `PluginAudioThread` to prevent them from being shared with other - // threads. But we'll need to temporarily lift that restriction in order - // to create this `PluginAudioThread`. - unsafe impl<'lib> Send for SendWrapper<'lib> {} - unsafe impl<'lib> Sync for SendWrapper<'lib> {} - - impl<'lib> SendWrapper<'lib> { - fn get(&self) -> &'lib Plugin<'lib> { - self.0 - } - } - - self.status().assert_is(PluginStatus::Activated); - - let is_running = AtomicCell::new(true); - let send_wrapper = SendWrapper(self); - - crossbeam::scope(|s| { - let audio_thread = s - .builder() - .name(String::from("audio-thread")) - .spawn(|_| { - struct SetFalseOnDrop<'a>(&'a AtomicCell); - impl<'a> Drop for SetFalseOnDrop<'a> { - fn drop(&mut self) { - self.0.store(false); - } - } - - let this = send_wrapper.get(); - - // So we know when to stop handling callbacks on the main thread - // even if the audio thread panics - let _guard = SetFalseOnDrop(&is_running); - - // This is used to check that calls are run from an audio thread - this.state - .audio_thread_id - .store(Some(std::thread::current().id())); + pub fn on_audio_thread T + Send>(&self, f: F) -> T { + let plugin = unsafe { AssertSendSync::new(self) }; - f(PluginAudioThread::new(this)) - }) - .expect("Unable to spawn an audio thread"); + let result = std::thread::scope(|s| { + let thread = s.spawn(|| f(PluginAudioThread::new(plugin.get()))); // Handle callbacks requests on the main thread while the audio thread is running - while is_running.load() { - self.handle_callback_unchecked(); - std::thread::sleep(std::time::Duration::from_millis(5)); + while let Ok(task) = self.main.task_receiver.recv() { + match task { + MainThreadTask::Closure(closure) => closure(self), + MainThreadTask::CallbackRequest => self.handle_callback_unchecked(), + MainThreadTask::StopAudioThread => break, + } } - self.handle_callback_unchecked(); + thread.join().unwrap_or_else(|panic_info| resume_unwind(panic_info)) + }); - audio_thread - .join() - .unwrap_or_else(|panic_info| resume_unwind(panic_info)) - }) - .unwrap() + self.handle_callback_unchecked(); + result } /// Initialize the plugin. This needs to be called before doing anything else. @@ -357,7 +311,7 @@ impl<'lib> Plugin<'lib> { "clap_plugin::on_main_thread is null" ); - self.state.status.store(PluginStatus::Deactivated); + self.shared.status.store(PluginStatus::Deactivated); Ok(()) } else { anyhow::bail!("'clap_plugin::init()' returned false.") @@ -367,12 +321,7 @@ impl<'lib> Plugin<'lib> { /// Activate the plugin. Returns an error if the plugin returned `false`. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. - pub fn activate( - &self, - sample_rate: f64, - min_buffer_size: usize, - max_buffer_size: usize, - ) -> Result<()> { + pub fn activate(&self, sample_rate: f64, min_buffer_size: u32, max_buffer_size: u32) -> Result<()> { self.status().assert_is(PluginStatus::Deactivated); // Apparently 0 is invalid here @@ -380,18 +329,18 @@ impl<'lib> Plugin<'lib> { assert!(max_buffer_size >= min_buffer_size); // we need to track the `Activating` state to validate that we call clap_host_latency::changed only within the activation call. - self.state.status.store(PluginStatus::Activating); + self.shared.status.store(PluginStatus::Activating); let plugin = self.as_ptr(); let result = unsafe { - clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size as u32, max_buffer_size as u32) } + clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size, max_buffer_size) } }; if result { - self.state.status.store(PluginStatus::Activated); + self.shared.status.store(PluginStatus::Activated); Ok(()) } else { - self.state.status.store(PluginStatus::Deactivated); + self.shared.status.store(PluginStatus::Deactivated); anyhow::bail!("'clap_plugin::activate()' returned false.") } } @@ -407,11 +356,11 @@ impl<'lib> Plugin<'lib> { clap_call! { plugin=>deactivate(plugin) } } - self.state.status.store(PluginStatus::Deactivated); + self.shared.status.store(PluginStatus::Deactivated); } fn handle_callback_unchecked(&self) { - if self.state.requested_callback.swap(false) { + if self.shared.requested_callback.swap(false) { let plugin = self.as_ptr(); unsafe { clap_call! { plugin=>on_main_thread(plugin) } @@ -420,12 +369,19 @@ impl<'lib> Plugin<'lib> { } } +pub enum MainThreadTask { + Closure(Box), + CallbackRequest, + StopAudioThread, +} + /// Runtime information about a plugin instance. This keeps track of pending callbacks and things /// like audio threads. It also contains the plugin's unique `clap_host` struct so host callbacks /// can be linked back to this specific plugin instance. -struct InstanceState { - pub callback_events: SegQueue, - pub callback_error: AtomicCell>, +pub struct InstanceShared { + pub task_sender: Sender, + pub callback_sender: Sender, + pub callback_error: Mutex>, /// The plugin's current state in terms of activation and processing status. pub status: AtomicCell, @@ -456,14 +412,21 @@ struct InstanceState { clap_host_voice_info: clap_host_voice_info, } -impl InstanceState { - pub fn new() -> Pin> { - static VERSION: OnceLock = OnceLock::new(); +struct InstanceMainThread { + callback_receiver: Receiver, + task_receiver: Receiver, +} +impl InstanceShared { + fn new() -> (Pin>, InstanceMainThread) { let main_thread = std::thread::current().id(); - let instance = Arc::pin(InstanceState { - callback_events: SegQueue::new(), - callback_error: AtomicCell::new(None), + let (callback_sender, callback_receiver) = channel(); + let (task_sender, task_receiver) = channel(); + + let shared = Arc::pin(InstanceShared { + task_sender, + callback_sender, + callback_error: Mutex::new(None), status: AtomicCell::new(PluginStatus::Uninitialized), main_thread_id: main_thread, @@ -478,9 +441,7 @@ impl InstanceState { name: c"clap-validator".as_ptr(), vendor: c"Robbert van der Helm".as_ptr(), url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: VERSION - .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).unwrap()) - .as_ptr(), + version: validator_version().as_ptr(), get_extension: Some(Self::get_extension), request_restart: Some(Self::request_restart), request_process: Some(Self::request_process), @@ -522,15 +483,20 @@ impl InstanceState { }, }); + let main = InstanceMainThread { + callback_receiver, + task_receiver, + }; + // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct // so it can be retrieved in host callbacks unsafe { - (&raw const instance.clap_host.host_data) + (&raw const shared.clap_host.host_data) .cast_mut() - .write(&*instance as *const _ as *mut std::ffi::c_void); + .write(&*shared as *const _ as *mut std::ffi::c_void); } - instance + (shared, main) } pub fn clap_host_ptr(&self) -> *const clap_host { @@ -540,7 +506,7 @@ impl InstanceState { #[track_caller] pub unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { unsafe { - let state = (*host).host_data as *const InstanceState; + let state = (*host).host_data as *const InstanceShared; &*state } } @@ -548,8 +514,9 @@ impl InstanceState { /// Set the callback error field if it does not already contain a value. Earlier errors are not /// overwritten. fn set_callback_error(&self, error: impl Into) { - if let Some(old_error) = self.callback_error.swap(Some(error.into())) { - self.callback_error.store(Some(old_error)); + let mut guard = self.callback_error.lock().unwrap(); + if guard.is_none() { + *guard = Some(error.into()); } } @@ -560,8 +527,7 @@ impl InstanceState { let current_thread_id = std::thread::current().id(); if current_thread_id != self.main_thread_id { self.set_callback_error(format!( - "'{}' may only be called from the main thread (thread {:?}), but it was called \ - from thread {:?}.", + "'{}' may only be called from the main thread (thread {:?}), but it was called from thread {:?}.", function_name, self.main_thread_id, current_thread_id )); } @@ -575,13 +541,13 @@ impl InstanceState { if self.audio_thread_id.load() != Some(current_thread_id) { if current_thread_id == self.main_thread_id { self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called \ - from the main thread." + "'{function_name}' may only be called from an audio thread, but it was called from the main \ + thread." )); } else { self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called \ - from an unknown thread." + "'{function_name}' may only be called from an audio thread, but it was called from an unknown \ + thread." )); } } @@ -599,12 +565,9 @@ impl InstanceState { } } - unsafe extern "C" fn get_extension( - host: *const clap_host, - extension_id: *const c_char, - ) -> *const c_void { + unsafe extern "C" fn get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { //check_null_ptr!(host, (*host).host_data, extension_id); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; // Right now there's no way to have the host only expose certain extensions. We can always // add that when test cases need it. @@ -634,7 +597,7 @@ impl InstanceState { unsafe extern "C" fn request_restart(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and // in the multi-iteration run function it will trigger a deactivate->reactivate cycle @@ -644,31 +607,29 @@ impl InstanceState { unsafe extern "C" fn request_process(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; // Handling this within the context of the validator would be a bit messy. Do plugins use // this? log::trace!("'clap_host::request_process()' was called by the plugin"); - this.callback_events.push(CallbackEvent::RequestProcess); + this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); } unsafe extern "C" fn request_callback(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; // This this is either handled by `handle_callbacks_blocking()` while the audio thread is // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the // callback is not handled before the plugin is destroyed. log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); this.requested_callback.store(true); + this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); } - unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported( - host: *const clap_host, - _flag: u32, - ) -> bool { + unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); @@ -677,33 +638,27 @@ impl InstanceState { unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::rescan()"); log::trace!("'clap_host_audio_ports::rescan()' was called"); if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - this.callback_events - .push(CallbackEvent::RescanAudioPortsNames); + this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); } if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { if this.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_audio_ports::rescan()' was called while the plugin was activated", - ); + this.set_callback_error("'clap_host_audio_ports::rescan()' was called while the plugin was activated"); } - this.callback_events - .push(CallbackEvent::RescanAudioPortsAll); + this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); } } - unsafe extern "C" fn ext_note_ports_supported_dialects( - host: *const clap_host, - ) -> clap_note_dialect { + unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::supported_dialects()"); log::trace!("'clap_host_note_ports::supported_dialects()' was called"); @@ -713,25 +668,24 @@ impl InstanceState { unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::rescan()"); log::trace!("'clap_host_note_ports::rescan()' was called"); if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { - this.callback_events - .push(CallbackEvent::RescanNotePortsNames); + this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); } if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { if this.status.load() > PluginStatus::Activated { this.set_callback_error( - "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while \ - the plugin was activated", + "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while the plugin was \ + activated", ); } - this.callback_events.push(CallbackEvent::RescanNotePortsAll); + this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); } } @@ -744,28 +698,27 @@ impl InstanceState { msg: *const c_char, ) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::on_error()"); let location = unsafe { LocationValue::new(location_kind, location) } .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) }.context( - "'clap_host_preset_load::on_error()' called with an invalid load_key parameter", - ); + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'clap_host_preset_load::on_error()' called with an invalid load_key parameter"); let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); match (location, load_key, msg) { (Ok(location), Ok(Some(load_key)), Ok(msg)) => { this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with load key \ - {load_key}, OS error code {os_error}, and the following error message: {msg}" + "'clap_host_preset_load::on_error()' called for {location} with load key {load_key}, OS error \ + code {os_error}, and the following error message: {msg}" )); } (Ok(location), Ok(None), Ok(msg)) => { this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with no load key, \ - OS error code {os_error}, and the following error message: {msg}" + "'clap_host_preset_load::on_error()' called for {location} with no load key, OS error code \ + {os_error}, and the following error message: {msg}" )); } (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { @@ -781,7 +734,7 @@ impl InstanceState { load_key: *const c_char, ) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::loaded()"); @@ -789,6 +742,7 @@ impl InstanceState { .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); + match (location, load_key) { (Ok(_location), Ok(_load_key)) => { log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); @@ -801,42 +755,37 @@ impl InstanceState { unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::rescan()"); log::trace!("'clap_host_params::rescan()' was called"); if flags & CLAP_PARAM_RESCAN_VALUES != 0 { - this.callback_events.push(CallbackEvent::RescanParamsValues); + this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); } if flags & CLAP_PARAM_RESCAN_TEXT != 0 { - this.callback_events.push(CallbackEvent::RescanParamsText); + this.callback_sender.send(CallbackEvent::ParamsRescanText).unwrap(); } if flags & CLAP_PARAM_RESCAN_INFO != 0 { - this.callback_events.push(CallbackEvent::RescanParamsInfo); + this.callback_sender.send(CallbackEvent::ParamsRescanInfo).unwrap(); } if flags & CLAP_PARAM_RESCAN_ALL != 0 { if this.status.load() > PluginStatus::Activated { this.set_callback_error( - "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the \ - plugin is activated", + "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is activated", ); } - this.callback_events.push(CallbackEvent::RescanParamsAll); + this.callback_sender.send(CallbackEvent::ParamsRescanAll).unwrap(); } } - unsafe extern "C" fn ext_params_clear( - host: *const clap_host, - _param_id: clap_id, - _flags: clap_param_clear_flags, - ) { + unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::clear()"); log::debug!("TODO: Handle 'clap_host_params::clear()'"); @@ -844,65 +793,64 @@ impl InstanceState { unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_not_audio_thread("clap_host_params::request_flush()"); log::trace!("'clap_host_params::request_flush()' was called"); - this.callback_events.push(CallbackEvent::RequestFlush); + this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); } unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_state::mark_dirty()"); log::trace!("'clap_host_state::mark_dirty()' was called"); - this.callback_events.push(CallbackEvent::ChangedState); + this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); } unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.main_thread_id == std::thread::current().id() } unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.audio_thread_id.load() == Some(std::thread::current().id()) } unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; if this.status.load() != PluginStatus::Activating { this.set_callback_error( - "'clap_host_latency::changed()' must only be called within \ - 'clap_plugin::activate()'", + "'clap_host_latency::changed()' must only be called within 'clap_plugin::activate()'", ); } this.assert_main_thread("clap_host_latency::changed()"); log::trace!("'clap_host_latency::changed()' was called"); - this.callback_events.push(CallbackEvent::ChangedLatency); + this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); } unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_audio_thread("clap_host_tail::changed()"); log::trace!("'clap_host_tail::changed()' was called"); - this.callback_events.push(CallbackEvent::ChangedTail); + this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); } unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceState::from_clap_host(host) }; + let this = unsafe { InstanceShared::from_clap_host(host) }; this.assert_main_thread("clap_host_voice_info::changed()"); log::trace!("'clap_host_voice_info::changed()' was called"); - this.callback_events.push(CallbackEvent::ChangedVoiceInfo); + this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); } } diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index dc5b3f1..ab1b9b4 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -1,17 +1,19 @@ //! Abstractions for single CLAP plugin instances for audio thread interactions. -use super::process::ProcessData; use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; -use crate::util::clap_call; +use crate::plugin::instance::{InstanceShared, MainThreadTask}; +use crate::util::{AssertSendSync, clap_call}; use anyhow::Result; use clap_sys::plugin::clap_plugin; use clap_sys::process::{ - CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR, - CLAP_PROCESS_SLEEP, CLAP_PROCESS_TAIL, + CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR, CLAP_PROCESS_SLEEP, + CLAP_PROCESS_TAIL, clap_process, }; use std::marker::PhantomData; +use std::pin::Pin; use std::ptr::NonNull; +use std::sync::{Arc, Condvar, Mutex}; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. @@ -20,8 +22,9 @@ pub struct PluginAudioThread<'a> { /// thread instance cannot outlive the plugin instance (which cannot outlive the plugin /// library). This `Plugin` also contains a reference to the plugin instance's state. pub(super) plugin: &'a Plugin<'a>, - /// To honor CLAP's thread safety guidelines, this audio thread abstraction cannot be shared - /// with or sent to other threads. + + /// To honor CLAP's thread safety guidelines, the thread this object was created from is + /// designated the 'audio thread', and this object cannot be shared with other threads. _send_sync_marker: PhantomData<*const ()>, } @@ -37,20 +40,20 @@ pub enum ProcessStatus { impl Drop for PluginAudioThread<'_> { fn drop(&mut self) { - match self.status() { - PluginStatus::Processing => self.stop_processing(), - PluginStatus::Activated => (), - state => panic!( - "The plugin was in an invalid state '{state:?}' when the audio thread got \ - dropped, this is a clap-validator bug" - ), - } + self.plugin.shared.audio_thread_id.store(None); + self.plugin + .shared + .task_sender + .send(MainThreadTask::StopAudioThread) + .unwrap(); } } impl<'a> PluginAudioThread<'a> { - pub fn new(plugin: &'a Plugin) -> Self { - PluginAudioThread { + pub(crate) fn new(plugin: &'a Plugin<'a>) -> Self { + plugin.shared.audio_thread_id.store(Some(std::thread::current().id())); + + Self { plugin, _send_sync_marker: PhantomData, } @@ -66,11 +69,14 @@ impl<'a> PluginAudioThread<'a> { self.plugin.status() } + /// Get a reference to the plugin's shared state. + pub fn shared(&self) -> &Pin> { + &self.plugin.shared + } + /// Get the _audio thread_ extension abstraction for the extension `T`, if the plugin supports /// this extension. Returns `None` if it does not. The plugin needs to be initialized using /// [`init()`][Self::init()] before this may be called. - // - // TODO: Remove this unused attribute once we implement audio thread extensions #[allow(unused)] pub fn get_extension>(&'a self) -> Option { self.status().assert_is_not(PluginStatus::Uninitialized); @@ -82,18 +88,57 @@ impl<'a> PluginAudioThread<'a> { }; if !extension_ptr.is_null() { - return unsafe { - Some(T::new( - self, - NonNull::new(extension_ptr as *mut T::Struct).unwrap(), - )) - }; + return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; } } None } + /// Dispatch a task to be executed on the main thread. This is a blocking call that will wait + /// for the task to complete and return its result. + pub fn send_main_thread T + Send, T: Send>(&self, callback: F) -> T { + struct Scope<'a, F, O> { + condvar: &'a Condvar, + output: &'a Mutex>, + callback: F, + } + + let output = Mutex::new(None); + let condvar = Condvar::new(); + let scope = Scope { + condvar: &condvar, + output: &output, + callback, + }; + + let scope_ptr = unsafe { AssertSendSync::new(&scope as *const Scope as *const ()) }; + + self.post_main_thread(move |plugin| unsafe { + let scope = (scope_ptr.get() as *const Scope).read(); + let result = (scope.callback)(plugin); + scope.output.lock().unwrap().replace(result); + scope.condvar.notify_one(); + }); + + scope + .condvar + .wait_while(scope.output.lock().unwrap(), |v| v.is_none()) + .unwrap() + .take() + .unwrap() + } + + /// Post a task to be executed on the main thread. This is a non-blocking call that does not + /// wait for the task to complete and does not return a result. + pub fn post_main_thread(&self, task: impl FnOnce(&Plugin) + Send + 'static) { + self.plugin + .shared + .task_sender + .send(MainThreadTask::Closure(Box::new(task))) + .unwrap(); + } + /// Prepare for audio processing. Returns an error if the plugin returned `false`. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. @@ -106,7 +151,7 @@ impl<'a> PluginAudioThread<'a> { }; if result { - self.plugin.state.status.store(PluginStatus::Processing); + self.plugin.shared.status.store(PluginStatus::Processing); Ok(()) } else { anyhow::bail!("'clap_plugin::start_processing()' returned false.") @@ -117,25 +162,24 @@ impl<'a> PluginAudioThread<'a> { /// status code, then this will return an error. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. - pub fn process(&self, process_data: &mut ProcessData) -> Result { + pub fn process(&self, process_data: &clap_process) -> Result { self.status().assert_is(PluginStatus::Processing); let plugin = self.as_ptr(); - let result = process_data.with_clap_process_data(|clap_process_data| unsafe { - clap_call! { plugin=>process(plugin, &clap_process_data) } - }); + let result = unsafe { + clap_call! { plugin=>process(plugin, process_data) } + }; match result { - CLAP_PROCESS_ERROR => anyhow::bail!( - "The plugin returned 'CLAP_PROCESS_ERROR' from 'clap_plugin::process()'." - ), + CLAP_PROCESS_ERROR => { + anyhow::bail!("The plugin returned 'CLAP_PROCESS_ERROR' from 'clap_plugin::process()'.") + } CLAP_PROCESS_CONTINUE => Ok(ProcessStatus::Continue), CLAP_PROCESS_CONTINUE_IF_NOT_QUIET => Ok(ProcessStatus::ContinueIfNotQuiet), CLAP_PROCESS_TAIL => Ok(ProcessStatus::Tail), CLAP_PROCESS_SLEEP => Ok(ProcessStatus::Sleep), result => anyhow::bail!( - "The plugin returned an unknown 'clap_process_status' value {result} from \ - 'clap_plugin::process()'." + "The plugin returned an unknown 'clap_process_status' value {result} from 'clap_plugin::process()'." ), } } @@ -161,6 +205,6 @@ impl<'a> PluginAudioThread<'a> { clap_call! { plugin=>stop_processing(plugin) } }; - self.plugin.state.status.store(PluginStatus::Activated); + self.plugin.shared.status.store(PluginStatus::Activated); } } diff --git a/src/plugin/instance/process.rs b/src/plugin/instance/process.rs deleted file mode 100644 index 1946a46..0000000 --- a/src/plugin/instance/process.rs +++ /dev/null @@ -1,983 +0,0 @@ -//! Data structures and functions surrounding audio processing. - -use crate::plugin::ext::audio_ports::AudioPortConfig; -use crate::plugin::instance::Plugin; -use crate::plugin::instance::audio_thread::PluginAudioThread; -use crate::util::check_null_ptr; -use anyhow::Result; -use clap_sys::audio_buffer::clap_audio_buffer; -use clap_sys::events::{ - CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_CHOKE, CLAP_EVENT_NOTE_END, - CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_MOD, - CLAP_EVENT_PARAM_VALUE, CLAP_EVENT_TRANSPORT, CLAP_TRANSPORT_HAS_BEATS_TIMELINE, - CLAP_TRANSPORT_HAS_SECONDS_TIMELINE, CLAP_TRANSPORT_HAS_TEMPO, - CLAP_TRANSPORT_HAS_TIME_SIGNATURE, CLAP_TRANSPORT_IS_PLAYING, clap_event_header, - clap_event_midi, clap_event_note, clap_event_note_expression, clap_event_param_mod, - clap_event_param_value, clap_event_transport, clap_input_events, clap_output_events, -}; -use clap_sys::fixedpoint::{CLAP_BEATTIME_FACTOR, CLAP_SECTIME_FACTOR}; -use clap_sys::process::clap_process; -use either::Either; -use parking_lot::Mutex; -use rand::Rng; -use rand_pcg::Pcg32; -use std::ffi::c_void; -use std::fmt::Debug; -use std::pin::Pin; -use std::ptr::null_mut; - -/// The input and output data for a call to `clap_plugin::process()`. -pub struct ProcessData<'a> { - /// The input and output audio buffers. - pub buffers: &'a mut AudioBuffers, - /// The input events. - pub input_events: Pin>, - /// The output events. - pub output_events: Pin>, - /// The length of the current block in samples. - pub block_size: u32, - - config: ProcessConfig, - /// The current transport information. This is populated when constructing this object, and the - /// transport can be advanced `N` samples using the - /// [`advance_transport()`][Self::advance_transport()] method. - transport_info: clap_event_transport, - /// The current sample position. This is used to recompute values in `transport_info`. - sample_pos: u32, - // TODO: Maybe do something with `steady_time` -} - -/// Control flow for the processing loop. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProcessControlFlow { - Continue, - Reset, - Exit, -} - -/// The general context information for a process call. -#[derive(Debug, Clone, Copy)] -pub struct ProcessConfig { - /// The current sample rate. - pub sample_rate: f64, - // The current tempo in beats per minute. - pub tempo: f64, - // The time signature's numerator. - pub time_sig_numerator: u16, - // The time signature's denominator. - pub time_sig_denominator: u16, -} - -/// Audio buffers for audio processing. These contain both input and output buffers, that can be either in-place -/// or out-of-place, single or double precision. -#[derive(Clone, Debug)] -pub struct AudioBuffers { - // These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be - // mutable because reborrwing them from here is the only way to modify them without - // reinitializing the pointers. - buffers: Vec, - - // These are point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a - // `*const *const f32` - _pointers: Vec>, - - clap_inputs: Vec, - clap_outputs: Vec, - - /// The number of samples for this buffer. This is consistent across all inner vectors. - num_samples: usize, -} - -#[derive(Clone)] -pub enum AudioBuffer { - Float32 { - input: Option, - output: Option, - data: Vec>, - }, - - #[allow(unused)] //TODO: use for future 64 bit processing tests - Float64 { - input: Option, - output: Option, - data: Vec>, - }, -} - -pub trait AudioBufferFill { - fn fill_input_f32(&mut self, bus: usize, channel: usize, slice: &mut [f32]); - fn fill_input_f64(&mut self, bus: usize, channel: usize, slice: &mut [f64]); - fn fill_output_f32(&mut self, bus: usize, channel: usize, slice: &mut [f32]) { - self.fill_input_f32(bus, channel, slice); - } - fn fill_output_f64(&mut self, bus: usize, channel: usize, slice: &mut [f64]) { - self.fill_input_f64(bus, channel, slice); - } - fn fill_inplace_f32(&mut self, input: usize, output: usize, channel: usize, slice: &mut [f32]) { - let _ = output; - self.fill_input_f32(input, channel, slice); - } - fn fill_inplace_f64(&mut self, input: usize, output: usize, channel: usize, slice: &mut [f64]) { - let _ = output; - self.fill_input_f64(input, channel, slice); - } -} - -// SAFETY: Sharing these pointers with other threads is safe as they refer to the borrowed input and -// output slices. The pointers thus cannot be invalidated. -unsafe impl Send for AudioBuffers {} -unsafe impl Sync for AudioBuffers {} - -/// An event queue that can be used as either an input queue or an output queue. This is always -/// allocated through a `Pin>` so the pointers are stable. The `VTable` type -/// argument should be either `clap_input_events` or `clap_output_events`. -#[derive(Debug)] -pub struct EventQueue { - vtable_input: clap_input_events, - vtable_output: clap_output_events, - /// The actual event queue. Since we're going for correctness over performance, this uses a very - /// suboptimal memory layout by just using an `enum` instead of doing fancy bit packing. - events: Mutex>, -} - -/// An event sent to or from the plugin. This uses an enum to make the implementation simple and -/// correct at the cost of more wasteful memory usage. -#[derive(Debug, Clone)] -#[repr(C, align(8))] -pub enum Event { - /// `CLAP_EVENT_NOTE_ON`, `CLAP_EVENT_NOTE_OFF`, `CLAP_EVENT_NOTE_CHOKE`, or `CLAP_EVENT_NOTE_END`. - Note(clap_event_note), - /// `CLAP_EVENT_NOTE_EXPRESSION`. - NoteExpression(clap_event_note_expression), - /// `CLAP_EVENT_MIDI`. - Midi(clap_event_midi), - /// `CLAP_EVENT_PARAM_VALUE`. - ParamValue(clap_event_param_value), - /// `CLAP_EVENT_PARAM_MOD`. - ParamMod(clap_event_param_mod), - /// An unhandled event type. This is only used when the plugin outputs an event we don't handle - /// or recognize. - Unknown(clap_event_header), -} - -impl Default for ProcessConfig { - fn default() -> Self { - Self { - sample_rate: 44_100.0, - tempo: 110.0, - time_sig_numerator: 4, - time_sig_denominator: 4, - } - } -} - -impl<'a> ProcessData<'a> { - /// Initialize the process data using the given audio buffers. The transport information will be - /// initialized at the start of the project, and it can be moved using the - /// [`advance_transport()`][Self::advance_transport()] method. - // - // TODO: More transport info options. Missing fields, loop regions, flags, etc. - pub fn new(buffers: &'a mut AudioBuffers, config: ProcessConfig) -> Self { - ProcessData { - input_events: EventQueue::new(), - output_events: EventQueue::new(), - block_size: buffers.len() as u32, - buffers, - - config, - transport_info: clap_event_transport { - header: clap_event_header { - size: std::mem::size_of::() as u32, - time: 0, - space_id: CLAP_CORE_EVENT_SPACE_ID, - type_: CLAP_EVENT_TRANSPORT, - flags: 0, - }, - flags: CLAP_TRANSPORT_HAS_TEMPO - | CLAP_TRANSPORT_HAS_BEATS_TIMELINE - | CLAP_TRANSPORT_HAS_SECONDS_TIMELINE - | CLAP_TRANSPORT_HAS_TIME_SIGNATURE - | CLAP_TRANSPORT_IS_PLAYING, - song_pos_beats: 0, - song_pos_seconds: 0, - tempo: config.tempo, - tempo_inc: 0.0, - // These four currently aren't used - loop_start_beats: 0, - loop_end_beats: 0, - loop_start_seconds: 0, - loop_end_seconds: 0, - bar_start: 0, - bar_number: 0, - tsig_num: config.time_sig_numerator, - tsig_denom: config.time_sig_denominator, - }, - sample_pos: 0, - } - } - - /// Construct the CLAP process data, and evaluate a closure with it. The `clap_process_data` - /// contains raw pointers to this struct's data, so the closure is there to prevent dangling - /// pointers. - pub fn with_clap_process_data T>(&mut self, f: F) -> T { - assert!( - self.block_size as usize <= self.buffers.len(), - "Process block size is larger than the maximum allowed buffer size. This is a \ - clap-validator bug." - ); - - let (inputs, outputs) = self.buffers.clap_buffers(); - - let process_data = clap_process { - steady_time: self.sample_pos as i64, - frames_count: self.block_size, - transport: &self.transport_info, - audio_inputs: if inputs.is_empty() { - std::ptr::null() - } else { - inputs.as_ptr() - }, - audio_outputs: if outputs.is_empty() { - std::ptr::null_mut() - } else { - outputs.as_mut_ptr() - }, - audio_inputs_count: inputs.len() as u32, - audio_outputs_count: outputs.len() as u32, - in_events: self.input_events.vtable_input(), - out_events: self.output_events.vtable_output(), - }; - - f(process_data) - } - - /// Get current the transport information. - #[allow(unused)] - pub fn transport_info(&self) -> clap_event_transport { - self.transport_info - } - - /// Advance the transport by a certain number of samples. - pub fn advance_next(&mut self) { - self.input_events.clear(); - self.output_events.clear(); - - self.sample_pos += self.block_size; - self.transport_info.song_pos_beats = - ((self.sample_pos as f64 / self.config.sample_rate / 60.0 * self.transport_info.tempo) - * CLAP_BEATTIME_FACTOR as f64) - .round() as i64; - self.transport_info.song_pos_seconds = ((self.sample_pos as f64 / self.config.sample_rate) - * CLAP_SECTIME_FACTOR as f64) - .round() as i64; - } - - pub fn reset(&mut self) { - self.sample_pos = 0; - self.transport_info.song_pos_beats = 0; - self.transport_info.song_pos_seconds = 0; - self.input_events.clear(); - self.output_events.clear(); - } - - pub fn run(&mut self, plugin: &Plugin, mut process: Process) -> Result<()> - where - Process: FnMut(&PluginAudioThread, &mut Self) -> Result + Send, - { - let mut running = true; - while running { - plugin.activate(self.config.sample_rate, 1, self.buffers.len())?; - plugin.handle_callback()?; - - self.reset(); - - plugin.on_audio_thread(|plugin| -> Result<()> { - plugin.start_processing()?; - - // This test can be repeated a couple of times - // NOTE: We intentionally do not disable denormals here - 'processing: while running { - let flow = process(&plugin, self)?; - running &= flow != ProcessControlFlow::Exit; - self.advance_next(); - - // Restart processing as necessary - if plugin - .plugin - .state - .requested_restart - .compare_exchange(true, false) - .is_ok() - { - log::trace!( - "Restarting the plugin during processing cycle after a call to \ - 'clap_host::request_restart()'", - ); - break 'processing; - } - - if flow == ProcessControlFlow::Reset { - break 'processing; - } - } - - plugin.stop_processing(); - - Ok(()) - })?; - - plugin.deactivate(); - } - - // Handle callbacks the plugin may have made during deactivate - plugin.handle_callback()?; - - Ok(()) - } - - pub fn run_once(&mut self, plugin: &Plugin, process: Process) -> Result<()> - where - Process: FnOnce(&PluginAudioThread, &mut Self) -> Result<()> + Send, - { - let mut process = Some(process); - self.run(plugin, |plugin, instance| { - if let Some(process) = process.take() { - process(plugin, instance)?; - } - - Ok(ProcessControlFlow::Exit) - }) - } -} - -impl AudioBuffers { - /// Construct the audio buffers from the given buffer configurations. The number of samples must - /// be greater than zero and all channel vectors must have the same length. - pub fn new(buffers: Vec, num_samples: usize) -> Self { - assert!( - num_samples > 0, - "Number of samples must be greater than zero." - ); - - let mut pointers = vec![]; - let mut clap_inputs = vec![]; - let mut clap_outputs = vec![]; - - for buffer in buffers.iter() { - let pointer_list = match buffer { - AudioBuffer::Float32 { data, .. } => { - assert!( - data.iter().all(|x| x.len() == num_samples), - "Channel buffer length does not match" - ); - - data.iter() - .map(|x| x.as_ptr() as *const ()) - .collect::>() - } - AudioBuffer::Float64 { data, .. } => { - assert!( - data.iter().all(|x| x.len() == num_samples), - "Channel buffer length does not match" - ); - - data.iter() - .map(|x| x.as_ptr() as *const ()) - .collect::>() - } - }; - - if let Some(input) = buffer.input() { - if clap_inputs.len() <= input { - clap_inputs.resize(input + 1, None); - } - - clap_inputs[input] = Some(clap_audio_buffer { - data32: if buffer.is_64bit() { - null_mut() - } else { - pointer_list.as_ptr() as *mut *mut f32 - }, - - data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *mut *mut f64 - } else { - null_mut() - }, - - channel_count: pointer_list.len() as u32, - latency: 0, //TODO: do some interesting tests with these 2 fields - constant_mask: 0, - }); - } - - if let Some(output) = buffer.output() { - if clap_outputs.len() <= output { - clap_outputs.resize(output + 1, None); - } - - clap_outputs[output] = Some(clap_audio_buffer { - data32: if buffer.is_64bit() { - null_mut() - } else { - pointer_list.as_ptr() as *mut *mut f32 - }, - - data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *mut *mut f64 - } else { - null_mut() - }, - - channel_count: pointer_list.len() as u32, - latency: 0, //TODO: do some interesting tests with these 2 fields - constant_mask: 0, - }); - } - - pointers.push(pointer_list); - } - - Self { - buffers, - _pointers: pointers, - clap_inputs: clap_inputs - .into_iter() - .collect::>>() - .expect("Missing an input bus"), - clap_outputs: clap_outputs - .into_iter() - .collect::>>() - .expect("Missing an output bus"), - num_samples, - } - } - - /// Construct the out of place audio buffers. This allocates the channel pointers that are - /// handed to the plugin in the process function. - pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: usize) -> Self { - Self::new( - config - .inputs - .iter() - .enumerate() - .map(|(index, port)| { - AudioBuffer::new_out_of_place( - index, - true, - false, - port.num_channels as usize, - num_samples, - ) - }) - .chain(config.outputs.iter().enumerate().map(|(index, port)| { - AudioBuffer::new_out_of_place( - index, - false, - false, - port.num_channels as usize, - num_samples, - ) - })) - .collect(), - num_samples, - ) - } - - /// Construct the in place audio buffers. This allocates the channel pointers that are handed to - /// the plugin in the process function. - pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: usize) -> Self { - let mut buffers = vec![]; - - for (index, port) in config.inputs.iter().enumerate() { - let in_place = port - .in_place_pair_idx - .filter(|output| config.outputs[*output].num_channels == port.num_channels); - - if in_place.is_none() { - buffers.push(AudioBuffer::Float32 { - input: Some(index), - output: None, - data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], - }); - } - } - - for (index, port) in config.outputs.iter().enumerate() { - let in_place = port - .in_place_pair_idx - .filter(|input| config.inputs[*input].num_channels == port.num_channels); - - buffers.push(AudioBuffer::Float32 { - input: in_place, - output: Some(index), - data: vec![vec![0.0f32; num_samples]; port.num_channels as usize], - }); - } - - Self::new(buffers, num_samples) - } - - pub fn new_out_of_place_f64(config: &AudioPortConfig, num_samples: usize) -> Option { - if !config - .inputs - .iter() - .chain(config.outputs.iter()) - .any(|port| port.supports_double_sample_size) - { - return None; - } - - Some(Self::new( - config - .inputs - .iter() - .enumerate() - .map(|(index, port)| { - AudioBuffer::new_out_of_place( - index, - true, - port.supports_double_sample_size, - port.num_channels as usize, - num_samples, - ) - }) - .chain(config.outputs.iter().enumerate().map(|(index, port)| { - AudioBuffer::new_out_of_place( - index, - false, - port.supports_double_sample_size, - port.num_channels as usize, - num_samples, - ) - })) - .collect(), - num_samples, - )) - } - - /// The number of samples in the buffer. - pub fn len(&self) -> usize { - self.num_samples - } - - /// Pointers for the inputs and the outputs. These can be used to construct the `clap_process` - /// data. - pub fn clap_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { - (&self.clap_inputs, &mut self.clap_outputs) - } - - /// Pointers to the internal audio buffers - pub fn buffers(&self) -> &[AudioBuffer] { - &self.buffers - } - - /// Check whether the audio buffers are identical to another set of audio buffers. - pub fn is_same(&self, other: &Self) -> bool { - if self.buffers.len() != other.buffers.len() { - return false; - } - - for (this, other) in self.buffers.iter().zip(other.buffers.iter()) { - if !this.is_same(other) { - return false; - } - } - - true - } - - /// Fill the input and output buffers with arbitrary values. - pub fn fill(&mut self, mut fill: impl AudioBufferFill) { - for bus in &mut self.buffers { - match bus { - AudioBuffer::Float32 { - input, - output, - data, - } => { - for (channel_idx, channel) in data.iter_mut().enumerate() { - match (*input, *output) { - (Some(input), Some(output)) => { - fill.fill_inplace_f32(input, output, channel_idx, channel); - } - (Some(input), None) => { - fill.fill_input_f32(input, channel_idx, channel); - } - (None, Some(output)) => { - fill.fill_output_f32(output, channel_idx, channel); - } - (None, None) => {} - } - } - } - AudioBuffer::Float64 { - input, - output, - data, - } => { - for (channel_idx, channel) in data.iter_mut().enumerate() { - match (*input, *output) { - (Some(input), Some(output)) => { - fill.fill_inplace_f64(input, output, channel_idx, channel); - } - (Some(input), None) => { - fill.fill_input_f64(input, channel_idx, channel); - } - (None, Some(output)) => { - fill.fill_output_f64(output, channel_idx, channel); - } - (None, None) => {} - } - } - } - } - } - - for input in &mut self.clap_inputs { - input.constant_mask = 0; - } - - for output in &mut self.clap_outputs { - output.constant_mask = 0; - } - } - - /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). - /// Output buffers are filled with random NaN values to detect if they have been written to. - pub fn randomize(&mut self, prng: &mut Pcg32) { - struct Randomize<'a>(&'a mut Pcg32); - - impl AudioBufferFill for Randomize<'_> { - fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { - for sample in slice.iter_mut() { - let y = self.0.random_range(-1.0..=1.0f32); - *sample = if y.is_subnormal() { 0.0 } else { y }; - } - } - - fn fill_input_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { - for sample in slice.iter_mut() { - let y = self.0.random_range(-1.0..=1.0f64); - *sample = if y.is_subnormal() { 0.0 } else { y }; - } - } - - // fill with random NaN values so we can detect if a plugin left the output uninitialized - fn fill_output_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { - for sample in slice.iter_mut() { - let y: u32 = self.0.random(); - let y = f32::from_bits(y | 0x7F800001); - assert!(y.is_nan()); - *sample = y; - } - } - - fn fill_output_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { - for sample in slice.iter_mut() { - let y: u64 = self.0.random(); - let y = f64::from_bits(y | 0x7FF0000000000001); - assert!(y.is_nan()); - *sample = y; - } - } - } - - self.fill(Randomize(prng)); - } - - pub fn silence_all_inputs(&mut self) { - struct Silence; - - impl AudioBufferFill for Silence { - fn fill_input_f32(&mut self, _bus: usize, _channel: usize, slice: &mut [f32]) { - slice.fill(0.0); - } - - fn fill_input_f64(&mut self, _bus: usize, _channel: usize, slice: &mut [f64]) { - slice.fill(0.0); - } - - fn fill_output_f32(&mut self, _bus: usize, _channel: usize, _slice: &mut [f32]) {} - fn fill_output_f64(&mut self, _bus: usize, _channel: usize, _slice: &mut [f64]) {} - } - - self.fill(Silence); - - for input in &mut self.clap_inputs { - input.constant_mask = 1u64.unbounded_shl(input.channel_count).wrapping_sub(1); - } - } - - pub fn output_constant_mask(&self, bus: usize) -> u64 { - self.clap_outputs[bus].constant_mask - } -} - -impl AudioBuffer { - pub fn new_out_of_place( - port_index: usize, - is_input: bool, - is_double: bool, - num_channels: usize, - num_samples: usize, - ) -> Self { - let input = is_input.then_some(port_index); - let output = (!is_input).then_some(port_index); - - if is_double { - AudioBuffer::Float64 { - input, - output, - data: vec![vec![0.0f64; num_samples]; num_channels], - } - } else { - AudioBuffer::Float32 { - input, - output, - data: vec![vec![0.0f32; num_samples]; num_channels], - } - } - } - - /// Get the index of the input bus for this buffer. - pub fn input(&self) -> Option { - match self { - AudioBuffer::Float32 { input, .. } => *input, - AudioBuffer::Float64 { input, .. } => *input, - } - } - - /// Get the index of the output bus for this buffer. - pub fn output(&self) -> Option { - match self { - AudioBuffer::Float32 { output, .. } => *output, - AudioBuffer::Float64 { output, .. } => *output, - } - } - - /// Check whether this is a double precision buffer. - pub fn is_64bit(&self) -> bool { - match self { - AudioBuffer::Float32 { .. } => false, - AudioBuffer::Float64 { .. } => true, - } - } - - pub fn is_same(&self, other: &Self) -> bool { - match (self, other) { - (AudioBuffer::Float32 { data: this, .. }, AudioBuffer::Float32 { data: other, .. }) => { - for (this, other) in this.iter().zip(other.iter()) { - for (this, other) in this.iter().zip(other.iter()) { - if this.to_bits() != other.to_bits() { - return false; - } - } - } - - true - } - - (AudioBuffer::Float64 { data: this, .. }, AudioBuffer::Float64 { data: other, .. }) => { - for (this, other) in this.iter().zip(other.iter()) { - for (this, other) in this.iter().zip(other.iter()) { - if this.to_bits() != other.to_bits() { - return false; - } - } - } - - true - } - - _ => false, - } - } - - pub fn len(&self) -> usize { - match self { - AudioBuffer::Float32 { data, .. } => data.first().map_or(0, |x| x.len()), - AudioBuffer::Float64 { data, .. } => data.first().map_or(0, |x| x.len()), - } - } - - pub fn channels(&self) -> usize { - match self { - AudioBuffer::Float32 { data, .. } => data.len(), - AudioBuffer::Float64 { data, .. } => data.len(), - } - } - - pub fn get(&self, channel: usize, sample: usize) -> Either { - match self { - AudioBuffer::Float32 { data, .. } => Either::Right(data[channel][sample]), - AudioBuffer::Float64 { data, .. } => Either::Left(data[channel][sample]), - } - } -} - -impl Debug for AudioBuffer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Float32 { input, output, .. } => f - .debug_struct("Float32") - .field("input", input) - .field("output", output) - .finish_non_exhaustive(), - Self::Float64 { input, output, .. } => f - .debug_struct("Float64") - .field("input", input) - .field("output", output) - .finish_non_exhaustive(), - } - } -} - -impl EventQueue { - /// Construct a new event queue. This can be used as both an input and an output queue. - pub fn new() -> Pin> { - let mut queue = Box::pin(Self { - vtable_input: clap_input_events { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - size: Some(Self::size), - get: Some(Self::get), - }, - - vtable_output: clap_output_events { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - try_push: Some(Self::try_push), - }, - - // Using a mutex here is obviously a terrible idea in a real host, but we're not a real - // host - events: Mutex::new(Vec::new()), - }); - - queue.vtable_input.ctx = &*queue as *const Self as *mut c_void; - queue.vtable_output.ctx = &*queue as *const Self as *mut c_void; - queue - } - - pub fn clear(&self) { - self.events.lock().clear(); - } - - pub fn add_events(&self, extend: impl IntoIterator) { - let mut events = self.events.lock(); - let should_sort = !events.is_empty(); - events.extend(extend); - if should_sort { - events.sort_by_key(|event| event.header().time); - } - } - - pub fn read(&self) -> Vec { - self.events.lock().clone() - } - - /// Get the vtable pointer for input events. - pub fn vtable_input(self: &Pin>) -> *const clap_input_events { - &self.vtable_input - } - - /// Get the vtable pointer for output events. - pub fn vtable_output(self: &Pin>) -> *const clap_output_events { - &self.vtable_output - } - - unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { - unsafe { - check_null_ptr!(list, (*list).ctx); - let this = &*((*list).ctx as *const Self); - this.events.lock().len() as u32 - } - } - - unsafe extern "C" fn get( - list: *const clap_input_events, - index: u32, - ) -> *const clap_event_header { - unsafe { - check_null_ptr!(list, (*list).ctx); - let this = &*((*list).ctx as *const Self); - - let events = this.events.lock(); - match events.get(index as usize) { - Some(event) => event.header(), - None => { - log::warn!( - "The plugin tried to get an event with index {index} ({} total events)", - events.len() - ); - std::ptr::null() - } - } - } - } - - unsafe extern "C" fn try_push( - list: *const clap_output_events, - event: *const clap_event_header, - ) -> bool { - unsafe { - check_null_ptr!(list, (*list).ctx, event); - let this = &*((*list).ctx as *const Self); - - // The monotonicity of the plugin's event insertion order is checked as part of the output - // consistency checks - this.events - .lock() - .push(Event::from_header_ptr(event).unwrap()); - - true - } - } -} - -impl Event { - /// Parse an event from a plugin-provided pointer. Returns an error if the pointer as a null pointer - pub unsafe fn from_header_ptr(ptr: *const clap_event_header) -> Result { - if ptr.is_null() { - anyhow::bail!("Null pointer provided for 'clap_event_header'."); - } - - unsafe { - match ((*ptr).space_id, ((*ptr).type_)) { - ( - CLAP_CORE_EVENT_SPACE_ID, - CLAP_EVENT_NOTE_ON - | CLAP_EVENT_NOTE_OFF - | CLAP_EVENT_NOTE_CHOKE - | CLAP_EVENT_NOTE_END, - ) => Ok(Event::Note(*(ptr as *const clap_event_note))), - (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_EXPRESSION) => Ok( - Event::NoteExpression(*(ptr as *const clap_event_note_expression)), - ), - (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_VALUE) => { - Ok(Event::ParamValue(*(ptr as *const clap_event_param_value))) - } - (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_MOD) => { - Ok(Event::ParamMod(*(ptr as *const clap_event_param_mod))) - } - (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI) => { - Ok(Event::Midi(*(ptr as *const clap_event_midi))) - } - (_, _) => Ok(Event::Unknown(*ptr)), - } - } - } - - /// Get a a reference to the event's header. - pub fn header(&self) -> &clap_event_header { - match self { - Event::Note(event) => &event.header, - Event::NoteExpression(event) => &event.header, - Event::ParamValue(event) => &event.header, - Event::ParamMod(event) => &event.header, - Event::Midi(event) => &event.header, - Event::Unknown(header) => header, - } - } -} diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 1a90da3..df88caf 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -6,14 +6,13 @@ use crate::util::{self, clap_call}; use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory}; -use clap_sys::factory::preset_discovery::{ - CLAP_PRESET_DISCOVERY_FACTORY_ID, clap_preset_discovery_factory, -}; +use clap_sys::factory::preset_discovery::{CLAP_PRESET_DISCOVERY_FACTORY_ID, clap_preset_discovery_factory}; use clap_sys::plugin::clap_plugin_descriptor; use clap_sys::version::clap_version; use serde::Serialize; use std::collections::HashSet; use std::ffi::CString; +use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::ptr::NonNull; @@ -25,8 +24,12 @@ pub struct PluginLibrary { /// contained within the bundle. plugin_path: PathBuf, /// The plugin's library. Its entry point has already been initialized, and it will - /// autoamtically be deinitialized when this object gets dropped. + /// automatically be deinitialized when this object gets dropped. library: libloading::Library, + + /// To honor CLAP's thread safety guidelines, the thread this object was created from is + /// designated the 'main thread', and this object cannot be shared with other threads. + _thread: PhantomData<*const ()>, } /// Metadata for a CLAP plugin library, which may contain multiple plugins. @@ -79,8 +82,8 @@ impl Drop for PluginLibrary { fn drop(&mut self) { // The `Plugin` only exists if `init()` returned true, so we ned to deinitialize the // plugin here - let entry_point = get_clap_entry_point(&self.library) - .expect("A Plugin was constructed for a plugin with no entry point"); + let entry_point = + get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); unsafe { clap_call! { entry_point=>deinit() }; @@ -91,18 +94,29 @@ impl Drop for PluginLibrary { impl PluginLibrary { /// Load a CLAP plugin from a path to a `.clap` file or bundle. This will return an error if the /// plugin could not be loaded. + /// + /// This MUST be called on the OS main thread (if applicable). pub fn load(path: impl AsRef) -> Result { - Self::load_with(path, |path| { - unsafe { libloading::Library::new(path) }.context("Could not load the plugin library") - }) + unsafe { + Self::load_with(path, |path| { + libloading::Library::new(path).context("Could not load the plugin library") + }) + } } /// The same as [`load()`][`Self::load()`], but with a custom library loading function. Useful /// for testing different `dlopen()` options. + /// + /// This MUST be called on the OS main thread (if applicable). pub fn load_with( path: impl AsRef, load: impl FnOnce(&Path) -> Result, ) -> Result { + // assert!( + // IS_OS_MAIN_THREAD.with(|cell| cell.get()), + // "PluginLibrary must be loaded from the OS main thread" + // ); + // NOTE: We'll always make sure `path` is either relative to the current directory or // absolute. Otherwise the system libraries may be searched instead which would lead // to unexpected behavior. Joining an absolute path to a relative directory gets you @@ -113,12 +127,8 @@ impl PluginLibrary { // This is the path passed to `clap_entry::init()`. On macOS this should point to the // bundle, not the DSO. - let path_cstring = CString::new( - path.as_os_str() - .to_str() - .context("Path contains invalid UTF-8")?, - ) - .context("Path contains null bytes")?; + let path_cstring = CString::new(path.as_os_str().to_str().context("Path contains invalid UTF-8")?) + .context("Path contains null bytes")?; // NOTE: Apple says you can dlopen() bundles. This is a lie. #[cfg(not(target_os = "macos"))] @@ -128,9 +138,8 @@ impl PluginLibrary { use core_foundation::bundle::CFBundle; use core_foundation::url::CFURL; - let bundle = - CFBundle::new(CFURL::from_path(&path, true).context("Could not create CFURL")?) - .context("Could not open bundle")?; + let bundle = CFBundle::new(CFURL::from_path(&path, true).context("Could not create CFURL")?) + .context("Could not open bundle")?; let executable = bundle .executable_url() .context("Could not get executable URL within bundle")?; @@ -156,6 +165,7 @@ impl PluginLibrary { Ok(PluginLibrary { plugin_path: path, library, + _thread: PhantomData, }) } @@ -166,8 +176,8 @@ impl PluginLibrary { /// Get the metadata for all plugins stored in this plugin library. Most plugin libraries /// contain a single plugin, but this may return metadata for zero or more plugins. pub fn metadata(&self) -> Result { - let entry_point = get_clap_entry_point(&self.library) - .expect("A Plugin was constructed for a plugin with no entry point"); + let entry_point = + get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); let plugin_factory = unsafe { clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } } as *const clap_plugin_factory; @@ -196,8 +206,8 @@ impl PluginLibrary { if descriptor.is_null() { anyhow::bail!( - "The plugin returned a null plugin descriptor for plugin index {i} (expected \ - {num_plugins} total plugins)." + "The plugin returned a null plugin descriptor for plugin index {i} (expected {num_plugins} total \ + plugins)." ); } @@ -223,11 +233,10 @@ impl PluginLibrary { /// assert that querying a factory with a non-existent ID returns a null pointer instead of /// always returning the plugin factory. pub fn factory_exists(&self, factory_id: &str) -> bool { - let factory_id_cstring = - CString::new(factory_id).expect("The factory ID contained internal null bytes"); + let factory_id_cstring = CString::new(factory_id).expect("The factory ID contained internal null bytes"); - let entry_point = get_clap_entry_point(&self.library) - .expect("A Plugin was constructed for a plugin with no entry point"); + let entry_point = + get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); let factory_pointer = unsafe { clap_call! { entry_point=>get_factory(factory_id_cstring.as_ptr()) } }; @@ -240,8 +249,8 @@ impl PluginLibrary { /// [`metadata()`][Self::metadata()]. The returned plugin has not yet been initialized, and /// `destroy()` will be called automatically when the object is dropped. pub fn create_plugin(&self, id: &str) -> Result> { - let entry_point = get_clap_entry_point(&self.library) - .expect("A Plugin was constructed for a plugin with no entry point"); + let entry_point = + get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); let plugin_factory = unsafe { clap_call! { entry_point=>get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) } @@ -255,13 +264,13 @@ impl PluginLibrary { } let id_cstring = CString::new(id).context("Plugin ID contained null bytes")?; - Plugin::new(self, unsafe { &*plugin_factory }, &id_cstring) + unsafe { Plugin::new(&*plugin_factory, &id_cstring) } } /// Returns the plugin's preset discovery factory, if it has one. pub fn preset_discovery_factory(&self) -> Result> { - let entry_point = get_clap_entry_point(&self.library) - .expect("A Plugin was constructed for a plugin with no entry point"); + let entry_point = + get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); let preset_discovery_factory = unsafe { clap_call! { entry_point=>get_factory(CLAP_PRESET_DISCOVERY_FACTORY_ID.as_ptr()) @@ -269,9 +278,7 @@ impl PluginLibrary { } as *mut clap_preset_discovery_factory; match NonNull::new(preset_discovery_factory) { - Some(preset_discovery_factory) => { - Ok(PresetDiscoveryFactory::new(self, preset_discovery_factory)) - } + Some(preset_discovery_factory) => Ok(PresetDiscoveryFactory::new(self, preset_discovery_factory)), None => { anyhow::bail!( "The plugin does not support the '{}' factory.", @@ -296,11 +303,20 @@ impl PluginLibraryMetadata { /// Get a plugin's entry point. fn get_clap_entry_point(library: &libloading::Library) -> Result<&clap_plugin_entry> { let entry_point: libloading::Symbol<*const clap_plugin_entry> = - unsafe { library.get(b"clap_entry") } - .context("The library does not expose a 'clap_entry' symbol")?; + unsafe { library.get(b"clap_entry") }.context("The library does not expose a 'clap_entry' symbol")?; if entry_point.is_null() { anyhow::bail!("'clap_entry' is a null pointer."); } Ok(unsafe { &**entry_point }) } + +thread_local! { + static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +pub(crate) fn mark_current_thread_as_os_main_thread() { + IS_OS_MAIN_THREAD.with(|cell| { + cell.set(true); + }); +} diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 9298fa1..335df6d 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -1,16 +1,9 @@ //! The indexer abstraction for a CLAP plugin's preset discovery factory. During initialization the //! plugin fills this object with its supported locations, file types, and sound packs. +use crate::util::{self, check_null_ptr, validator_version}; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; -use serde::Serialize; -use std::cell::RefCell; -use std::ffi::{CStr, CString, c_char, c_void}; -use std::fmt::Display; -use std::path::Path; -use std::pin::Pin; -use std::thread::ThreadId; - use clap_sys::factory::preset_discovery::{ CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, @@ -19,9 +12,13 @@ use clap_sys::factory::preset_discovery::{ clap_preset_discovery_location_kind, clap_preset_discovery_soundpack, }; use clap_sys::version::CLAP_VERSION; -use parking_lot::Mutex; - -use crate::util::{self, check_null_ptr}; +use serde::Serialize; +use std::cell::RefCell; +use std::ffi::{CStr, CString, c_char, c_void}; +use std::fmt::Display; +use std::path::Path; +use std::pin::Pin; +use std::thread::ThreadId; #[derive(Debug)] pub struct Indexer { @@ -36,11 +33,9 @@ pub struct Indexer { /// The data written to this object by the plugin. results: RefCell, - /// The validator's version, reported in the `clap_preset_discovery_indexer` struct. - _clap_validator_version: CString, /// The vtable that's passed to the provider. The `indexer_data` field is populated with a /// pointer to this object. - clap_preset_discovery_indexer: Mutex, + clap_preset_discovery_indexer: clap_preset_discovery_indexer, } /// The data written to the indexer by the plugin during the @@ -368,31 +363,28 @@ impl Drop for Indexer { impl Indexer { pub fn new() -> Pin> { - let clap_validator_version = - CString::new(env!("CARGO_PKG_VERSION")).expect("Invalid bytes in crate version"); - let indexer = Box::pin(Self { + let mut indexer = Box::pin(Self { expected_thread_id: std::thread::current().id(), callback_error: RefCell::new(None), results: RefCell::default(), - clap_preset_discovery_indexer: Mutex::new(clap_preset_discovery_indexer { + clap_preset_discovery_indexer: clap_preset_discovery_indexer { clap_version: CLAP_VERSION, name: c"clap-validator".as_ptr(), vendor: c"Robbert van der Helm".as_ptr(), url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: clap_validator_version.as_ptr(), + version: validator_version().as_ptr(), // This is filled with a pointer to this struct after the `Box` has been allocated indexer_data: std::ptr::null_mut(), declare_filetype: Some(Self::declare_filetype), declare_location: Some(Self::declare_location), declare_soundpack: Some(Self::declare_soundpack), get_extension: Some(Self::get_extension), - }), - _clap_validator_version: clap_validator_version, + }, }); - indexer.clap_preset_discovery_indexer.lock().indexer_data = + indexer.clap_preset_discovery_indexer.indexer_data = &*indexer as *const Self as *mut c_void; indexer @@ -403,7 +395,7 @@ impl Indexer { pub fn clap_preset_discovery_indexer_ptr( self: &Pin>, ) -> *const clap_preset_discovery_indexer { - self.clap_preset_discovery_indexer.data_ptr() + &self.clap_preset_discovery_indexer } /// Get the values written to this indexer by the plugin during the diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index b74c33c..28466c0 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -2,6 +2,8 @@ //! querying metadata for a plugin's file. This is sort of like a state machine the plugin writes //! one or more presets to. +use super::{Flags, LocationValue}; +use crate::util::{self, check_null_ptr}; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use clap_sys::factory::preset_discovery::{ @@ -11,7 +13,6 @@ use clap_sys::factory::preset_discovery::{ }; use clap_sys::timestamp::clap_timestamp; use clap_sys::universal_plugin_id::clap_universal_plugin_id; -use parking_lot::Mutex; use serde::Serialize; use std::cell::RefCell; use std::collections::BTreeMap; @@ -20,9 +21,6 @@ use std::fmt::Display; use std::pin::Pin; use std::thread::ThreadId; -use super::{Flags, LocationValue}; -use crate::util::{self, check_null_ptr}; - /// An implementation of the preset discovery's metadata receiver. This borrows a /// `Result` because the important work is done when this object is dropped. When this /// object is dropped, that result will contain either an error, a single preset, or a container of @@ -73,7 +71,7 @@ pub struct MetadataReceiver<'a> { /// The vtable that's passed to the provider. The `receiver_data` field is populated with a /// pointer to this object. - clap_preset_discovery_metadata_receiver: Mutex, + clap_preset_discovery_metadata_receiver: clap_preset_discovery_metadata_receiver, } /// One or more presets declared by the plugin through a preset provider metadata receiver. @@ -276,7 +274,7 @@ impl<'a> MetadataReceiver<'a> { // written to it in the `Drop` implementation *result = None; - let metadata_receiver = Box::pin(Self { + let mut metadata_receiver = Box::pin(Self { expected_thread_id: std::thread::current().id(), location, @@ -285,27 +283,24 @@ impl<'a> MetadataReceiver<'a> { next_preset_data: RefCell::new(None), next_load_key: RefCell::new(None), - clap_preset_discovery_metadata_receiver: Mutex::new( - clap_preset_discovery_metadata_receiver { - // This is set to a pointer to this pinned data structure later - receiver_data: std::ptr::null_mut(), - on_error: Some(Self::on_error), - begin_preset: Some(Self::begin_preset), - add_plugin_id: Some(Self::add_plugin_id), - set_soundpack_id: Some(Self::set_soundpack_id), - set_flags: Some(Self::set_flags), - add_creator: Some(Self::add_creator), - set_description: Some(Self::set_description), - set_timestamps: Some(Self::set_timestamps), - add_feature: Some(Self::add_feature), - add_extra_info: Some(Self::add_extra_info), - }, - ), + clap_preset_discovery_metadata_receiver: clap_preset_discovery_metadata_receiver { + // This is set to a pointer to this pinned data structure later + receiver_data: std::ptr::null_mut(), + on_error: Some(Self::on_error), + begin_preset: Some(Self::begin_preset), + add_plugin_id: Some(Self::add_plugin_id), + set_soundpack_id: Some(Self::set_soundpack_id), + set_flags: Some(Self::set_flags), + add_creator: Some(Self::add_creator), + set_description: Some(Self::set_description), + set_timestamps: Some(Self::set_timestamps), + add_feature: Some(Self::add_feature), + add_extra_info: Some(Self::add_extra_info), + }, }); metadata_receiver .clap_preset_discovery_metadata_receiver - .lock() .receiver_data = &*metadata_receiver as *const Self as *mut c_void; metadata_receiver @@ -316,7 +311,7 @@ impl<'a> MetadataReceiver<'a> { pub fn clap_preset_discovery_metadata_receiver_ptr( self: &Pin>, ) -> *const clap_preset_discovery_metadata_receiver { - self.clap_preset_discovery_metadata_receiver.data_ptr() + &self.clap_preset_discovery_metadata_receiver } /// Checks that this function is called from the same thread the indexer was created on. If it diff --git a/src/plugin/process.rs b/src/plugin/process.rs new file mode 100644 index 0000000..1538c86 --- /dev/null +++ b/src/plugin/process.rs @@ -0,0 +1,251 @@ +//! Data structures and functions surrounding audio processing. +use crate::plugin::instance::{PluginAudioThread, PluginStatus}; +use anyhow::Result; +use clap_sys::process::*; +use std::pin::Pin; + +mod buffer; +mod events; +mod transport; + +pub use buffer::*; +pub use events::*; +pub use transport::*; + +pub struct ProcessScope<'a> { + plugin: &'a PluginAudioThread<'a>, + buffer: &'a mut AudioBuffers, + + events_input: Pin>, + events_output: Pin>, + + transport: TransportState, + sample_rate: f64, +} + +impl<'a> ProcessScope<'a> { + pub fn new(plugin: &'a PluginAudioThread, buffer: &'a mut AudioBuffers) -> Result { + Self::with_sample_rate(plugin, buffer, 44100.0) + } + + pub fn with_sample_rate( + plugin: &'a PluginAudioThread, + buffer: &'a mut AudioBuffers, + sample_rate: f64, + ) -> Result { + plugin.status().assert_is(PluginStatus::Deactivated); + + Ok(ProcessScope { + plugin, + buffer, + + events_input: EventQueue::new(), + events_output: EventQueue::new(), + transport: TransportState::default(), + sample_rate, + }) + } + + pub fn max_block_size(&self) -> u32 { + self.buffer.len() + } + + pub fn sample_rate(&self) -> f64 { + self.sample_rate + } + + pub fn input_queue(&self) -> &EventQueue { + &self.events_input + } + + pub fn output_queue(&self) -> &EventQueue { + &self.events_output + } + + pub fn transport(&mut self) -> &mut TransportState { + &mut self.transport + } + + pub fn audio_buffers(&mut self) -> &mut AudioBuffers { + self.buffer + } + + pub fn reset(&mut self) { + if self.plugin.status() >= PluginStatus::Activated { + self.plugin.reset(); + } + } + + pub fn run(&mut self) -> Result<()> { + self.run_with_block_size(self.buffer.len()) + } + + pub fn run_with_block_size(&mut self, samples: u32) -> Result<()> { + assert!(samples > 0 && samples <= self.buffer.len()); + + // check for requested restart + if self.plugin.shared().requested_restart.load() { + self.restart(); + } + + // check state, activate if needed + if self.plugin.status() == PluginStatus::Deactivated { + self.plugin.shared().requested_restart.store(false); + self.plugin + .send_main_thread(|plugin| plugin.activate(self.sample_rate, 1, self.buffer.len()))?; + } + + // start processing if needed + if self.plugin.status() == PluginStatus::Activated { + self.plugin.start_processing()?; + } + + // prepare output event queue for processing + self.events_output.clear(); + + // prepare output audio buffers for processing + // this is used to detect uninitialized output buffers + for buffer in self.buffer.buffers_mut() { + if buffer.is_output_only() { + buffer.fill(CHECK_NAN_F32, CHECK_NAN_F64); + } + } + + // save original buffers for consistency check + let original_buffers = self.buffer.buffers().to_owned(); + + // run processing + let transport = self.transport.as_clap_transport(0); + let (inputs, outputs) = self.buffer.clap_buffers(); + self.plugin.process(&clap_process { + steady_time: self.transport.sample_pos, + frames_count: samples, + transport: &transport, + audio_inputs: inputs.as_ptr(), + audio_outputs: outputs.as_mut_ptr(), + audio_inputs_count: inputs.len() as u32, + audio_outputs_count: outputs.len() as u32, + in_events: self.events_input.vtable_input(), + out_events: self.events_output.vtable_output(), + })?; + + // clear input event queue and advance transport + self.events_input.clear(); + self.transport.advance(samples, self.sample_rate); + + // check output audio buffers for NaNs or infinities + check_process_call_consistency(self.buffer.buffers(), &original_buffers, &self.events_output, samples) + } + + pub fn restart(&mut self) { + if self.plugin.status() == PluginStatus::Processing { + self.plugin.stop_processing(); + } + + if self.plugin.status() == PluginStatus::Activated { + self.plugin.send_main_thread(|plugin| { + plugin.deactivate(); + }); + } + } +} + +impl Drop for ProcessScope<'_> { + fn drop(&mut self) { + self.restart(); + } +} + +/// NaN values used for checking if output buffers have been written to. +/// These are quiet NaNs with a specific payload to avoid accidental matches with other NaN values. +/// The payload is chosen to be unlikely to appear in normal processing. +const CHECK_NAN_F32: f32 = f32::from_bits(0x7FC0_1234); +const CHECK_NAN_F64: f64 = f64::from_bits(0x7FF8_1234_5678_1234); + +/// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, +/// infinite, or denormal values, that the input buffers have not been modified by the plugin, and +/// that the output event queue is monotonically ordered. +fn check_process_call_consistency( + resulting_buffers: &[AudioBuffer], + original_buffers: &[AudioBuffer], + output_events: &EventQueue, + block_size: u32, +) -> Result<()> { + for (buffer, before) in resulting_buffers.iter().zip(original_buffers.iter()) { + // Input-only buffers must not be overwritten during out of place processing + match buffer.port() { + AudioBufferPort::Input(index) => { + if !buffer.is_same(before) { + anyhow::bail!( + "The plugin has overwritten an input buffer (index {index}) during out-of-place processing." + ); + } + } + + // Output buffers must not contain any non-finite or denormal values + AudioBufferPort::Output(port_idx) | AudioBufferPort::Inplace(_, port_idx) => { + let maybe_non_finite = (0..buffer.channels()) + .flat_map(|channel| (0..block_size).map(move |sample| (channel, sample))) + .find_map(|(channel, sample)| { + let x = buffer.get(channel, sample); + if x.either( + |x| !x.is_finite() || x.is_subnormal(), + |x| !x.is_finite() || x.is_subnormal(), + ) { + Some((x, channel, sample)) + } else { + None + } + }); + + if let Some((sample, channel_idx, sample_idx)) = maybe_non_finite { + let is_subnormal = sample.either(|x| x.is_subnormal(), |x| x.is_subnormal()); + let is_unwritten = sample.either( + |x| x.to_bits() == CHECK_NAN_F64.to_bits(), + |x| x.to_bits() == CHECK_NAN_F32.to_bits(), + ); + + if is_subnormal { + anyhow::bail!( + "The sample written to output port {port_idx}, channel {channel_idx}, and sample index \ + {sample_idx} is subnormal ({sample})." + ); + } else if is_unwritten { + anyhow::bail!( + "The sample at output port {port_idx}, channel {channel_idx}, and sample index \ + {sample_idx} was left unwritten." + ); + } else { + anyhow::bail!( + "The sample written to output port {port_idx}, channel {channel_idx}, and sample index \ + {sample_idx} is {sample}." + ); + } + } + } + } + } + + // If the plugin output any events, then they should be in a monotonically increasing order + let mut last_event_time = 0; + for event in output_events.read() { + let event_time = event.header().time; + if event_time < last_event_time { + anyhow::bail!( + "The plugin output an event for sample {event_time} after it had previously output an event for \ + sample {last_event_time}." + ) + } + + if event_time >= block_size { + anyhow::bail!( + "The plugin output an event for sample {event_time} but the audio buffer only contains {block_size} \ + samples." + ) + } + + last_event_time = event_time; + } + + Ok(()) +} diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs new file mode 100644 index 0000000..bb0bf97 --- /dev/null +++ b/src/plugin/process/buffer.rs @@ -0,0 +1,497 @@ +use crate::plugin::{ext::audio_ports::AudioPortConfig, process::ConstantMask}; +use clap_sys::audio_buffer::*; +use either::Either; +use rand::Rng; +use rand_pcg::Pcg32; +use std::ptr::null_mut; + +/// Audio buffers for audio processing. These contain both input and output buffers, that can be either in-place +/// or out-of-place, single or double precision. +#[derive(Clone)] +pub struct AudioBuffers { + // These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be + // mutable because reborrwing them from here is the only way to modify them without + // reinitializing the pointers. + buffers: Box<[AudioBuffer]>, + + // These are point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a + // `*const *const f32` + _pointers: Box<[Box<[*const ()]>]>, + + clap_inputs: Box<[clap_audio_buffer]>, + clap_outputs: Box<[clap_audio_buffer]>, + + /// The number of samples for this buffer. This is consistent across all inner vectors. + num_samples: u32, +} + +/// A single audio buffer, either input, output or in-place. This can be either single or double precision. +#[derive(Clone)] +pub enum AudioBuffer { + Float32 { + port: AudioBufferPort, + data: Box<[Box<[f32]>]>, + }, + + Float64 { + port: AudioBufferPort, + data: Box<[Box<[f64]>]>, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum AudioBufferPort { + Input(usize), + Output(usize), + Inplace(usize, usize), +} + +// SAFETY: Sharing these pointers with other threads is safe as they refer to the borrowed input and +// output slices. The pointers thus cannot be invalidated. +unsafe impl Send for AudioBuffers {} +unsafe impl Sync for AudioBuffers {} + +impl AudioBuffers { + /// Construct the audio buffers from the given buffer configurations. The number of samples must + /// be greater than zero and all channel vectors must have the same length. + pub fn new(buffers: Vec, num_samples: u32) -> Self { + assert!(num_samples > 0, "Number of samples must be greater than zero."); + + let mut pointers = vec![]; + let mut clap_inputs = vec![]; + let mut clap_outputs = vec![]; + + for buffer in buffers.iter() { + let pointer_list = match buffer { + AudioBuffer::Float32 { data, .. } => { + assert!( + data.iter().all(|x| x.len() as u32 == num_samples), + "Channel buffer length does not match" + ); + + data.iter().map(|x| x.as_ptr() as *const ()).collect::>() + } + AudioBuffer::Float64 { data, .. } => { + assert!( + data.iter().all(|x| x.len() as u32 == num_samples), + "Channel buffer length does not match" + ); + + data.iter().map(|x| x.as_ptr() as *const ()).collect::>() + } + }; + + if let Some(input) = buffer.port().as_input() { + if clap_inputs.len() <= input { + clap_inputs.resize(input + 1, None); + } + + clap_inputs[input] = Some(clap_audio_buffer { + data32: if buffer.is_64bit() { + null_mut() + } else { + pointer_list.as_ptr() as *mut *mut f32 + }, + + data64: if buffer.is_64bit() { + pointer_list.as_ptr() as *mut *mut f64 + } else { + null_mut() + }, + + channel_count: pointer_list.len() as u32, + latency: 0, //TODO: do some interesting tests with these 2 fields + constant_mask: 0, + }); + } + + if let Some(output) = buffer.port().as_output() { + if clap_outputs.len() <= output { + clap_outputs.resize(output + 1, None); + } + + clap_outputs[output] = Some(clap_audio_buffer { + data32: if buffer.is_64bit() { + null_mut() + } else { + pointer_list.as_ptr() as *mut *mut f32 + }, + + data64: if buffer.is_64bit() { + pointer_list.as_ptr() as *mut *mut f64 + } else { + null_mut() + }, + + channel_count: pointer_list.len() as u32, + latency: 0, //TODO: do some interesting tests with these 2 fields + constant_mask: 0, + }); + } + + pointers.push(pointer_list.into_boxed_slice()); + } + + Self { + buffers: buffers.into_boxed_slice(), + _pointers: pointers.into_boxed_slice(), + clap_inputs: clap_inputs + .into_iter() + .collect::>>() + .expect("Missing an input bus") + .into_boxed_slice(), + clap_outputs: clap_outputs + .into_iter() + .collect::>>() + .expect("Missing an output bus") + .into_boxed_slice(), + num_samples, + } + } + + /// Construct the out of place audio buffers. This allocates the channel pointers that are + /// handed to the plugin in the process function. + pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { + Self::new( + config + .inputs + .iter() + .enumerate() + .map(|(index, port)| { + AudioBuffer::new(AudioBufferPort::Input(index), false, port.num_channels, num_samples) + }) + .chain(config.outputs.iter().enumerate().map(|(index, port)| { + AudioBuffer::new(AudioBufferPort::Output(index), false, port.num_channels, num_samples) + })) + .collect(), + num_samples, + ) + } + + /// Construct the in place audio buffers. This allocates the channel pointers that are handed to + /// the plugin in the process function. + pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { + let mut buffers = vec![]; + + for (index, port) in config.inputs.iter().enumerate() { + let in_place = port + .in_place_pair_idx + .filter(|output| config.outputs[*output].num_channels == port.num_channels); + + if in_place.is_none() { + buffers.push(AudioBuffer::new( + AudioBufferPort::Input(index), + false, + port.num_channels, + num_samples, + )); + } + } + + for (index, port) in config.outputs.iter().enumerate() { + let in_place = port + .in_place_pair_idx + .filter(|input| config.inputs[*input].num_channels == port.num_channels); + + buffers.push(AudioBuffer::new( + match in_place { + Some(input) => AudioBufferPort::Inplace(input, index), + None => AudioBufferPort::Output(index), + }, + false, + port.num_channels, + num_samples, + )); + } + + Self::new(buffers, num_samples) + } + + pub fn new_out_of_place_f64(config: &AudioPortConfig, num_samples: u32) -> Self { + Self::new( + config + .inputs + .iter() + .enumerate() + .map(|(index, port)| { + AudioBuffer::new( + AudioBufferPort::Input(index), + port.supports_double_sample_size, + port.num_channels, + num_samples, + ) + }) + .chain(config.outputs.iter().enumerate().map(|(index, port)| { + AudioBuffer::new( + AudioBufferPort::Output(index), + port.supports_double_sample_size, + port.num_channels, + num_samples, + ) + })) + .collect(), + num_samples, + ) + } + + /// Construct the in place audio buffers. This allocates the channel pointers that are handed to + /// the plugin in the process function. + pub fn new_in_place_f64(config: &AudioPortConfig, num_samples: u32) -> Self { + let mut buffers = vec![]; + + for (index, port) in config.inputs.iter().enumerate() { + let in_place = port + .in_place_pair_idx + .filter(|output| config.outputs[*output].num_channels == port.num_channels); + + if in_place.is_none() { + buffers.push(AudioBuffer::new( + AudioBufferPort::Input(index), + port.supports_double_sample_size, + port.num_channels, + num_samples, + )); + } + } + + for (index, port) in config.outputs.iter().enumerate() { + let in_place = port.in_place_pair_idx.filter(|input| { + let input = &config.inputs[*input]; + port.num_channels == input.num_channels + && port.supports_double_sample_size == input.supports_double_sample_size + }); + + buffers.push(AudioBuffer::new( + match in_place { + Some(input) => AudioBufferPort::Inplace(input, index), + None => AudioBufferPort::Output(index), + }, + port.supports_double_sample_size, + port.num_channels, + num_samples, + )); + } + + Self::new(buffers, num_samples) + } + + /// The number of samples in the buffer. + pub fn len(&self) -> u32 { + self.num_samples + } + + /// Pointers for the inputs and the outputs. These can be used to construct the `clap_process` + /// data. + pub fn clap_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { + (&self.clap_inputs, &mut self.clap_outputs) + } + + /// Pointers to the internal audio buffers + pub fn buffers(&self) -> &[AudioBuffer] { + &self.buffers + } + + /// Pointers to the internal audio buffers + pub fn buffers_mut(&mut self) -> &mut [AudioBuffer] { + &mut self.buffers + } + + /// Check whether the audio buffers are identical to another set of audio buffers. + pub fn is_same(&self, other: &Self) -> bool { + if self.buffers.len() != other.buffers.len() { + return false; + } + + for (this, other) in self.buffers.iter().zip(other.buffers.iter()) { + if !this.is_same(other) { + return false; + } + } + + true + } + + /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). + /// Output buffers are filled with random NaN values to detect if they have been written to. + pub fn randomize(&mut self, prng: &mut Pcg32) { + for buffer in self.buffers_mut() { + if buffer.is_input() { + buffer.fill_white_noise(prng); + } + } + } + + pub fn silence_inputs(&mut self) { + for buffer in self.buffers_mut() { + if buffer.is_input() { + buffer.fill(0.0, 0.0); + } + } + + for input in &mut self.clap_inputs { + input.constant_mask = u64::MAX; + } + } + + pub fn set_input_constant_mask(&mut self, bus: usize, mask: ConstantMask) { + self.clap_inputs[bus].constant_mask = mask.0; + } + + pub fn get_output_constant_mask(&self, bus: usize) -> ConstantMask { + ConstantMask(self.clap_outputs[bus].constant_mask) + } +} + +impl AudioBufferPort { + pub fn as_input(&self) -> Option { + match self { + AudioBufferPort::Input(index) => Some(*index), + AudioBufferPort::Inplace(index, _) => Some(*index), + AudioBufferPort::Output(_) => None, + } + } + + pub fn as_output(&self) -> Option { + match self { + AudioBufferPort::Output(index) => Some(*index), + AudioBufferPort::Inplace(_, index) => Some(*index), + AudioBufferPort::Input(_) => None, + } + } +} + +impl AudioBuffer { + pub fn new(port: AudioBufferPort, is_double_precision: bool, num_channels: u32, num_samples: u32) -> Self { + if is_double_precision { + AudioBuffer::Float64 { + port, + data: vec![vec![0.0f64; num_samples as usize].into_boxed_slice(); num_channels as usize] + .into_boxed_slice(), + } + } else { + AudioBuffer::Float32 { + port, + data: vec![vec![0.0f32; num_samples as usize].into_boxed_slice(); num_channels as usize] + .into_boxed_slice(), + } + } + } + + pub fn port(&self) -> AudioBufferPort { + match self { + AudioBuffer::Float32 { port, .. } => *port, + AudioBuffer::Float64 { port, .. } => *port, + } + } + + /// Check whether this is a double precision buffer. + pub fn is_64bit(&self) -> bool { + match self { + AudioBuffer::Float32 { .. } => false, + AudioBuffer::Float64 { .. } => true, + } + } + + pub fn is_input(&self) -> bool { + self.port().as_input().is_some() + } + + pub fn is_output_only(&self) -> bool { + self.port().as_output().is_some() && self.port().as_input().is_none() + } + + /// Check whether this audio buffer's contents are identical to another audio buffer. + pub fn is_same(&self, other: &Self) -> bool { + match (self, other) { + (AudioBuffer::Float32 { data: this, .. }, AudioBuffer::Float32 { data: other, .. }) => { + for (this, other) in this.iter().zip(other.iter()) { + for (this, other) in this.iter().zip(other.iter()) { + if this.to_bits() != other.to_bits() { + return false; + } + } + } + + true + } + + (AudioBuffer::Float64 { data: this, .. }, AudioBuffer::Float64 { data: other, .. }) => { + for (this, other) in this.iter().zip(other.iter()) { + for (this, other) in this.iter().zip(other.iter()) { + if this.to_bits() != other.to_bits() { + return false; + } + } + } + + true + } + + _ => false, + } + } + + /// The number of samples in this buffer. + pub fn len(&self) -> u32 { + match self { + AudioBuffer::Float32 { data, .. } => data.first().map_or(0, |x| x.len() as u32), + AudioBuffer::Float64 { data, .. } => data.first().map_or(0, |x| x.len() as u32), + } + } + + /// The number of channels in this buffer. + pub fn channels(&self) -> u32 { + match self { + AudioBuffer::Float32 { data, .. } => data.len() as u32, + AudioBuffer::Float64 { data, .. } => data.len() as u32, + } + } + + /// Get a sample from the buffer. + pub fn get(&self, channel: u32, sample: u32) -> Either { + match self { + AudioBuffer::Float32 { data, .. } => Either::Right(data[channel as usize][sample as usize]), + AudioBuffer::Float64 { data, .. } => Either::Left(data[channel as usize][sample as usize]), + } + } + + /// Fill the buffer with silence (zeros). + pub fn fill(&mut self, value_f32: f32, value_f64: f64) { + match self { + AudioBuffer::Float32 { data, .. } => { + for channel in data { + for sample in channel { + *sample = value_f32; + } + } + } + AudioBuffer::Float64 { data, .. } => { + for channel in data { + for sample in channel { + *sample = value_f64; + } + } + } + } + } + + /// Fill the buffer with white noise (random values in the range [-1, 1]). + pub fn fill_white_noise(&mut self, prng: &mut Pcg32) { + match self { + AudioBuffer::Float32 { data, .. } => { + for channel in data { + for sample in channel { + *sample = prng.random_range(-1.0..=1.0f32); + } + } + } + AudioBuffer::Float64 { data, .. } => { + for channel in data { + for sample in channel { + *sample = prng.random_range(-1.0..=1.0f64); + } + } + } + } + } +} diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs new file mode 100644 index 0000000..a8f1834 --- /dev/null +++ b/src/plugin/process/events.rs @@ -0,0 +1,190 @@ +use clap_sys::events::*; +use std::{pin::Pin, sync::Mutex}; + +use crate::util::check_null_ptr; + +/// An event queue that can be used as either an input queue or an output queue. This is always +/// allocated through a `Pin>` so the pointers are stable. The `VTable` type +/// argument should be either `clap_input_events` or `clap_output_events`. +#[derive(Debug)] +pub struct EventQueue { + vtable_input: clap_input_events, + vtable_output: clap_output_events, + /// The actual event queue. Since we're going for correctness over performance, this uses a very + /// suboptimal memory layout by just using an `enum` instead of doing fancy bit packing. + events: Mutex>, +} + +/// An event sent to or from the plugin. This uses an enum to make the implementation simple and +/// correct at the cost of more wasteful memory usage. +#[derive(Debug, Clone)] +#[repr(C, align(8))] +pub enum Event { + /// `CLAP_EVENT_NOTE_ON`, `CLAP_EVENT_NOTE_OFF`, `CLAP_EVENT_NOTE_CHOKE`, or `CLAP_EVENT_NOTE_END`. + Note(clap_event_note), + /// `CLAP_EVENT_NOTE_EXPRESSION`. + NoteExpression(clap_event_note_expression), + /// `CLAP_EVENT_MIDI`. + Midi(clap_event_midi), + /// `CLAP_EVENT_PARAM_VALUE`. + ParamValue(clap_event_param_value), + /// `CLAP_EVENT_PARAM_MOD`. + ParamMod(clap_event_param_mod), + /// `CLAP_EVENT_TRANSPORT`. + Transport(clap_event_transport), + /// An unhandled event type. This is only used when the plugin outputs an event we don't handle + /// or recognize. + Unknown(clap_event_header), +} + +impl EventQueue { + /// Construct a new event queue. This can be used as both an input and an output queue. + pub fn new() -> Pin> { + let mut queue = Box::pin(Self { + vtable_input: clap_input_events { + // This is set to point to this object below + ctx: std::ptr::null_mut(), + size: Some(Self::size), + get: Some(Self::get), + }, + + vtable_output: clap_output_events { + // This is set to point to this object below + ctx: std::ptr::null_mut(), + try_push: Some(Self::try_push), + }, + + // Using a mutex here is obviously a terrible idea in a real host, but we're not a real + // host + events: Mutex::new(Vec::new()), + }); + + queue.vtable_input.ctx = &*queue as *const Self as *mut _; + queue.vtable_output.ctx = &*queue as *const Self as *mut _; + queue + } + + pub fn clear(&self) { + self.events.lock().unwrap().clear(); + } + + pub fn add_events(&self, extend: impl IntoIterator) { + let mut events = self.events.lock().unwrap(); + let should_sort = !events.is_empty(); + events.extend(extend); + if should_sort { + events.sort_by_key(|event| event.header().time); + } + } + + pub fn read(&self) -> Vec { + self.events.lock().unwrap().clone() + } + + /// Get the vtable pointer for input events. + pub fn vtable_input(self: &Pin>) -> *const clap_input_events { + &self.vtable_input + } + + /// Get the vtable pointer for output events. + pub fn vtable_output(self: &Pin>) -> *const clap_output_events { + &self.vtable_output + } + + unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { + unsafe { + check_null_ptr!(list, (*list).ctx); + let this = &*((*list).ctx as *const Self); + this.events.lock().unwrap().len() as u32 + } + } + + unsafe extern "C" fn get( + list: *const clap_input_events, + index: u32, + ) -> *const clap_event_header { + unsafe { + check_null_ptr!(list, (*list).ctx); + let this = &*((*list).ctx as *const Self); + + let events = this.events.lock().unwrap(); + match events.get(index as usize) { + Some(event) => event.header(), + None => { + log::warn!( + "The plugin tried to get an event with index {index} ({} total events)", + events.len() + ); + std::ptr::null() + } + } + } + } + + unsafe extern "C" fn try_push( + list: *const clap_output_events, + event: *const clap_event_header, + ) -> bool { + unsafe { + check_null_ptr!(list, (*list).ctx, event); + let this = &*((*list).ctx as *const Self); + + // The monotonicity of the plugin's event insertion order is checked as part of the output + // consistency checks + this.events.lock().unwrap().push(Event::from_raw(event)); + + true + } + } +} + +impl Event { + /// Parse an event from a plugin-provided pointer. Returns an error if the pointer as a null pointer + pub unsafe fn from_raw(ptr: *const clap_event_header) -> Self { + assert!( + !ptr.is_null(), + "Null pointer provided for 'clap_event_header'." + ); + + unsafe { + match ((*ptr).space_id, ((*ptr).type_)) { + ( + CLAP_CORE_EVENT_SPACE_ID, + CLAP_EVENT_NOTE_ON + | CLAP_EVENT_NOTE_OFF + | CLAP_EVENT_NOTE_CHOKE + | CLAP_EVENT_NOTE_END, + ) => Event::Note(*(ptr as *const clap_event_note)), + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_EXPRESSION) => { + Event::NoteExpression(*(ptr as *const clap_event_note_expression)) + } + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_VALUE) => { + Event::ParamValue(*(ptr as *const clap_event_param_value)) + } + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_MOD) => { + Event::ParamMod(*(ptr as *const clap_event_param_mod)) + } + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI) => { + Event::Midi(*(ptr as *const clap_event_midi)) + } + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_TRANSPORT) => { + Event::Transport(*(ptr as *const clap_event_transport)) + } + (_, _) => Event::Unknown(*ptr), + } + } + } + + /// Get a a reference to the event's header. + pub fn header(&self) -> &clap_event_header { + match self { + Event::Note(event) => &event.header, + Event::NoteExpression(event) => &event.header, + Event::ParamValue(event) => &event.header, + Event::ParamMod(event) => &event.header, + Event::Midi(event) => &event.header, + Event::Transport(event) => &event.header, + Event::Unknown(header) => header, + } + } +} diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs new file mode 100644 index 0000000..ec1ab5a --- /dev/null +++ b/src/plugin/process/transport.rs @@ -0,0 +1,99 @@ +use clap_sys::{events::*, fixedpoint::*}; + +/// The current transport state. This can be modified between process calls to simulate +/// transport changes. +#[derive(Debug, Clone, Default)] +pub struct TransportState { + pub sample_pos: i64, + + pub is_playing: bool, + pub is_recording: bool, + + pub tempo: Option<(f64, f64)>, + pub time_signature: Option<(u16, u16)>, + + pub position_beats: Option, + pub position_seconds: Option, +} + +impl TransportState { + pub fn advance(&mut self, samples: u32, sample_rate: f64) { + self.sample_pos += samples as i64; + + if let Some(position_seconds) = &mut self.position_seconds { + *position_seconds += samples as f64 / sample_rate; + } + + if let Some((tempo, tempo_inc)) = &mut self.tempo { + let tempo_start = *tempo; + let tempo_end = tempo_start + (*tempo_inc * samples as f64); + *tempo = tempo_end; + + if let Some(position_beats) = &mut self.position_beats { + // Integrate tempo over the sample block using the trapezoidal rule + *position_beats += + (samples as f64 * (tempo_end + tempo_start) / 60.0 * 0.5) / sample_rate; + } + } + } + + pub fn as_clap_transport(&self, offset: u32) -> clap_event_transport { + let mut flags = 0; + flags |= self.is_playing as u32 * CLAP_TRANSPORT_IS_PLAYING; + flags |= self.is_recording as u32 * CLAP_TRANSPORT_IS_RECORDING; + flags |= self.position_beats.is_some() as u32 * CLAP_TRANSPORT_HAS_BEATS_TIMELINE; + flags |= self.position_seconds.is_some() as u32 * CLAP_TRANSPORT_HAS_SECONDS_TIMELINE; + flags |= self.tempo.is_some() as u32 * CLAP_TRANSPORT_HAS_TEMPO; + flags |= self.time_signature.is_some() as u32 * CLAP_TRANSPORT_HAS_TIME_SIGNATURE; + + clap_event_transport { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_TRANSPORT, + flags: 0, + }, + flags, + + // sending intentional invalid values when the info is not available + // the plugin **must** check the flags to see what info is valid + song_pos_beats: self + .position_beats + .map(|b| (b * CLAP_BEATTIME_FACTOR as f64).round() as i64) + .unwrap_or(i64::MIN), + song_pos_seconds: self + .position_seconds + .map(|s| (s * CLAP_SECTIME_FACTOR as f64).round() as i64) + .unwrap_or(i64::MIN), + tempo: self.tempo.map(|(t, _)| t).unwrap_or(f64::NAN), + tempo_inc: self.tempo.map(|(_, ti)| ti).unwrap_or(f64::NAN), + loop_start_beats: i64::MAX, + loop_end_beats: i64::MIN, + loop_start_seconds: i64::MAX, + loop_end_seconds: i64::MIN, + bar_start: i64::MAX, + bar_number: i32::MIN, + tsig_num: self.time_signature.map(|(n, _)| n).unwrap_or(u16::MAX), + tsig_denom: self.time_signature.map(|(_, d)| d).unwrap_or(0), + } + } +} + +/// A constant mask for audio processing. Each bit represents whether the corresponding audio channel +/// is constant (1) or not (0). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConstantMask(pub u64); + +impl ConstantMask { + pub const CONSTANT: Self = Self(u64::MAX); + pub const DYNAMIC: Self = Self(0); + + pub fn is_channel_constant(&self, channel: u32) -> bool { + self.0 & 1u64.unbounded_shl(channel) != 0 + } + + pub fn are_channels_constant(&self, channels: u32) -> bool { + self.0 & 1u64.unbounded_shl(channels).wrapping_sub(1) == 0 + } +} diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 3c55467..3e0c2c9 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -32,6 +32,8 @@ pub enum PluginTestCase { ProcessAudioInPlaceBasic, #[strum(serialize = "process-audio-out-of-place-double")] ProcessAudioOutOfPlaceDouble, + #[strum(serialize = "process-audio-in-place-double")] + ProcessAudioInPlaceDouble, #[strum(serialize = "process-audio-constant-mask")] ProcessAudioConstantMask, #[strum(serialize = "process-audio-reset-determinism")] @@ -82,181 +84,167 @@ impl<'a> TestCase<'a> for PluginTestCase { fn description(&self) -> String { match self { PluginTestCase::DescriptorConsistency => String::from( - "The plugin descriptor returned from the plugin factory and the plugin descriptor \ - stored on the 'clap_plugin object should be equivalent.", - ), - PluginTestCase::FeaturesCategories => String::from( - "The plugin needs to have at least one of the main CLAP category features.", + "The plugin descriptor returned from the plugin factory and the plugin descriptor stored on the \ + 'clap_plugin object should be equivalent.", ), + PluginTestCase::FeaturesCategories => { + String::from("The plugin needs to have at least one of the main CLAP category features.") + } PluginTestCase::FeaturesDuplicates => { String::from("The plugin's features array should not contain any duplicates.") } PluginTestCase::ProcessAudioOutOfPlaceBasic => String::from( - "Processes random audio through the plugin with its default parameter values and \ - tests whether the output does not contain any non-finite or subnormal values. \ - Uses out-of-place audio processing.", + "Processes random audio through the plugin with its default parameter values and tests whether the \ + output does not contain any non-finite or subnormal values. Uses out-of-place audio processing.", ), PluginTestCase::ProcessAudioInPlaceBasic => String::from( - "Processes random audio through the plugin with its default parameter values and \ - tests whether the output does not contain any non-finite or subnormal values. \ - Uses in-place audio processing for buses that support it.", + "Processes random audio through the plugin with its default parameter values and tests whether the \ + output does not contain any non-finite or subnormal values. Uses in-place audio processing for buses \ + that support it.", ), PluginTestCase::ProcessAudioOutOfPlaceDouble => format!( - "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones \ - for ports that support it.", + "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ + support it.", PluginTestCase::ProcessAudioOutOfPlaceBasic, ), + PluginTestCase::ProcessAudioInPlaceDouble => format!( + "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ + support it.", + PluginTestCase::ProcessAudioInPlaceBasic, + ), PluginTestCase::LayoutConfigurableAudioPorts => format!( - "Performs the same test as {}, but this time it tries random configurations \ - exposed via the 'configurable-audio-ports' extension.", + "Performs the same test as {}, but this time it tries random configurations exposed via the \ + 'configurable-audio-ports' extension.", PluginTestCase::ProcessAudioOutOfPlaceBasic, ), PluginTestCase::LayoutAudioPortsConfig => format!( - "Performs the same test as {}, but this time it tries all available port \ - configurations exposed via the 'audio-ports-config' extension.", + "Performs the same test as {}, but this time it tries all available port configurations exposed via \ + the 'audio-ports-config' extension.", PluginTestCase::ProcessAudioInPlaceBasic, ), PluginTestCase::ProcessAudioConstantMask => String::from( - "Processes random audio through the plugin with its default parameter values \ - while setting the constant mask on silent blocks, and tests whether the output \ - does not contain any non-finite or subnormal values and that the plugin sets the \ - constant mask correctly. Uses out-of-place audio processing.", + "Processes random audio through the plugin with its default parameter values while setting the \ + constant mask on silent blocks, and tests whether the output does not contain any non-finite or \ + subnormal values and that the plugin sets the constant mask correctly. Uses out-of-place audio \ + processing.", ), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( - "Sends audio and random note and MIDI events to the plugin with its default \ - parameter values and tests the output for consistency. Uses out-of-place audio \ - processing.", + "Sends audio and random note and MIDI events to the plugin with its default parameter values and \ + tests the output for consistency. Uses out-of-place audio processing.", ), PluginTestCase::ProcessNoteInconsistent => String::from( - "Sends intentionally inconsistent and mismatching note and MIDI events to the \ - plugin with its default parameter values and tests the output for consistency. \ - Uses out-of-place audio processing.", + "Sends intentionally inconsistent and mismatching note and MIDI events to the plugin with its default \ + parameter values and tests the output for consistency. Uses out-of-place audio processing.", ), PluginTestCase::ProcessVaryingSampleRates => String::from( - "Processes random audio and random note events through the plugin with its \ - default parameter values while trying different sample rates ranging from 1kHz \ - to 768kHz, including fractional rates, and tests whether the output does not \ - contain any non-finite or subnormal values. Uses out-of-place audio processing.", + "Processes random audio and random note events through the plugin with its default parameter values \ + while trying different sample rates ranging from 1kHz to 768kHz, including fractional rates, and \ + tests whether the output does not contain any non-finite or subnormal values. Uses out-of-place \ + audio processing.", ), PluginTestCase::ProcessVaryingBlockSizes => String::from( - "Processes random audio and random note events through the plugin with its \ - default parameter values while trying different maximum block sizes ranging from \ - 1 to 32768, including non-power-of-two ones, and tests whether the output does \ - not contain any non-finite or subnormal values. Uses out-of-place audio \ - processing.", + "Processes random audio and random note events through the plugin with its default parameter values \ + while trying different maximum block sizes ranging from 1 to 32768, including non-power-of-two ones, \ + and tests whether the output does not contain any non-finite or subnormal values. Uses out-of-place \ + audio processing.", ), PluginTestCase::ProcessRandomBlockSizes => String::from( - "Processes random audio and random note events through the plugin with maximum \ - block size of 2048 while randomizing block sizes for each process call, and \ - tests whether the output does not contain any non-finite or subnormal values. \ - Uses out-of-place audio processing.", + "Processes random audio and random note events through the plugin with maximum block size of 2048 \ + while randomizing block sizes for each process call, and tests whether the output does not contain \ + any non-finite or subnormal values. Uses out-of-place audio processing.", ), PluginTestCase::ProcessAudioResetDeterminism => String::from( - "Asserts that resetting the plugin via 'clap_plugin::reset()' and via \ - re-activation results in deterministic output when processing the same audio and \ - events again.", + "Asserts that resetting the plugin via 'clap_plugin::reset()' and via re-activation results in \ + deterministic output when processing the same audio and events again.", ), PluginTestCase::ParamConversions => String::from( - "Asserts that value to string and string to value conversions are supported for \ - ether all or none of the plugin's parameters, and that conversions between \ - values and strings roundtrip consistently.", + "Asserts that value to string and string to value conversions are supported for ether all or none of \ + the plugin's parameters, and that conversions between values and strings roundtrip consistently.", ), PluginTestCase::ParamFuzzBasic => format!( - "Generates {} sets of random parameter values, sets those on the plugin, and has \ - the plugin process {} buffers of random audio and note events. The plugin passes \ - the test if it doesn't produce any infinite or NaN values, and doesn't crash.", + "Generates {} sets of random parameter values, sets those on the plugin, and has the plugin process \ + {} buffers of random audio and note events. The plugin passes the test if it doesn't produce any \ + infinite or NaN values, and doesn't crash.", params::FUZZ_NUM_PERMUTATIONS, params::FUZZ_RUNS_PER_PERMUTATION ), PluginTestCase::ParamFuzzBounds => format!( - "The exact same test as {}, but this time the parameter values are snapped to the \ - minimum and maximum values.", + "The exact same test as {}, but this time the parameter values are snapped to the minimum and maximum \ + values.", PluginTestCase::ParamFuzzBasic ), PluginTestCase::ParamFuzzSampleAccurate => String::from( - "Sets parameter values in a sample-accurate fashion while processing audio, \ - generating them at fixed intervals (1, 100, 1000 samples). The plugin passes the \ - test if it doesn't produce any infinite or NaN values, and doesn't crash.", + "Sets parameter values in a sample-accurate fashion while processing audio, generating them at fixed \ + intervals (1, 100, 1000 samples). The plugin passes the test if it doesn't produce any infinite or \ + NaN values, and doesn't crash.", ), PluginTestCase::ParamFuzzModulation => String::from( - "Sends parameter change events, including monophonic modulation and polyphonic \ - automation/modulation events at random irregular unsynchronized intervals, and \ - have the plugin process them. The plugin passes the test if it doesn't produce \ - any infinite or NaN values, and doesn't crash.", + "Sends parameter change events, including monophonic modulation and polyphonic automation/modulation \ + events at random irregular unsynchronized intervals, and have the plugin process them. The plugin \ + passes the test if it doesn't produce any infinite or NaN values, and doesn't crash.", ), PluginTestCase::ParamSetWrongNamespace => String::from( - "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with \ - a mismatching namespace ID. Asserts that the plugin's parameter values don't \ - change.", + "Sends events to the plugin with the 'CLAP_EVENT_PARAM_VALUE' event type but with a mismatching \ + namespace ID. Asserts that the plugin's parameter values don't change.", ), PluginTestCase::ParamDefaultValues => String::from( - "Asserts that the values for all parameters are set correctly to their default \ - values when the plugin is initialized.", + "Asserts that the values for all parameters are set correctly to their default values when the plugin \ + is initialized.", ), PluginTestCase::StateInvalidEmpty => String::from( - "The plugin should return false when 'clap_plugin_state::load()' is called with \ - an empty state.", + "The plugin should return false when 'clap_plugin_state::load()' is called with an empty state.", ), PluginTestCase::StateInvalidRandom => String::from( - "Loads 3x1MB chunks of random bytes via 'clap_plugin_state::load()' and asserts \ - that the plugin doesn't crash.", + "Loads 3x1MB chunks of random bytes via 'clap_plugin_state::load()' and asserts that the plugin \ + doesn't crash.", ), PluginTestCase::StateReproducibilityBasic => String::from( - "Randomizes a plugin's parameters, saves its state, recreates the plugin \ - instance, reloads the state, and then checks whether the parameter values are \ - the same and whether saving the state once more results in the same state file \ - as before. The parameter values are updated using the process function.", + "Randomizes a plugin's parameters, saves its state, recreates the plugin instance, reloads the state, \ + and then checks whether the parameter values are the same and whether saving the state once more \ + results in the same state file as before. The parameter values are updated using the process \ + function.", ), PluginTestCase::StateReproducibilityNullCookies => format!( - "The exact same test as {}, but with all cookies in the parameter events set to \ - null pointers. The plugin should handle this in the same way as the other test \ - case.", + "The exact same test as {}, but with all cookies in the parameter events set to null pointers. The \ + plugin should handle this in the same way as the other test case.", PluginTestCase::StateReproducibilityBasic ), PluginTestCase::StateReproducibilityFlush => String::from( - "Randomizes a plugin's parameters, saves its state, recreates the plugin \ - instance, sets the same parameters as before, saves the state again, and then \ - asserts that the two states are identical. The parameter values are set updated \ - using the process function to create the first state, and using the flush \ - function to create the second state.", + "Randomizes a plugin's parameters, saves its state, recreates the plugin instance, sets the same \ + parameters as before, saves the state again, and then asserts that the two states are identical. The \ + parameter values are set updated using the process function to create the first state, and using the \ + flush function to create the second state.", ), PluginTestCase::StateBufferedStreams => format!( - "Performs the same state and parameter reproducibility check as in '{}', but this \ - time the plugin is only allowed to read a small prime number of bytes at a time \ - when reloading and resaving the state.", + "Performs the same state and parameter reproducibility check as in '{}', but this time the plugin is \ + only allowed to read a small prime number of bytes at a time when reloading and resaving the state.", PluginTestCase::StateReproducibilityBasic ), } } fn run(&self, (library_path, plugin_id): Self::TestArgs) -> Result { + // SAFETY: This is called on the main thread. let library = &PluginLibrary::load(library_path) .with_context(|| format!("Could not load '{}'", library_path.display()))?; match self { - PluginTestCase::DescriptorConsistency => { - descriptor::test_consistency(library, plugin_id) - } - PluginTestCase::FeaturesCategories => { - descriptor::test_features_categories(library, plugin_id) - } - PluginTestCase::FeaturesDuplicates => { - descriptor::test_features_duplicates(library, plugin_id) - } - PluginTestCase::LayoutAudioPortsConfig => { - layout::test_layout_audio_ports_config(library, plugin_id) - } + PluginTestCase::DescriptorConsistency => descriptor::test_consistency(library, plugin_id), + PluginTestCase::FeaturesCategories => descriptor::test_features_categories(library, plugin_id), + PluginTestCase::FeaturesDuplicates => descriptor::test_features_duplicates(library, plugin_id), + PluginTestCase::LayoutAudioPortsConfig => layout::test_layout_audio_ports_config(library, plugin_id), PluginTestCase::LayoutConfigurableAudioPorts => { layout::test_layout_configurable_audio_ports(library, plugin_id) } PluginTestCase::ProcessAudioOutOfPlaceBasic => { processing::test_process_audio_basic(library, plugin_id, false) } - PluginTestCase::ProcessAudioInPlaceBasic => { - processing::test_process_audio_basic(library, plugin_id, true) - } + PluginTestCase::ProcessAudioInPlaceBasic => processing::test_process_audio_basic(library, plugin_id, true), PluginTestCase::ProcessAudioOutOfPlaceDouble => { - processing::test_process_audio_double(library, plugin_id) + processing::test_process_audio_double(library, plugin_id, false) + } + PluginTestCase::ProcessAudioInPlaceDouble => { + processing::test_process_audio_double(library, plugin_id, true) } PluginTestCase::ProcessAudioConstantMask => { processing::test_process_audio_constant_mask(library, plugin_id) @@ -276,42 +264,24 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessVaryingBlockSizes => { processing::test_process_varying_block_sizes(library, plugin_id) } - PluginTestCase::ProcessRandomBlockSizes => { - processing::test_process_random_block_sizes(library, plugin_id) - } + PluginTestCase::ProcessRandomBlockSizes => processing::test_process_random_block_sizes(library, plugin_id), PluginTestCase::ParamConversions => params::test_param_conversions(library, plugin_id), - PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id), - PluginTestCase::ParamFuzzBounds => params::test_param_fuzz_bounds(library, plugin_id), - PluginTestCase::ParamFuzzSampleAccurate => { - params::test_param_fuzz_sample_accurate(library, plugin_id) - } - PluginTestCase::ParamFuzzModulation => { - params::test_param_fuzz_modulation(library, plugin_id) - } - PluginTestCase::ParamSetWrongNamespace => { - params::test_param_set_wrong_namespace(library, plugin_id) - } - PluginTestCase::ParamDefaultValues => { - params::test_param_default_values(library, plugin_id) - } - PluginTestCase::StateInvalidEmpty => { - state::test_state_invalid_empty(library, plugin_id) - } - PluginTestCase::StateInvalidRandom => { - state::test_state_invalid_random(library, plugin_id) - } + PluginTestCase::ParamFuzzBasic => params::test_param_fuzz_basic(library, plugin_id, false), + PluginTestCase::ParamFuzzBounds => params::test_param_fuzz_basic(library, plugin_id, true), + PluginTestCase::ParamFuzzSampleAccurate => params::test_param_fuzz_sample_accurate(library, plugin_id), + PluginTestCase::ParamFuzzModulation => params::test_param_fuzz_modulation(library, plugin_id), + PluginTestCase::ParamSetWrongNamespace => params::test_param_set_wrong_namespace(library, plugin_id), + PluginTestCase::ParamDefaultValues => params::test_param_default_values(library, plugin_id), + PluginTestCase::StateInvalidEmpty => state::test_state_invalid_empty(library, plugin_id), + PluginTestCase::StateInvalidRandom => state::test_state_invalid_random(library, plugin_id), PluginTestCase::StateReproducibilityBasic => { state::test_state_reproducibility_basic(library, plugin_id, false) } PluginTestCase::StateReproducibilityNullCookies => { state::test_state_reproducibility_basic(library, plugin_id, true) } - PluginTestCase::StateReproducibilityFlush => { - state::test_state_reproducibility_flush(library, plugin_id) - } - PluginTestCase::StateBufferedStreams => { - state::test_state_buffered_streams(library, plugin_id) - } + PluginTestCase::StateReproducibilityFlush => state::test_state_reproducibility_flush(library, plugin_id), + PluginTestCase::StateBufferedStreams => state::test_state_buffered_streams(library, plugin_id), } } } diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 5491cb7..40a2f51 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -2,17 +2,16 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::audio_ports_config::{AudioPortsConfig, AudioPortsConfigInfo}; use crate::plugin::ext::configurable_audio_ports::{AudioPortsRequest, ConfigurableAudioPorts}; use crate::plugin::ext::note_ports::NotePorts; -use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::library::PluginLibrary; +use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; -use crate::tests::plugin::processing::run_simple; use crate::tests::rng::{NoteGenerator, new_prng}; use anyhow::{Context, Result}; use rand::Rng; use rand::seq::SliceRandom; use rand_pcg::Pcg32; -const BUFFER_SIZE: usize = 512; +const BUFFER_SIZE: u32 = 512; /// The test for `PluginTestCase::LayoutAudioPortsConfig`. pub fn test_layout_audio_ports_config( @@ -175,26 +174,29 @@ pub fn test_layout_audio_ports_config( // TODO: check info } - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + plugin + .on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = + AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } - Ok(()) - }) - .with_context(|| { - format!( - "Error while processing audio with IO configuration '{}' ({})", - config_audio_ports_config.name, config_audio_ports_config.id, - ) - })?; + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with IO configuration '{}' ({})", + config_audio_ports_config.name, config_audio_ports_config.id, + ) + })?; } plugin @@ -319,26 +321,29 @@ pub fn test_layout_configurable_audio_ports( .config() .context("Error while querying 'audio-ports' IO configuration")?; - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + plugin + .on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = + AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } - Ok(()) - }) - .with_context(|| { - format!( - "Error while processing audio with the following configuration: {}", - print_layout_requests(&requests) - ) - })?; + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with the following configuration: {}", + print_layout_requests(&requests) + ) + })?; } plugin diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index ee929fb..dda34d2 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -4,9 +4,8 @@ use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::ext::params::{ParamInfo, Params}; -use crate::plugin::instance::process::{AudioBuffers, Event, ProcessData}; use crate::plugin::library::PluginLibrary; -use crate::tests::plugin::processing::run_simple; +use crate::plugin::process::{AudioBuffers, Event, ProcessScope}; use crate::tests::rng::{NoteGenerator, ParamFuzzer, new_prng}; use crate::tests::{TestCase, TestStatus}; use anyhow::{Context, Result}; @@ -16,7 +15,7 @@ use serde::Serialize; use std::collections::BTreeMap; /// The fixed buffer size to use for these tests. -const BUFFER_SIZE: usize = 512; +const BUFFER_SIZE: u32 = 512; /// The number of different parameter combinations to try in the parameter fuzzing tests. pub const FUZZ_NUM_PERMUTATIONS: usize = 50; /// How many buffers of [`BUFFER_SIZE`] samples to process at each parameter permutation. This @@ -192,8 +191,12 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul Ok(TestStatus::Success { details: None }) } -/// The test for `ProcessingTest::ParamFuzzBasic`. -pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result { +/// The test for `ProcessingTest::ParamFuzzBasic` and `ProcessingTest::ParamFuzzBounds`. +pub fn test_param_fuzz_basic( + library: &PluginLibrary, + plugin_id: &str, + snap_to_bounds: bool, +) -> Result { let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) @@ -234,184 +237,57 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str) -> Result // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. - let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - - // We'll keep track of the current and the previous set of parameter value so we can write them - // to a file if the test fails - let mut current_events: Option>; - let mut previous_events: Option> = None; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); - - for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { - current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); - - let mut have_set_parameters = false; - let run_result = run_simple( - &plugin, - &mut process_data, - FUZZ_RUNS_PER_PERMUTATION, - |process_data| { - if !have_set_parameters { - process_data - .input_events - .add_events(current_events.clone().unwrap()); - have_set_parameters = true; - } - - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - process_data.buffers.randomize(&mut prng); - - Ok(()) - }, - ); - - // If the run failed we'll want to write the parameter values to a file first - if run_result.is_err() { - let (previous_param_values_file_path, previous_param_values_file) = - PluginTestCase::ParamFuzzBasic - .temporary_file(plugin_id, PREVIOUS_PARAM_VALUES_FILE_NAME)?; - let (current_param_values_file_path, current_param_values_file) = - PluginTestCase::ParamFuzzBasic - .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; - - serde_json::to_writer_pretty( - previous_param_values_file, - &ParamValue::from_events(previous_events, ¶m_infos), - )?; - serde_json::to_writer_pretty( - current_param_values_file, - &ParamValue::from_events(current_events, ¶m_infos), - )?; - - // This is a bit weird and there may be a better way to do this, but we only want to - // write the parameter values if we know the run has failed, and we only know the - // filename after writing those values to a file - return Err(run_result - .with_context(|| { - format!( - "Invalid output detected in parameter value permutation {} of {} ('{}' \ - and '{}' contain the current and previous parameter values)", - permutation_no, - FUZZ_NUM_PERMUTATIONS, - current_param_values_file_path.display(), - previous_param_values_file_path.display(), - ) - }) - .unwrap_err()); - } - - std::mem::swap(&mut previous_events, &mut current_events); + let mut param_fuzzer = ParamFuzzer::new(¶m_infos); + if snap_to_bounds { + param_fuzzer = param_fuzzer.snap_to_bounds(); } - plugin - .handle_callback() - .context("An error occured during a callback")?; - - Ok(TestStatus::Success { details: None }) -} - -/// The test for `ProcessingTest::ParamFuzzBounds`. -pub fn test_param_fuzz_bounds(library: &PluginLibrary, plugin_id: &str) -> Result { - let mut prng = new_prng(); - - let plugin = library - .create_plugin(plugin_id) - .context("Could not create the plugin instance")?; - plugin.init().context("Error during initialization")?; - - // Both audio and note ports are optional - let audio_ports = plugin.get_extension::(); - let note_ports = plugin.get_extension::(); - let params = match plugin.get_extension::() { - Some(params) => params, - None => { - return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), - }); - } - }; - - plugin - .handle_callback() - .context("An error occured during a callback")?; - - let audio_ports_config = audio_ports - .map(|ports| ports.config()) - .transpose() - .context("Could not fetch the plugin's audio port config")? - .unwrap_or_default(); - let note_ports_config = note_ports - .map(|ports| ports.config()) - .transpose() - .context("Could not fetch the plugin's note port config")? - .unwrap_or_default(); - let param_infos = params - .info() - .context("Could not fetch the plugin's parameters")?; - - // For each set of runs we'll generate new parameter values, and if the plugin supports notes - // we'll also generate note events. - let param_fuzzer = ParamFuzzer::new(¶m_infos).with_snap_to_bounds(); - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails let mut current_events: Option>; let mut previous_events: Option> = None; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); let mut have_set_parameters = false; - let run_result = run_simple( - &plugin, - &mut process_data, - FUZZ_RUNS_PER_PERMUTATION, - |process_data| { + let run_result = plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..FUZZ_RUNS_PER_PERMUTATION { if !have_set_parameters { - process_data - .input_events + process + .input_queue() .add_events(current_events.clone().unwrap()); have_set_parameters = true; } - // Audio and MIDI/note events are randomized in accordance to what the plugin - // supports - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - process_data.buffers.randomize(&mut prng); + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } - Ok(()) - }, - ); + Ok(()) + }); // If the run failed we'll want to write the parameter values to a file first if run_result.is_err() { let (previous_param_values_file_path, previous_param_values_file) = - PluginTestCase::ParamFuzzBounds + PluginTestCase::ParamFuzzBasic .temporary_file(plugin_id, PREVIOUS_PARAM_VALUES_FILE_NAME)?; let (current_param_values_file_path, current_param_values_file) = - PluginTestCase::ParamFuzzBounds + PluginTestCase::ParamFuzzBasic .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; serde_json::to_writer_pretty( previous_param_values_file, &ParamValue::from_events(previous_events, ¶m_infos), )?; - serde_json::to_writer_pretty( current_param_values_file, &ParamValue::from_events(current_events, ¶m_infos), @@ -492,69 +368,41 @@ pub fn test_param_fuzz_sample_accurate( // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut current_events: Option> = None; let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); for &interval in INTERVALS { - let num_steps = interval.div_ceil(BUFFER_SIZE as u32); - let mut current_sample = 0; - let run_result = run_simple( - &plugin, - &mut process_data, - num_steps as usize, - |process_data| { - while current_sample < BUFFER_SIZE as u32 { + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + let num_steps = interval.div_ceil(BUFFER_SIZE); + let mut current_sample = 0; + for _ in 0..num_steps { + while current_sample < BUFFER_SIZE { let events: Vec = param_fuzzer .randomize_params_at(&mut prng, current_sample) .collect(); - process_data.input_events.add_events(events.clone()); + process.input_queue().add_events(events.clone()); current_events = Some(events); current_sample += interval; } + current_sample -= BUFFER_SIZE; + // Audio and MIDI/note events are randomized in accordance to what the plugin // supports - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - BUFFER_SIZE as u32, - ); - process_data.buffers.randomize(&mut prng); - current_sample -= BUFFER_SIZE as u32; - - Ok(()) - }, - ); - - // If the run failed we'll want to write the parameter values to a file first - if run_result.is_err() { - let (current_param_values_file_path, current_param_values_file) = - PluginTestCase::ParamFuzzSampleAccurate - .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; - - serde_json::to_writer_pretty( - current_param_values_file, - &ParamValue::from_events(current_events, ¶m_infos), - )?; + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } - // This is a bit weird and there may be a better way to do this, but we only want to - // write the parameter values if we know the run has failed, and we only know the - // filename after writing those values to a file - return Err(run_result - .with_context(|| { - format!( - "Invalid output detected when automating parameters with interval of {} \ - samples ('{}' contains the current parameter values)", - interval, - current_param_values_file_path.display(), - ) - }) - .unwrap_err()); - } + Ok(()) + })?; } plugin @@ -601,25 +449,20 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R .info() .context("Could not fetch the plugin's parameters")?; - let mut note_event_rng = NoteGenerator::new(¬e_ports).with_params(¶m_infos); let param_fuzzer = ParamFuzzer::new(¶m_infos); - + let mut note_rng = NoteGenerator::new(¬e_ports).with_params(¶m_infos); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - param_fuzzer.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(param_fuzzer.generate_events(&mut prng, process.max_block_size())); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, process.max_block_size())); Ok(()) })?; @@ -684,17 +527,13 @@ pub fn test_param_set_wrong_namespace( } } - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, Default::default()); - - process_data.run_once(&plugin, move |plugin, process_data| { - process_data.buffers.randomize(&mut prng); - process_data - .input_events - .add_events(random_param_set_events); - plugin.process(process_data)?; + plugin.on_audio_thread(|plugin| { + let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process = ProcessScope::new(&plugin, &mut buffers)?; - Ok(()) + process.audio_buffers().randomize(&mut prng); + process.input_queue().add_events(random_param_set_events); + process.run() })?; // We'll check that the plugin has these sames values after reloading the state. These values diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index e288bec..6a6a700 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -2,67 +2,17 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::NotePorts; -use crate::plugin::instance::Plugin; -use crate::plugin::instance::process::{ - AudioBuffers, ProcessConfig, ProcessControlFlow, ProcessData, -}; use crate::plugin::library::PluginLibrary; +use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng}; use anyhow::{Context, Result}; use rand::Rng; -const BUFFER_SIZE: usize = 512; - -pub fn run_simple( - plugin: &Plugin, - data: &mut ProcessData, - num_iters: usize, - mut preprocess: Callback, -) -> Result<()> -where - Callback: FnMut(&mut ProcessData) -> Result<()> + Send, -{ - let mut original_buffers = data.buffers.clone(); - let mut curr_iter = 0; - - data.run(plugin, |plugin, process| { - curr_iter += 1; - - preprocess(process).with_context(|| { - format!( - "Failed to preprocess cycle {} out of {}", - curr_iter, num_iters - ) - })?; - - original_buffers.clone_from(process.buffers); - - plugin.process(process).with_context(|| { - format!("Failed to process cycle {} out of {}", curr_iter, num_iters) - })?; - - check_process_call_consistency(process, &original_buffers, true).with_context(|| { - format!( - "Failed to validate cycle {} out of {}", - curr_iter, num_iters - ) - })?; - - if curr_iter < num_iters { - Ok(ProcessControlFlow::Continue) - } else { - Ok(ProcessControlFlow::Exit) - } - }) -} +const BUFFER_SIZE: u32 = 512; /// The test for `PluginTestCase::ProcessAudioOutOfPlaceBasic` and `PluginTestCase::ProcessAudioInPlaceBasic`. -pub fn test_process_audio_basic( - library: &PluginLibrary, - plugin_id: &str, - in_place: bool, -) -> Result { +pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_place: bool) -> Result { let mut prng = new_prng(); let plugin = library @@ -83,31 +33,30 @@ pub fn test_process_audio_basic( } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; - let mut audio_buffers = if in_place { AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE) } else { AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE) }; - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process.run()?; + } + Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } // The test for `PluginTestCase::ProcessAudioOutOfPlaceDouble`. -pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str) -> Result { +pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_place: bool) -> Result { let mut prng = new_prng(); let plugin = library @@ -128,29 +77,49 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str) -> Re } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); - let Some(mut audio_buffers) = - AudioBuffers::new_out_of_place_f64(&audio_ports_config, BUFFER_SIZE) - else { + plugin.handle_callback().context("An error occured during a callback")?; + + let has_double_support = audio_ports_config + .inputs + .iter() + .chain(audio_ports_config.outputs.iter()) + .any(|port| port.supports_double_sample_size); + + if !has_double_support { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not support 64-bit floating point audio.", - )), + details: Some(String::from("The plugin does not support 64-bit floating point audio.")), }); + } + + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = if in_place { + AudioBuffers::new_in_place_f64(&audio_ports_config, BUFFER_SIZE) + } else { + AudioBuffers::new_out_of_place_f64(&audio_ports_config, BUFFER_SIZE) }; - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } + Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -194,51 +163,44 @@ pub fn test_process_note_out_of_place( if note_ports_config.inputs.is_empty() { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin implements the 'note-ports' extension but it does not have any input \ - note ports.", + "The plugin implements the 'note-ports' extension but it does not have any input note ports.", )), }); } - plugin - .handle_callback() - .context("An error occured during a callback")?; - // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); if !consistent { - note_event_rng = note_event_rng.with_inconsistent_events(); + note_rng = note_rng.with_inconsistent_events(); } - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } + Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `PluginTestCase::ProcessVaryingSampleRates`. -pub fn test_process_varying_sample_rates( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &str) -> Result { const SAMPLE_RATES: &[f64] = &[ - 1000.0, 10000.0, 22050.0, 32000.0, 44100.0, 48000.0, 88200.0, 96000.0, 192000.0, 384000.0, - 768000.0, 1234.5678, 12345.678, 45678.901, 123456.78, + 1000.0, 10000.0, 22050.0, 32000.0, 44100.0, 48000.0, 88200.0, 96000.0, 192000.0, 384000.0, 768000.0, 1234.5678, + 12345.678, 45678.901, 123456.78, ]; let mut prng = new_prng(); @@ -262,52 +224,35 @@ pub fn test_process_varying_sample_rates( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); - plugin - .handle_callback() - .context("An error occured during a callback")?; + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); for &sample_rate in SAMPLE_RATES { - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut process_data = ProcessData::new( - &mut audio_buffers, - ProcessConfig { - sample_rate, - ..Default::default() - }, - ); - - run_simple(&plugin, &mut process_data, 5, |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); - - Ok(()) - }) - .context(format!( - "Error while processing with {:.2}hz sample rate", - sample_rate - ))?; + plugin + .on_audio_thread(|plugin| -> Result<()> { + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::with_sample_rate(&plugin, &mut audio_buffers, sample_rate)?; + + for _ in 0..5 { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } + + Ok(()) + }) + .with_context(|| format!("Error while processing with {:.2}hz sample rate", sample_rate))?; } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `PluginTestCase::ProcessVaryingBlockSizes`. -pub fn test_process_varying_block_sizes( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { - const BLOCK_SIZES: &[u32] = &[ - 1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 32768, 1536, 10, 17, 2027, - ]; +pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str) -> Result { + const BLOCK_SIZES: &[u32] = &[1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 32768, 1536, 10, 17, 2027]; let mut prng = new_prng(); @@ -331,49 +276,33 @@ pub fn test_process_varying_block_sizes( .unwrap_or_default(); for &buffer_size in BLOCK_SIZES { - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = - AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size as usize); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - let num_iters = (32768 / buffer_size).min(5); - plugin - .handle_callback() - .context("An error occured during a callback")?; - - run_simple( - &plugin, - &mut process_data, - num_iters as usize, - |process_data| { - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + .on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + let num_iters = (32768 / buffer_size).min(5); + + for _ in 0..num_iters { + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, buffer_size)); + process.run()?; + } Ok(()) - }, - ) - .context(format!( - "Error while processing with buffer size of {}", - buffer_size - ))?; + }) + .with_context(|| format!("Error while processing with buffer size of {}", buffer_size))?; } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `PluginTestCase::ProcessRandomBlockSizes`. -pub fn test_process_random_block_sizes( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) -> Result { const MAX_BUFFER_SIZE: u32 = 2048; let mut prng = new_prng(); @@ -397,44 +326,37 @@ pub fn test_process_random_block_sizes( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); - plugin - .handle_callback() - .context("An error occured during a callback")?; - - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = - AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE as usize); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - - run_simple(&plugin, &mut process_data, 20, |process_data| { - process_data.block_size = if prng.random_bool(0.8) { - prng.random_range(2..=MAX_BUFFER_SIZE) - } else { - 1 - }; - - process_data.buffers.randomize(&mut prng); - note_event_rng.fill_event_queue( - &mut prng, - &process_data.input_events, - process_data.block_size, - ); + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, MAX_BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..20 { + let buffer_size = if prng.random_bool(0.8) { + prng.random_range(2..=MAX_BUFFER_SIZE) + } else { + 1 + }; + + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, buffer_size)); + process + .run_with_block_size(buffer_size) + .with_context(|| format!("Error while processing with buffer size of {}", buffer_size))?; + } Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } -/// The test for `PluginTestCase::ProcessVaryingBlockSizes`. -pub fn test_process_audio_constant_mask( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +/// The test for `PluginTestCase::ProcessAudioConstantMask`. +pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -457,78 +379,69 @@ pub fn test_process_audio_constant_mask( if audio_ports_config.inputs.is_empty() { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not have any audio input ports.", - )), + details: Some(String::from("The plugin does not have any audio input ports.")), }); } let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut original_buffers = audio_buffers.clone(); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - - let mut curr_iter = 0; - let mut has_received_constant_output = false; let mut has_received_constant_flag = false; - plugin - .handle_callback() - .context("An error occured during a callback")?; - - process_data.run(&plugin, |plugin, process| { - process.buffers.randomize(&mut prng); - - if curr_iter != 1 { - process.buffers.silence_all_inputs(); - } - - original_buffers.clone_from(process.buffers); - curr_iter += 1; - - plugin - .process(process) - .with_context(|| format!("Failed to process cycle {} out of 20", curr_iter))?; - - check_process_call_consistency(process, &original_buffers, true) - .with_context(|| format!("Failed to validate cycle {} out of 20", curr_iter))?; - - for buffer in process.buffers.buffers() { - let Some(output) = buffer.output() else { + let mut check_buffers = |buffers: &AudioBuffers| -> Result<()> { + for buffer in buffers.buffers() { + let Some(output) = buffer.port().as_output() else { continue; }; for channel in 0..buffer.channels() { - let is_constant = (0..buffer.len()) - .all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); + let is_constant = (0..buffer.len()).all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); // TODO: relax, allow small variations? - let marked_constant = process.buffers.output_constant_mask(output) - & (1u64.unbounded_shl(channel as u32)) - != 0; + let marked_constant = buffers.get_output_constant_mask(output).is_channel_constant(channel); if marked_constant && !is_constant { anyhow::bail!( - "Failed to validate cycle {curr_iter} out of 20: The plugin has marked \ - output port {output}, channel {channel} as constant, but it contains \ + "The plugin has marked output port {output}, channel {channel} as constant, but it contains \ non-constant data." ); } - has_received_constant_flag |= marked_constant; - has_received_constant_output |= is_constant; + if marked_constant { + has_received_constant_flag |= true; + } + + if is_constant { + has_received_constant_output |= true; + } } } - if curr_iter < 20 { - Ok(ProcessControlFlow::Continue) - } else { - Ok(ProcessControlFlow::Exit) + Ok(()) + }; + + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + // block 1: silent inputs, see what the plugin does + process.run()?; + check_buffers(process.audio_buffers())?; + + // block 2: randomize inputs, see if the plugin tracks constant channels + process.audio_buffers().randomize(&mut prng); + process.run()?; + check_buffers(process.audio_buffers())?; + + // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly + // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay + process.audio_buffers().silence_inputs(); + for _ in 3..=40 { + process.run()?; + check_buffers(process.audio_buffers())?; } + + Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if !has_received_constant_flag && has_received_constant_output { return Ok(TestStatus::Warning { @@ -542,10 +455,9 @@ pub fn test_process_audio_constant_mask( } /// The test for `PluginTestCase::ProcessResetDeterminism`. -pub fn test_process_audio_reset_determinism( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: &str) -> Result { + const BUFFER_SIZE: u32 = 4096; + let plugin = library .create_plugin(plugin_id) .context("Could not create the plugin instance")?; @@ -565,173 +477,53 @@ pub fn test_process_audio_reset_determinism( .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); - plugin - .handle_callback() - .context("An error occured during a callback")?; - - let mut note_event_rng = NoteGenerator::new(¬e_ports_config); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32( - &audio_ports_config, - BUFFER_SIZE * 8, /* we do it in one block to simplify the test */ - ); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - let mut curr_iter = 0; - let mut audio_output = vec![]; - - process_data.run(&plugin, |plugin, process_data| { - let mut rng = new_prng(); - - process_data.buffers.randomize(&mut rng); - note_event_rng.fill_event_queue( - &mut new_prng(), - &process_data.input_events, - process_data.block_size, - ); - - let result = match curr_iter { - 0 => ProcessControlFlow::Reset, - 1 => ProcessControlFlow::Continue, - _ => { - plugin.reset(); - process_data.reset(); - note_event_rng.reset(); - - ProcessControlFlow::Exit - } - }; - - plugin.process(process_data)?; - audio_output.push(process_data.buffers.clone()); - curr_iter += 1; - - Ok(result) - })?; - - if !audio_output[0].is_same(&audio_output[1]) { - return Ok(TestStatus::Warning { - details: Some(String::from( - "Plugin output does not seem to be deterministic after reactivation", - )), - }); - } - - if !audio_output[1].is_same(&audio_output[2]) { - anyhow::bail!("Plugin output differs after reset"); - } - - plugin - .handle_callback() - .context("An error occured during a callback")?; - - Ok(TestStatus::Success { details: None }) -} - -/// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, -/// infinite, or denormal values, that the input buffers have not been modified by the plugin, and -/// that the output event queue is monotonically ordered. -fn check_process_call_consistency( - process_data: &ProcessData, - original_buffers: &AudioBuffers, - check_denormals: bool, -) -> Result<()> { - let block_size = process_data.block_size as usize; - - for (buffer, before) in process_data - .buffers - .buffers() - .iter() - .zip(original_buffers.buffers()) - { - // Input-only buffers must not be overwritten during out of place processing - if let (Some(index), None) = (buffer.input(), buffer.output()) { - if !buffer.is_same(before) { - anyhow::bail!( - "The plugin has overwritten an input buffer (index {index}) during \ - out-of-place processing." - ); - } - } - - // Output-only buffers must not be left "untouched" during out of place processing - if let (Some(index), None) = (buffer.output(), buffer.input()) { - let is_all_nans = (0..buffer.channels()).all(|channel| { - (0..block_size).all(|sample| { - buffer - .get(channel, sample) - .either(|x| x.is_nan(), |x| x.is_nan()) - }) + let result = plugin.on_audio_thread(|plugin| -> Result { + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + // first run, "control" run + process.audio_buffers().randomize(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + let output_control = process.audio_buffers().clone(); + + // second run, deactivate and reactivate the plugin, see if the output changes + process.restart(); + process.audio_buffers().randomize(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + let output_reactivated = process.audio_buffers().clone(); + + // third run, reset the plugin, see if the output matches the control run + process.reset(); + process.audio_buffers().randomize(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + let output_reset = process.audio_buffers().clone(); + + if !output_control.is_same(&output_reactivated) { + return Ok(TestStatus::Warning { + details: Some(String::from( + "Plugin output does not seem to be deterministic after reactivation", + )), }); - - if is_all_nans && buffer.is_same(before) { - anyhow::bail!( - "The plugin has left an output buffer (index {index}) untouched during \ - out-of-place processing." - ); - } } - // Output buffers must not contain any non-finite or denormal values - if let Some(port_idx) = buffer.output() { - let maybe_non_finite = (0..buffer.channels()) - .flat_map(|channel| (0..block_size).map(move |sample| (channel, sample))) - .find_map(|(channel, sample)| { - let x = buffer.get(channel, sample); - if x.either(|x| !x.is_finite(), |x| !x.is_finite()) { - Some((x, channel, sample)) - } else { - None - } - }); - - if let Some((sample, channel_idx, sample_idx)) = maybe_non_finite { - anyhow::bail!( - "The sample written to output port {port_idx}, channel {channel_idx}, and \ - sample index {sample_idx} is {sample}." - ); - } - - if check_denormals { - let maybe_denormal = (0..buffer.channels()) - .flat_map(|channel| (0..block_size).map(move |sample| (channel, sample))) - .find_map(|(channel, sample)| { - let x = buffer.get(channel, sample); - if x.either(|x| x.is_subnormal(), |x| x.is_subnormal()) { - Some((x, channel, sample)) - } else { - None - } - }); - - if let Some((sample, channel_idx, sample_idx)) = maybe_denormal { - anyhow::bail!( - "The sample written to output port {port_idx}, channel {channel_idx}, and \ - sample index {sample_idx} is subnormal ({sample})." - ); - } - } - } - } - - // If the plugin output any events, then they should be in a monotonically increasing order - let mut last_event_time = 0; - for event in process_data.output_events.read() { - let event_time = event.header().time; - if event_time < last_event_time { - anyhow::bail!( - "The plugin output an event for sample {event_time} after it had previously \ - output an event for sample {last_event_time}." - ) + if !output_reactivated.is_same(&output_reset) { + anyhow::bail!("Plugin output differs after reset"); } - if event_time >= block_size as u32 { - anyhow::bail!( - "The plugin output an event for sample {event_time} but the audio buffer only \ - contains {block_size} samples." - ) - } + Ok(TestStatus::Success { details: None }) + })?; - last_event_time = event_time; - } + plugin.handle_callback().context("An error occured during a callback")?; - Ok(()) + Ok(result) } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 6fc211e..c659240 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -10,10 +10,8 @@ use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::params::Params; use crate::plugin::ext::state::State; -use crate::plugin::instance::process::{ - AudioBuffers, Event, EventQueue, ProcessConfig, ProcessData, -}; use crate::plugin::library::PluginLibrary; +use crate::plugin::process::{AudioBuffers, Event, EventQueue, ProcessScope}; use crate::tests::plugin::params::param_compare_approx; use crate::tests::rng::{ParamFuzzer, new_prng}; use crate::tests::{TestCase, TestStatus}; @@ -25,8 +23,13 @@ const ACTUAL_STATE_FILE_NAME: &str = "state-actual"; /// The file name we'll use to dump parameter diffs when a test fails. const PARAM_DIFF_FILE_NAME: &str = "param-diff.csv"; +const BUFFER_SIZE: u32 = 512; + /// The test for `PluginTestCase::StateInvalidEmpty`. -pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Result { +pub fn test_state_invalid_empty( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { let plugin = library .create_plugin(plugin_id) .context("Could not create the plugin instance")?; @@ -52,8 +55,8 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res match result { Ok(_) => Ok(TestStatus::Warning { details: Some(String::from( - "The plugin returned true when 'clap_plugin_state::load()' was called when an \ - empty state, this is likely a bug.", + "The plugin returned true when 'clap_plugin_state::load()' \ + was called when an empty state, this is likely a bug.", )), }), Err(_) => Ok(TestStatus::Success { details: None }), @@ -61,7 +64,10 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res } /// The test for `PluginTestCase::StateInvalidRandom`. -pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Result { +pub fn test_state_invalid_random( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { let mut prng = new_prng(); let plugin = library @@ -101,8 +107,8 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re false => Ok(TestStatus::Success { details: None }), true => Ok(TestStatus::Warning { details: Some(String::from( - "The plugin loaded random bytes successfully, which is unexpected, but the plugin \ - did not crash.", + "The plugin loaded random bytes successfully, which is \ + unexpected, but the plugin did not crash.", )), }), } @@ -131,9 +137,9 @@ pub fn test_state_reproducibility_basic( plugin.init().context("Error during initialization")?; let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, + Some(audio_ports) => audio_ports.config().context( + "Error while querying 'audio-ports' IO configuration", + )?, None => AudioPortConfig::default(), }; @@ -181,21 +187,25 @@ pub fn test_state_reproducibility_basic( event.cookie = std::ptr::null_mut(); } event => { - panic!("Unexpected event {event:?}, this is a clap-validator bug") + panic!( + "Unexpected event {event:?}, this is a \ + clap-validator bug" + ) } } } } - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + plugin.on_audio_thread(|plugin| { + let mut buffers = AudioBuffers::new_out_of_place_f32( + &audio_ports_config, + BUFFER_SIZE, + ); + let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process_data.run_once(&plugin, move |plugin, process_data| { - process_data - .input_events - .add_events(random_param_set_events); - plugin.process(process_data)?; - Ok(()) + process.audio_buffers().randomize(&mut prng); + process.input_queue().add_events(random_param_set_events); + process.run() })?; // We'll check that the plugin has these sames values after reloading the state. These @@ -203,7 +213,9 @@ pub fn test_state_reproducibility_basic( // deserializatoin process. let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) + .map(|param_id| { + params.get(*param_id).map(|value| (*param_id, value)) + }) .collect::>>()?; let expected_state = state.save()?; @@ -245,7 +257,8 @@ pub fn test_state_reproducibility_basic( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the 'state' extension.", + "The plugin's second instance does not implement the \ + 'state' extension.", )), }); } @@ -272,15 +285,19 @@ pub fn test_state_reproducibility_basic( PluginTestCase::StateReproducibilityBasic }; - if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? - { + if let Some(diff) = generate_param_diff( + &actual_param_values, + &expected_param_values, + ¶ms, + )? { let (param_diff_file_path, mut param_diff_file) = test.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( - "After reloading the state, the plugin's parameter values do not match the old values \ - when queried through 'clap_plugin_params::get()'. \nDiff: '{}'.", + "After reloading the state, the plugin's parameter values do not \ + match the old values when queried through \ + 'clap_plugin_params::get()'. \nDiff: '{}'.", param_diff_file_path.display(), ); } @@ -305,8 +322,8 @@ pub fn test_state_reproducibility_basic( Ok(TestStatus::Warning { details: Some(format!( - "The saved state after loading differs from the original saved state. \nExpected: \ - '{}'. \nActual: '{}'.", + "The saved state after loading differs from the original \ + saved state. \nExpected: '{}'. \nActual: '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -365,7 +382,9 @@ pub fn test_state_reproducibility_flush( // implemented flush. let initial_param_values: BTreeMap = param_infos .keys() - .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) + .map(|param_id| { + params.get(*param_id).map(|value| (*param_id, value)) + }) .collect::>>()?; // The same param set events will be passed to the flush function in this pass and to the @@ -387,7 +406,9 @@ pub fn test_state_reproducibility_flush( // We'll compare against these values in that second pass let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) + .map(|param_id| { + params.get(*param_id).map(|value| (*param_id, value)) + }) .collect::>>()?; let expected_state = state.save()?; @@ -396,10 +417,13 @@ pub fn test_state_reproducibility_flush( .context("An error occured during a callback")?; // Plugins with no parameters at all should of course not trigger this error - if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { + if expected_param_values == initial_param_values + && !random_param_set_events.is_empty() + { anyhow::bail!( - "'clap_plugin_params::flush()' has been called with random parameter values, but \ - the plugin's reported parameter values have not changed." + "'clap_plugin_params::flush()' has been called with random \ + parameter values, but the plugin's reported parameter values \ + have not changed." ) } @@ -434,7 +458,8 @@ pub fn test_state_reproducibility_flush( // I sure hope that no plugin will eer hit this return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the 'params' extension.", + "The plugin's second instance does not implement the \ + 'params' extension.", )), }); } @@ -445,7 +470,8 @@ pub fn test_state_reproducibility_flush( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the 'state' extension.", + "The plugin's second instance does not implement the \ + 'state' extension.", )), }); } @@ -468,27 +494,32 @@ pub fn test_state_reproducibility_flush( .get(&event.param_id) .with_context(|| { format!( - "Expected the plugin to have a parameter with ID {}, but the \ - parameter is missing", + "Expected the plugin to have a parameter with ID \ + {}, but the parameter is missing", event.param_id, ) })? .cookie; } - event => panic!("Unexpected event {event:?}, this is a clap-validator bug"), + event => panic!( + "Unexpected event {event:?}, this is a clap-validator bug" + ), } } // In the previous pass we used flush, and here we use the process funciton - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); + plugin.on_audio_thread(|plugin| { + let mut buffers = AudioBuffers::new_out_of_place_f32( + &audio_ports_config, + BUFFER_SIZE, + ); + let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process_data.run_once(&plugin, move |plugin, process_data| { - process_data - .input_events + process.audio_buffers().randomize(&mut prng); + process + .input_queue() .add_events(new_random_param_set_events); - plugin.process(process_data)?; - Ok(()) + process.run() })?; let actual_param_values: BTreeMap = expected_param_values @@ -496,16 +527,21 @@ pub fn test_state_reproducibility_flush( .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? - { - let (param_diff_file_path, mut param_diff_file) = PluginTestCase::StateReproducibilityFlush - .temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; + if let Some(diff) = generate_param_diff( + &actual_param_values, + &expected_param_values, + ¶ms, + )? { + let (param_diff_file_path, mut param_diff_file) = + PluginTestCase::StateReproducibilityFlush + .temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( - "Setting the same parameter values through 'clap_plugin_params::flush()' and through \ - the process function results in different reported values when queried through \ + "Setting the same parameter values through \ + 'clap_plugin_params::flush()' and through the process function \ + results in different reported values when queried through \ 'clap_plugin_params::get_value()'. \nDiff: '{}'.", param_diff_file_path.display(), ); @@ -532,8 +568,9 @@ pub fn test_state_reproducibility_flush( Ok(TestStatus::Warning { details: Some(format!( - "Sending the same parameter values to two different instances of the plugin \ - resulted in different state files. \nExpected: '{}'. \nActual: '{}'.", + "Sending the same parameter values to two different instances \ + of the plugin resulted in different state files. \nExpected: \ + '{}'. \nActual: '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -542,7 +579,10 @@ pub fn test_state_reproducibility_flush( } /// The test for `PluginTestCase::StateBufferedStreams`. -pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> Result { +pub fn test_state_buffered_streams( + library: &PluginLibrary, + plugin_id: &str, +) -> Result { let mut prng = new_prng(); let plugin = library @@ -553,9 +593,9 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> plugin.init().context("Error during initialization")?; let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports - .config() - .context("Error while querying 'audio-ports' IO configuration")?, + Some(audio_ports) => audio_ports.config().context( + "Error while querying 'audio-ports' IO configuration", + )?, None => AudioPortConfig::default(), }; let params = match plugin.get_extension::() { @@ -586,19 +626,23 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - let mut process_data = ProcessData::new(&mut audio_buffers, ProcessConfig::default()); - process_data.run_once(&plugin, move |plugin, process_data| { - process_data - .input_events - .add_events(random_param_set_events); - plugin.process(process_data)?; - Ok(()) + plugin.on_audio_thread(|plugin| { + let mut buffers = AudioBuffers::new_out_of_place_f32( + &audio_ports_config, + BUFFER_SIZE, + ); + let mut process = ProcessScope::new(&plugin, &mut buffers)?; + + process.audio_buffers().randomize(&mut prng); + process.input_queue().add_events(random_param_set_events); + process.run() })?; let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) + .map(|param_id| { + params.get(*param_id).map(|value| (*param_id, value)) + }) .collect::>>()?; // This state file is saved without buffered writes. It's expected that the plugin @@ -629,7 +673,8 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the 'params' extension.", + "The plugin's second instance does not implement the \ + 'params' extension.", )), }); } @@ -640,7 +685,8 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the 'state' extension.", + "The plugin's second instance does not implement the \ + 'state' extension.", )), }); } @@ -662,17 +708,22 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? - { + if let Some(diff) = generate_param_diff( + &actual_param_values, + &expected_param_values, + ¶ms, + )? { let (param_diff_file_path, mut param_diff_file) = - PluginTestCase::StateBufferedStreams.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; + PluginTestCase::StateBufferedStreams + .temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( "After reloading the state by allowing the plugin to read at most \ - {BUFFERED_LOAD_MAX_BYTES} bytes at a time, the plugin's parameter values do not \ - match the old values when queried through 'clap_plugin_params::get()'. \nDiff: '{}'.", + {BUFFERED_LOAD_MAX_BYTES} bytes at a time, the plugin's \ + parameter values do not match the old values when queried \ + through 'clap_plugin_params::get()'. \nDiff: '{}'.", param_diff_file_path.display() ); } @@ -691,19 +742,22 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let (expected_state_file_path, mut expected_state_file) = PluginTestCase::StateBufferedStreams .temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; - let (actual_state_file_path, mut actual_state_file) = PluginTestCase::StateBufferedStreams - .temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; + let (actual_state_file_path, mut actual_state_file) = + PluginTestCase::StateBufferedStreams + .temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; Ok(TestStatus::Warning { details: Some(format!( - "Re-saving the loaded state resulted in a different state file. The original \ - state file being compared to was written unbuffered, reloaded by allowing the \ - plugin to read only {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written \ - again by allowing the plugin to write only {BUFFERED_SAVE_MAX_BYTES} bytes at a \ - time.\n Expected: '{}'.\n Actual: '{}'.", + "Re-saving the loaded state resulted in a different state \ + file. The original state file being compared to was written \ + unbuffered, reloaded by allowing the plugin to read only \ + {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written \ + again by allowing the plugin to write only \ + {BUFFERED_SAVE_MAX_BYTES} bytes at a time.\n Expected: \ + '{}'.\n Actual: '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -741,7 +795,12 @@ fn generate_param_diff( Some(format!( "{}, {:?}, {:?}, {:.4}, {:?}, {:.4}", - param_id, param_name, string_actual, actual_value, string_expected, expected_value, + param_id, + param_name, + string_actual, + actual_value, + string_expected, + expected_value, )) }) .collect::>(); @@ -750,8 +809,8 @@ fn generate_param_diff( Ok(None) } else { Ok(Some(format!( - "param-id, param-name, actual-string, actual-value, expected-string, \ - expected-value\n{}", + "param-id, param-name, actual-string, actual-value, \ + expected-string, expected-value\n{}", diff.join("\n") ))) } diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 5b2bdfa..f795d21 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -2,9 +2,9 @@ use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::preset_load::PresetLoad; -use crate::plugin::instance::process::{AudioBuffers, ProcessConfig, ProcessData}; use crate::plugin::library::PluginLibrary; use crate::plugin::preset_discovery::{LocationValue, PluginAbi, Preset, PresetFile}; +use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::CLAP_PRESET_DISCOVERY_FACTORY_ID; @@ -173,10 +173,10 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // We'll process a single buffer of silent audio just to make sure everything's // settled in - ProcessData::new(&mut audio_buffers, ProcessConfig::default()) - .run_once(&plugin, move |plugin, data| { - plugin.process(data)?; - Ok(()) + plugin + .on_audio_thread(|plugin| { + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + process.run() }) .with_context(|| { format!( diff --git a/src/tests/rng.rs b/src/tests/rng.rs index d5c1e28..324953a 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -1,22 +1,15 @@ //! Utilities for generating pseudo-random data. -use clap_sys::events::{ - CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_IS_LIVE, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_CHOKE, - CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_VALUE, - CLAP_NOTE_EXPRESSION_PRESSURE, CLAP_NOTE_EXPRESSION_TUNING, CLAP_NOTE_EXPRESSION_VOLUME, - clap_event_header, clap_event_midi, clap_event_note, clap_event_note_expression, - clap_event_param_value, -}; +use crate::plugin::ext::note_ports::NotePortConfig; +use crate::plugin::ext::params::{Param, ParamInfo}; +use crate::plugin::process::{Event, EventQueue}; +use clap_sys::events::*; use midi_consts::channel_event as midi; use rand::Rng; use rand::seq::IteratorRandom; use rand_pcg::Pcg32; use std::ops::RangeInclusive; -use crate::plugin::ext::note_ports::NotePortConfig; -use crate::plugin::ext::params::{Param, ParamInfo}; -use crate::plugin::instance::process::{Event, EventQueue}; - /// Create a new pseudo-random number generator with a fixed seed. pub fn new_prng() -> Pcg32 { Pcg32::new(1337, 420) @@ -131,66 +124,26 @@ impl<'a> NoteGenerator<'a> { /// Fill an event queue with random events for the next `num_samples` samples. This does not /// clear the event queue. If the queue was not empty, then this will do a stable sort after /// inserting _all_ events. - pub fn fill_event_queue(&mut self, prng: &mut Pcg32, queue: &EventQueue, num_samples: u32) { + pub fn generate_events(&mut self, prng: &mut Pcg32, num_samples: u32) -> Vec { let mut events = vec![]; let mut sample = prng.random_range(self.sample_offset_range.clone()).max(0) as u32; + while sample < num_samples { - let Some(event) = self.generate(prng, sample) else { - return; + let Some(event) = self.generate_event(prng, sample) else { + break; }; events.push(event); sample += prng.random_range(self.sample_offset_range.clone()).max(0) as u32; } - queue.add_events(events); - } - - #[allow(unused)] - pub fn stop_all_voices(&mut self, queue: &EventQueue, time_offset: u32) { - let mut events = vec![]; - for (note_port_idx, active_notes) in self.active_notes.drain(..).enumerate() { - let supports_clap = self.config.inputs[note_port_idx].supports_clap(); - - for note in active_notes { - if supports_clap { - events.push(Event::Note(clap_event_note { - header: clap_event_header { - size: std::mem::size_of::() as u32, - time: time_offset, - space_id: CLAP_CORE_EVENT_SPACE_ID, - type_: CLAP_EVENT_NOTE_OFF, - flags: 0, - }, - note_id: note.note_id, - port_index: note_port_idx as i16, - channel: note.channel, - key: note.key, - velocity: 0.0, - })); - } else { - events.push(Event::Midi(clap_event_midi { - header: clap_event_header { - size: std::mem::size_of::() as u32, - time: time_offset, - space_id: CLAP_CORE_EVENT_SPACE_ID, - type_: CLAP_EVENT_MIDI, - flags: 0, - }, - port_index: note_port_idx as u16, - data: [midi::NOTE_OFF | note.channel as u8, note.key as u8, 0], - })); - } - } - } - - queue.add_events(events); + events } /// Generate a random note event for one of the plugin's note ports depending on the port's /// capabilities. Returns an error if the plugin doesn't have any note ports or if the note /// ports don't support either MIDI or CLAP note events. - pub fn generate(&mut self, prng: &mut Pcg32, time_offset: u32) -> Option { + pub fn generate_event(&mut self, prng: &mut Pcg32, time_offset: u32) -> Option { if self.config.inputs.is_empty() { return None; } @@ -597,6 +550,48 @@ impl<'a> NoteGenerator<'a> { ); } + #[allow(unused)] + pub fn stop_all_voices(&mut self, queue: &EventQueue, time_offset: u32) { + let mut events = vec![]; + for (note_port_idx, active_notes) in self.active_notes.drain(..).enumerate() { + let supports_clap = self.config.inputs[note_port_idx].supports_clap(); + + for note in active_notes { + if supports_clap { + events.push(Event::Note(clap_event_note { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_NOTE_OFF, + flags: 0, + }, + note_id: note.note_id, + port_index: note_port_idx as i16, + channel: note.channel, + key: note.key, + velocity: 0.0, + })); + } else { + events.push(Event::Midi(clap_event_midi { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_MIDI, + flags: 0, + }, + port_index: note_port_idx as u16, + data: [midi::NOTE_OFF | note.channel as u8, note.key as u8, 0], + })); + } + } + } + + queue.add_events(events); + } + + #[allow(unused)] pub fn reset(&mut self) { self.next_note_id = 0; for active_notes in &mut self.active_notes { @@ -672,7 +667,7 @@ impl<'a> ParamFuzzer<'a> { } } - pub fn with_snap_to_bounds(mut self) -> Self { + pub fn snap_to_bounds(mut self) -> Self { self.snap_to_bounds = true; self } @@ -683,23 +678,23 @@ impl<'a> ParamFuzzer<'a> { /// /// Unlike [`ParamFuzzer::randomize_params_at`], this generates [`Event::ParamMod`] events as well as /// generating events at random irregular unsynchronized (between different parameters) intervals. - pub fn fill_event_queue(&'a self, prng: &'a mut Pcg32, queue: &EventQueue, num_samples: u32) { + pub fn generate_events(&self, prng: &mut Pcg32, num_samples: u32) -> Vec { let mut events = vec![]; let mut sample = prng.random_range(self.sample_offset_range.clone()).max(0) as u32; while sample < num_samples { - let Some(event) = self.generate(prng) else { - return; + let Some(event) = self.generate_event(prng) else { + break; }; events.push(event); sample += prng.random_range(self.sample_offset_range.clone()).max(0) as u32; } - queue.add_events(events); + events } /// Generate a single random parameter change event for one of the plugin's parameters. - pub fn generate(&'a self, prng: &'a mut Pcg32) -> Option { + pub fn generate_event(&self, prng: &mut Pcg32) -> Option { let (param_id, param_info) = self .params .iter() diff --git a/src/util.rs b/src/util.rs index 1815ed8..2b955ff 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,9 +4,10 @@ use anyhow::{Context, Result}; use chrono::{DateTime, TimeZone, Utc}; use clap_sys::timestamp::{CLAP_TIMESTAMP_UNKNOWN, clap_timestamp}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::ffi::CStr; +use std::ffi::CString; use std::os::raw::c_char; use std::path::PathBuf; +use std::{ffi::CStr, sync::OnceLock}; /// Assert that the specified pointers are non-null. Panics if this is not the case. macro_rules! check_null_ptr { @@ -161,8 +162,33 @@ pub fn validator_temp_dir() -> PathBuf { temp_dir().join("clap-validator") } +pub fn validator_version() -> &'static CStr { + static VERSION: OnceLock = OnceLock::new(); + VERSION + .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).unwrap()) + .as_c_str() +} + +/// A helper struct used to send stuff across thread boundary. +pub struct AssertSendSync(T); + +impl AssertSendSync { + pub unsafe fn new(value: T) -> Self { + AssertSendSync(value) + } + + pub fn get(self) -> T { + self.0 + } +} + +unsafe impl Send for AssertSendSync {} +unsafe impl Sync for AssertSendSync {} + impl IteratorExt for T where T: Iterator {} pub trait IteratorExt: Iterator { + /// Map the iterator in parallel if `parallel` is `true`, or sequentially if it is `false`. + /// Returns an iterator over the mapped values, in arbitrary order. fn map_parallel( self, parallel: bool, From fc143bf99ebc37c06f5882f01e941407c9eb2b7b Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 29 Jan 2026 16:26:12 +0400 Subject: [PATCH 044/114] big refactor (2) --- src/main.rs | 10 +- src/plugin/instance.rs | 788 +--------------------------- src/plugin/instance/audio_thread.rs | 85 ++- src/plugin/instance/main_thread.rs | 262 +++++++++ src/plugin/instance/shared.rs | 534 +++++++++++++++++++ src/plugin/library.rs | 12 +- src/plugin/process.rs | 13 +- src/plugin/process/buffer.rs | 23 +- src/plugin/process/transport.rs | 26 +- src/util.rs | 22 +- 10 files changed, 890 insertions(+), 885 deletions(-) create mode 100644 src/plugin/instance/main_thread.rs create mode 100644 src/plugin/instance/shared.rs diff --git a/src/main.rs b/src/main.rs index 7086492..e074f28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ use clap::{Parser, Subcommand, ValueEnum}; use std::process::ExitCode; -use crate::plugin::library::mark_current_thread_as_os_main_thread; - mod commands; mod index; mod plugin; @@ -53,8 +51,6 @@ enum Command { } fn main() -> ExitCode { - mark_current_thread_as_os_main_thread(); - let cli = Cli::parse(); // For now logging everything to the terminal is fine. In the future it may be useful to have @@ -76,8 +72,14 @@ fn main() -> ExitCode { simplelog::ColorChoice::Auto, ) .expect("Could not initialize logger"); + log_panics::init(); + // Mark the main thread as such for plugin instance creation checks. + unsafe { + plugin::instance::mark_current_thread_as_os_main_thread(); + } + let result = match cli.command { Command::Validate(settings) => commands::validate::validate(cli.verbosity, &settings), Command::RunSingleTest(settings) => commands::validate::run_single(&settings), diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index d1798e6..818103c 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -1,47 +1,21 @@ //! Abstractions for single CLAP plugin instances for main thread interactions. -use super::ext::Extension; -use super::library::{PluginLibrary, PluginMetadata}; -use crate::plugin::preset_discovery::LocationValue; -use crate::util::{self, AssertSendSync, check_null_ptr, clap_call, validator_version}; -use anyhow::{Context, Result}; -use clap_sys::ext::audio_ports::{CLAP_AUDIO_PORTS_RESCAN_NAMES, CLAP_EXT_AUDIO_PORTS, clap_host_audio_ports}; -use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_host_latency}; -use clap_sys::ext::note_ports::{ - CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, CLAP_NOTE_DIALECT_MIDI_MPE, - CLAP_NOTE_PORTS_RESCAN_ALL, CLAP_NOTE_PORTS_RESCAN_NAMES, clap_host_note_ports, clap_note_dialect, -}; -use clap_sys::ext::params::{ - CLAP_EXT_PARAMS, CLAP_PARAM_RESCAN_ALL, CLAP_PARAM_RESCAN_INFO, CLAP_PARAM_RESCAN_TEXT, CLAP_PARAM_RESCAN_VALUES, - clap_host_params, clap_param_clear_flags, clap_param_rescan_flags, -}; -use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; -use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; -use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_host_tail}; -use clap_sys::ext::thread_check::{CLAP_EXT_THREAD_CHECK, clap_host_thread_check}; -use clap_sys::ext::voice_info::{CLAP_EXT_VOICE_INFO, clap_host_voice_info}; -use clap_sys::factory::plugin_factory::clap_plugin_factory; -use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; -use clap_sys::host::clap_host; -use clap_sys::id::clap_id; -use clap_sys::plugin::clap_plugin; -use clap_sys::version::CLAP_VERSION; -use crossbeam_utils::atomic::AtomicCell; -use std::ffi::{CStr, c_char, c_void}; -use std::marker::PhantomData; -use std::panic::resume_unwind; -use std::pin::Pin; -use std::ptr::NonNull; -use std::sync::mpsc::{Receiver, Sender, channel}; -use std::sync::{Arc, Mutex}; -use std::thread::ThreadId; - mod audio_thread; +mod main_thread; +mod shared; + pub use audio_thread::*; +pub use main_thread::*; +pub use shared::*; +/// An event generated by plugin->host callbacks. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum CallbackEvent { + /// clap_plugin::request_process() RequestProcess, + + /// clap_plugin_params::request_flush() RequestFlush, ParamsRescanValues, @@ -54,9 +28,16 @@ pub enum CallbackEvent { NotePortsRescanNames, NotePortsRescanAll, + /// clap_plugin_latency::changed() LatencyChanged, + + /// clap_plugin_tail::changed() TailChanged, + + /// clap_plugin_voice_info::changed() VoiceInfoChanged, + + /// clap_plugin_state::mark_dirty() StateMarkDirty, } @@ -119,738 +100,3 @@ impl PluginStatus { } } } - -/// A CLAP plugin instance. The plugin will be deinitialized when this object is dropped. All -/// functions here are callable only from the main thread. Use the -/// [`on_audio_thread()`][Self::on_audio_thread()] method to spawn an audio thread. -/// -/// All functions on `Plugin` and the objects created from it will panic if the plugin is not in the -/// correct state. -pub struct Plugin<'lib> { - handle: NonNull, - - /// Information about this plugin instance stored on the host. This keeps track of things like - /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - shared: Pin>, - - main: InstanceMainThread, - - /// The CLAP plugin library this plugin instance was created from. This field is not used - /// directly, but keeping a reference to the library here prevents the plugin instance from - /// outliving the library. - _library: PhantomData<&'lib PluginLibrary>, - - /// To honor CLAP's thread safety guidelines, the thread this object was created from is - /// designated the 'main thread', and this object cannot be shared with other threads. The - /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call - /// the plugin's audio thread functions. - _thread: PhantomData<*const ()>, -} - -impl Drop for Plugin<'_> { - fn drop(&mut self) { - if let Some(error) = self.shared.callback_error.lock().unwrap().take() { - log::warn!( - "The validator's host has detected a callback error but this error has not been used as part of the \ - test result. This could be a clap-validator bug. The error message is: {error}" - ) - } - - // Make sure the plugin is in the correct state before it gets destroyed - match self.status() { - PluginStatus::Uninitialized | PluginStatus::Deactivated => (), - PluginStatus::Activated => self.deactivate(), - status => panic!( - "The plugin was in an invalid state '{status:?}' when the instance got dropped, this is a \ - clap-validator bug" - ), - } - - self.handle_callback_unchecked(); - - let plugin = self.as_ptr(); - unsafe { - clap_call! { plugin=>destroy(plugin) } - } - } -} - -impl<'lib> Plugin<'lib> { - /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the - /// plugin could not be created. The plugin instance will be registered with the host, and - /// unregistered when this object is dropped again. - /// - /// # Safety - /// This MUST be called on the OS main thread (if applicable). - pub(crate) unsafe fn new(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result { - let (shared, main) = InstanceShared::new(); - let plugin = unsafe { - clap_call! { - factory=>create_plugin(factory, shared.clap_host_ptr(), plugin_id.as_ptr()) - } - }; - - if plugin.is_null() { - anyhow::bail!("'clap_plugin_factory::create_plugin({plugin_id:?})' returned a null pointer."); - } - - Ok(Plugin { - handle: NonNull::new(plugin as *mut clap_plugin).unwrap(), - shared, - main, - - _library: PhantomData, - _thread: PhantomData, - }) - } - - /// Get the raw pointer to the `clap_plugin` instance. - pub fn as_ptr(&self) -> *const clap_plugin { - self.handle.as_ptr() - } - - /// Get this plugin's metadata descriptor. In theory this should be the same as the one - /// retrieved from the factory earlier. - pub fn descriptor(&self) -> Result { - let plugin = self.as_ptr(); - let descriptor = unsafe { (*plugin).desc }; - if descriptor.is_null() { - anyhow::bail!("The 'desc' field on the 'clap_plugin' struct is a null pointer."); - } - - PluginMetadata::from_descriptor(unsafe { &*descriptor }) - } - - /// The plugin's current initialization status. - pub fn status(&self) -> PluginStatus { - self.shared.status.load() - } - - pub fn shared(&self) -> &Pin> { - &self.shared - } - - /// Handle any pending main-thread callbacks for this plugin. - /// Returns an error if there is a callback error pending. - pub fn handle_callback(&self) -> Result<()> { - self.handle_callback_unchecked(); - - if let Some(error) = self.shared.callback_error.lock().unwrap().take() { - anyhow::bail!(error); - } - - // TODO: - // while let Ok(event) = self.shared.callback_receiver.lock().unwrap().recv() { - // println!("{:?}", event); - // } - - Ok(()) - } - - /// Get the _main thread_ extension abstraction for the extension `T`, if the plugin supports - /// this extension. Returns `None` if it does not. The plugin needs to be initialized using - /// [`init()`][Self::init()] before this may be called. - pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { - self.status().assert_is_not(PluginStatus::Uninitialized); - - let plugin = self.as_ptr(); - for id in T::IDS { - let extension_ptr = unsafe { - clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } - }; - - if !extension_ptr.is_null() { - return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; - } - } - - None - } - - /// Execute some code for this plugin from an audio thread context. The closure receives a - /// [`PluginAudioThread`], which disallows calling main thread functions, and permits calling - /// audio thread functions. - /// - /// If whatever happens on the audio thread caused main-thread callback requests to be emited, - /// then those will be handled concurrently. - pub fn on_audio_thread T + Send>(&self, f: F) -> T { - let plugin = unsafe { AssertSendSync::new(self) }; - - let result = std::thread::scope(|s| { - let thread = s.spawn(|| f(PluginAudioThread::new(plugin.get()))); - - // Handle callbacks requests on the main thread while the audio thread is running - while let Ok(task) = self.main.task_receiver.recv() { - match task { - MainThreadTask::Closure(closure) => closure(self), - MainThreadTask::CallbackRequest => self.handle_callback_unchecked(), - MainThreadTask::StopAudioThread => break, - } - } - - thread.join().unwrap_or_else(|panic_info| resume_unwind(panic_info)) - }); - - self.handle_callback_unchecked(); - result - } - - /// Initialize the plugin. This needs to be called before doing anything else. - pub fn init(&self) -> Result<()> { - self.status().assert_is(PluginStatus::Uninitialized); - - let plugin = self.as_ptr(); - let result = unsafe { - clap_call! { plugin=>init(plugin) } - }; - - if result { - // If the plugin never calls `request_callback`, the validator won't catch this - anyhow::ensure!( - unsafe { (*plugin).on_main_thread.is_some() }, - "clap_plugin::on_main_thread is null" - ); - - self.shared.status.store(PluginStatus::Deactivated); - Ok(()) - } else { - anyhow::bail!("'clap_plugin::init()' returned false.") - } - } - - /// Activate the plugin. Returns an error if the plugin returned `false`. See - /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the - /// preconditions. - pub fn activate(&self, sample_rate: f64, min_buffer_size: u32, max_buffer_size: u32) -> Result<()> { - self.status().assert_is(PluginStatus::Deactivated); - - // Apparently 0 is invalid here - assert!(min_buffer_size >= 1); - assert!(max_buffer_size >= min_buffer_size); - - // we need to track the `Activating` state to validate that we call clap_host_latency::changed only within the activation call. - self.shared.status.store(PluginStatus::Activating); - - let plugin = self.as_ptr(); - let result = unsafe { - clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size, max_buffer_size) } - }; - - if result { - self.shared.status.store(PluginStatus::Activated); - Ok(()) - } else { - self.shared.status.store(PluginStatus::Deactivated); - anyhow::bail!("'clap_plugin::activate()' returned false.") - } - } - - /// Deactivate the plugin. See - /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the - /// preconditions. - pub fn deactivate(&self) { - self.status().assert_is(PluginStatus::Activated); - - let plugin = self.as_ptr(); - unsafe { - clap_call! { plugin=>deactivate(plugin) } - } - - self.shared.status.store(PluginStatus::Deactivated); - } - - fn handle_callback_unchecked(&self) { - if self.shared.requested_callback.swap(false) { - let plugin = self.as_ptr(); - unsafe { - clap_call! { plugin=>on_main_thread(plugin) } - }; - } - } -} - -pub enum MainThreadTask { - Closure(Box), - CallbackRequest, - StopAudioThread, -} - -/// Runtime information about a plugin instance. This keeps track of pending callbacks and things -/// like audio threads. It also contains the plugin's unique `clap_host` struct so host callbacks -/// can be linked back to this specific plugin instance. -pub struct InstanceShared { - pub task_sender: Sender, - pub callback_sender: Sender, - pub callback_error: Mutex>, - - /// The plugin's current state in terms of activation and processing status. - pub status: AtomicCell, - - /// The plugin instance's main thread. Used for the main thread checks. - pub main_thread_id: ThreadId, - - /// The plugin instance's audio thread, if it has one. Used for the audio thread checks. - pub audio_thread_id: AtomicCell>, - - /// Whether the plugin has called `clap_host::request_callback()` and expects - /// `clap_plugin::on_main_thread()` to be called on the main thread. - pub requested_callback: AtomicCell, - - /// Whether the plugin has called `clap_host::request_restart()` and expects the plugin to be - /// deactivated and subsequently reactivated. - pub requested_restart: AtomicCell, - - clap_host: clap_host, - clap_host_audio_ports: clap_host_audio_ports, - clap_host_note_ports: clap_host_note_ports, - clap_host_params: clap_host_params, - clap_host_preset_load: clap_host_preset_load, - clap_host_state: clap_host_state, - clap_host_thread_check: clap_host_thread_check, - clap_host_latency: clap_host_latency, - clap_host_tail: clap_host_tail, - clap_host_voice_info: clap_host_voice_info, -} - -struct InstanceMainThread { - callback_receiver: Receiver, - task_receiver: Receiver, -} - -impl InstanceShared { - fn new() -> (Pin>, InstanceMainThread) { - let main_thread = std::thread::current().id(); - let (callback_sender, callback_receiver) = channel(); - let (task_sender, task_receiver) = channel(); - - let shared = Arc::pin(InstanceShared { - task_sender, - callback_sender, - callback_error: Mutex::new(None), - - status: AtomicCell::new(PluginStatus::Uninitialized), - main_thread_id: main_thread, - audio_thread_id: AtomicCell::new(None), - requested_callback: AtomicCell::new(false), - requested_restart: AtomicCell::new(false), - - clap_host: clap_host { - clap_version: CLAP_VERSION, - // This is populated with a pointer to the `Arc`'s data after creating the Arc - host_data: std::ptr::null_mut(), - name: c"clap-validator".as_ptr(), - vendor: c"Robbert van der Helm".as_ptr(), - url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: validator_version().as_ptr(), - get_extension: Some(Self::get_extension), - request_restart: Some(Self::request_restart), - request_process: Some(Self::request_process), - request_callback: Some(Self::request_callback), - }, - - clap_host_audio_ports: clap_host_audio_ports { - is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), - rescan: Some(Self::ext_audio_ports_rescan), - }, - clap_host_note_ports: clap_host_note_ports { - supported_dialects: Some(Self::ext_note_ports_supported_dialects), - rescan: Some(Self::ext_note_ports_rescan), - }, - clap_host_preset_load: clap_host_preset_load { - on_error: Some(Self::ext_preset_load_on_error), - loaded: Some(Self::ext_preset_load_loaded), - }, - clap_host_params: clap_host_params { - rescan: Some(Self::ext_params_rescan), - clear: Some(Self::ext_params_clear), - request_flush: Some(Self::ext_params_request_flush), - }, - clap_host_state: clap_host_state { - mark_dirty: Some(Self::ext_state_mark_dirty), - }, - clap_host_thread_check: clap_host_thread_check { - is_main_thread: Some(Self::ext_thread_check_is_main_thread), - is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), - }, - clap_host_latency: clap_host_latency { - changed: Some(Self::ext_latency_changed), - }, - clap_host_tail: clap_host_tail { - changed: Some(Self::ext_tail_changed), - }, - clap_host_voice_info: clap_host_voice_info { - changed: Some(Self::ext_voice_info_changed), - }, - }); - - let main = InstanceMainThread { - callback_receiver, - task_receiver, - }; - - // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct - // so it can be retrieved in host callbacks - unsafe { - (&raw const shared.clap_host.host_data) - .cast_mut() - .write(&*shared as *const _ as *mut std::ffi::c_void); - } - - (shared, main) - } - - pub fn clap_host_ptr(&self) -> *const clap_host { - &self.clap_host as *const clap_host - } - - #[track_caller] - pub unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { - unsafe { - let state = (*host).host_data as *const InstanceShared; - &*state - } - } - - /// Set the callback error field if it does not already contain a value. Earlier errors are not - /// overwritten. - fn set_callback_error(&self, error: impl Into) { - let mut guard = self.callback_error.lock().unwrap(); - if guard.is_none() { - *guard = Some(error.into()); - } - } - - /// Checks whether this is the main thread. If it is not, then an error indicating this can be - /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - fn assert_main_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if current_thread_id != self.main_thread_id { - self.set_callback_error(format!( - "'{}' may only be called from the main thread (thread {:?}), but it was called from thread {:?}.", - function_name, self.main_thread_id, current_thread_id - )); - } - } - - /// Checks whether this is the audio thread. If it is not, then an error indicating this can be - /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - fn assert_audio_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if self.audio_thread_id.load() != Some(current_thread_id) { - if current_thread_id == self.main_thread_id { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called from the main \ - thread." - )); - } else { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called from an unknown \ - thread." - )); - } - } - } - - /// Checks whether this is **not** the audio thread. If it is, then an error indicating this can - /// be retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread - /// safety errors will not overwrite earlier ones. - fn assert_not_audio_thread(&self, function_name: &str) { - let current_thread_id = std::thread::current().id(); - if self.audio_thread_id.load() == Some(current_thread_id) { - self.set_callback_error(format!( - "'{function_name}' was called from an audio thread, this is not allowed.", - )); - } - } - - unsafe extern "C" fn get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { - //check_null_ptr!(host, (*host).host_data, extension_id); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - // Right now there's no way to have the host only expose certain extensions. We can always - // add that when test cases need it. - let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; - if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { - &this.clap_host_audio_ports as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { - &this.clap_host_note_ports as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { - &this.clap_host_preset_load as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PARAMS { - &this.clap_host_params as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_STATE { - &this.clap_host_state as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { - &this.clap_host_thread_check as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_LATENCY { - &this.clap_host_latency as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_TAIL { - &this.clap_host_tail as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { - &this.clap_host_voice_info as *const _ as *const c_void - } else { - std::ptr::null() - } - } - - unsafe extern "C" fn request_restart(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and - // in the multi-iteration run function it will trigger a deactivate->reactivate cycle - log::trace!("'clap_host::request_restart()' was called by the plugin, setting the flag"); - this.requested_restart.store(true); - } - - unsafe extern "C" fn request_process(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - // Handling this within the context of the validator would be a bit messy. Do plugins use - // this? - log::trace!("'clap_host::request_process()' was called by the plugin"); - this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); - } - - unsafe extern "C" fn request_callback(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - // This this is either handled by `handle_callbacks_blocking()` while the audio thread is - // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the - // callback is not handled before the plugin is destroyed. - log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); - this.requested_callback.store(true); - this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); - } - - unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); - log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); - true - } - - unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_audio_ports::rescan()"); - log::trace!("'clap_host_audio_ports::rescan()' was called"); - - if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); - } - - if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - if this.status.load() > PluginStatus::Activated { - this.set_callback_error("'clap_host_audio_ports::rescan()' was called while the plugin was activated"); - } - - this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); - } - } - - unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_note_ports::supported_dialects()"); - log::trace!("'clap_host_note_ports::supported_dialects()' was called"); - - CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE - } - - unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_note_ports::rescan()"); - log::trace!("'clap_host_note_ports::rescan()' was called"); - - if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { - this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); - } - - if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { - if this.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while the plugin was \ - activated", - ); - } - - this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); - } - } - - unsafe extern "C" fn ext_preset_load_on_error( - host: *const clap_host, - location_kind: clap_preset_discovery_location_kind, - location: *const c_char, - load_key: *const c_char, - os_error: i32, - msg: *const c_char, - ) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_preset_load::on_error()"); - - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } - .context("'clap_host_preset_load::on_error()' called with an invalid load_key parameter"); - let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } - .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); - match (location, load_key, msg) { - (Ok(location), Ok(Some(load_key)), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with load key {load_key}, OS error \ - code {os_error}, and the following error message: {msg}" - )); - } - (Ok(location), Ok(None), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with no load key, OS error code \ - {os_error}, and the following error message: {msg}" - )); - } - (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - } - } - } - - unsafe extern "C" fn ext_preset_load_loaded( - host: *const clap_host, - location_kind: clap_preset_discovery_location_kind, - location: *const c_char, - load_key: *const c_char, - ) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_preset_load::loaded()"); - - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } - .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); - - match (location, load_key) { - (Ok(_location), Ok(_load_key)) => { - log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); - } - (Err(err), _) | (_, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - } - } - } - - unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_params::rescan()"); - log::trace!("'clap_host_params::rescan()' was called"); - - if flags & CLAP_PARAM_RESCAN_VALUES != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); - } - - if flags & CLAP_PARAM_RESCAN_TEXT != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanText).unwrap(); - } - - if flags & CLAP_PARAM_RESCAN_INFO != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanInfo).unwrap(); - } - - if flags & CLAP_PARAM_RESCAN_ALL != 0 { - if this.status.load() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is activated", - ); - } - - this.callback_sender.send(CallbackEvent::ParamsRescanAll).unwrap(); - } - } - - unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_params::clear()"); - log::debug!("TODO: Handle 'clap_host_params::clear()'"); - } - - unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_not_audio_thread("clap_host_params::request_flush()"); - log::trace!("'clap_host_params::request_flush()' was called"); - this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); - } - - unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_state::mark_dirty()"); - log::trace!("'clap_host_state::mark_dirty()' was called"); - this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); - } - - unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - this.main_thread_id == std::thread::current().id() - } - - unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - this.audio_thread_id.load() == Some(std::thread::current().id()) - } - - unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - if this.status.load() != PluginStatus::Activating { - this.set_callback_error( - "'clap_host_latency::changed()' must only be called within 'clap_plugin::activate()'", - ); - } - - this.assert_main_thread("clap_host_latency::changed()"); - log::trace!("'clap_host_latency::changed()' was called"); - this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); - } - - unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_audio_thread("clap_host_tail::changed()"); - log::trace!("'clap_host_tail::changed()' was called"); - this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); - } - - unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_voice_info::changed()"); - log::trace!("'clap_host_voice_info::changed()' was called"); - this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); - } -} diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index ab1b9b4..ccffc08 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -3,7 +3,7 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; use crate::plugin::instance::{InstanceShared, MainThreadTask}; -use crate::util::{AssertSendSync, clap_call}; +use crate::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; use clap_sys::process::{ @@ -18,10 +18,11 @@ use std::sync::{Arc, Condvar, Mutex}; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. pub struct PluginAudioThread<'a> { - /// The plugin instance this audio thread belongs to. This is needed to ensure that the audio - /// thread instance cannot outlive the plugin instance (which cannot outlive the plugin - /// library). This `Plugin` also contains a reference to the plugin instance's state. - pub(super) plugin: &'a Plugin<'a>, + /// Information about this plugin instance stored on the host. This keeps track of things like + /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. + shared: Pin>, + + _plugin_marker: PhantomData<&'a Plugin<'a>>, /// To honor CLAP's thread safety guidelines, the thread this object was created from is /// designated the 'audio thread', and this object cannot be shared with other threads. @@ -40,38 +41,34 @@ pub enum ProcessStatus { impl Drop for PluginAudioThread<'_> { fn drop(&mut self) { - self.plugin.shared.audio_thread_id.store(None); - self.plugin - .shared - .task_sender - .send(MainThreadTask::StopAudioThread) - .unwrap(); + self.shared.audio_thread_id.store(None); + self.shared.task_sender.send(MainThreadTask::StopAudioThread).unwrap(); } } impl<'a> PluginAudioThread<'a> { - pub(crate) fn new(plugin: &'a Plugin<'a>) -> Self { - plugin.shared.audio_thread_id.store(Some(std::thread::current().id())); - - Self { - plugin, + pub(super) fn new(shared: Pin>) -> PluginAudioThread<'a> { + shared.audio_thread_id.store(Some(std::thread::current().id())); + PluginAudioThread { + shared, + _plugin_marker: PhantomData, _send_sync_marker: PhantomData, } } /// Get the raw pointer to the `clap_plugin` instance. pub fn as_ptr(&self) -> *const clap_plugin { - self.plugin.as_ptr() + self.shared.clap_plugin_ptr() } /// Get the plugin's current initialization status. pub fn status(&self) -> PluginStatus { - self.plugin.status() + self.shared.status.load() } /// Get a reference to the plugin's shared state. pub fn shared(&self) -> &Pin> { - &self.plugin.shared + &self.shared } /// Get the _audio thread_ extension abstraction for the extension `T`, if the plugin supports @@ -98,47 +95,39 @@ impl<'a> PluginAudioThread<'a> { /// Dispatch a task to be executed on the main thread. This is a blocking call that will wait /// for the task to complete and return its result. pub fn send_main_thread T + Send, T: Send>(&self, callback: F) -> T { - struct Scope<'a, F, O> { - condvar: &'a Condvar, - output: &'a Mutex>, + struct Scope<'a, F, T> { callback: F, + mutex: &'a Mutex>, + condvar: &'a Condvar, } - let output = Mutex::new(None); + let mutex = Mutex::new(None); let condvar = Condvar::new(); let scope = Scope { - condvar: &condvar, - output: &output, callback, + mutex: &mutex, + condvar: &condvar, }; - let scope_ptr = unsafe { AssertSendSync::new(&scope as *const Scope as *const ()) }; - - self.post_main_thread(move |plugin| unsafe { - let scope = (scope_ptr.get() as *const Scope).read(); - let result = (scope.callback)(plugin); - scope.output.lock().unwrap().replace(result); - scope.condvar.notify_one(); - }); + self.shared + .task_sender + .send(MainThreadTask::Dispatch { + data: &scope as *const _ as _, + func: |plugin, data| { + let scope = unsafe { data.cast::>().read() }; + scope.mutex.lock().unwrap().replace((scope.callback)(plugin)); + scope.condvar.notify_one(); + }, + }) + .unwrap(); - scope - .condvar - .wait_while(scope.output.lock().unwrap(), |v| v.is_none()) + condvar + .wait_while(mutex.lock().unwrap(), |x| x.is_none()) .unwrap() .take() .unwrap() } - /// Post a task to be executed on the main thread. This is a non-blocking call that does not - /// wait for the task to complete and does not return a result. - pub fn post_main_thread(&self, task: impl FnOnce(&Plugin) + Send + 'static) { - self.plugin - .shared - .task_sender - .send(MainThreadTask::Closure(Box::new(task))) - .unwrap(); - } - /// Prepare for audio processing. Returns an error if the plugin returned `false`. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. @@ -151,7 +140,7 @@ impl<'a> PluginAudioThread<'a> { }; if result { - self.plugin.shared.status.store(PluginStatus::Processing); + self.shared.status.store(PluginStatus::Processing); Ok(()) } else { anyhow::bail!("'clap_plugin::start_processing()' returned false.") @@ -205,6 +194,6 @@ impl<'a> PluginAudioThread<'a> { clap_call! { plugin=>stop_processing(plugin) } }; - self.plugin.shared.status.store(PluginStatus::Activated); + self.shared.status.store(PluginStatus::Activated); } } diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs new file mode 100644 index 0000000..ed0d2ab --- /dev/null +++ b/src/plugin/instance/main_thread.rs @@ -0,0 +1,262 @@ +use crate::{ + plugin::{ + ext::Extension, + instance::{InstanceMainThread, InstanceShared, MainThreadTask, PluginAudioThread, PluginStatus}, + library::PluginMetadata, + }, + util::clap_call, +}; +use anyhow::Result; +use clap_sys::{factory::plugin_factory::clap_plugin_factory, plugin::clap_plugin}; +use std::{ffi::CStr, marker::PhantomData, panic::resume_unwind, pin::Pin, ptr::NonNull, sync::Arc}; + +/// A CLAP plugin instance. The plugin will be deinitialized when this object is dropped. All +/// functions here are callable only from the main thread. Use the +/// [`on_audio_thread()`][Self::on_audio_thread()] method to spawn an audio thread. +/// +/// All functions on `Plugin` and the objects created from it will panic if the plugin is not in the +/// correct state. +pub struct Plugin<'lib> { + main: InstanceMainThread, + + /// Information about this plugin instance stored on the host. This keeps track of things like + /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. + shared: Pin>, + + /// The CLAP plugin library this plugin instance was created from. This field is not used + /// directly, but keeping a reference to the library here prevents the plugin instance from + /// outliving the library. + _library: PhantomData<&'lib ()>, + + /// To honor CLAP's thread safety guidelines, the thread this object was created from is + /// designated the 'main thread', and this object cannot be shared with other threads. The + /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call + /// the plugin's audio thread functions. + _thread: PhantomData<*const ()>, +} + +impl Drop for Plugin<'_> { + fn drop(&mut self) { + if let Some(error) = self.shared.callback_error.lock().unwrap().take() { + log::warn!( + "The validator's host has detected a callback error but this error has not been used as part of the \ + test result. This could be a clap-validator bug. The error message is: {error}" + ) + } + + // Make sure the plugin is in the correct state before it gets destroyed + match self.status() { + PluginStatus::Uninitialized | PluginStatus::Deactivated => (), + PluginStatus::Activated => self.deactivate(), + status => log::warn!( + "The plugin was in an invalid state '{status:?}' when the instance got dropped, this is a \ + clap-validator bug" + ), + } + + self.handle_callback_unchecked(); + + let plugin = self.as_ptr(); + unsafe { + clap_call! { plugin=>destroy(plugin) } + } + } +} + +impl<'lib> Plugin<'lib> { + /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the + /// plugin could not be created. The plugin instance will be registered with the host, and + /// unregistered when this object is dropped again. + /// + /// # Panics + /// This MUST be called on the OS main thread (if applicable). + /// + /// # Safety + /// The `factory` object must be valid. + pub(crate) unsafe fn create_plugin(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result { + assert!(IS_OS_MAIN_THREAD.with(|cell| cell.get()), "not main thread"); + + let (shared, main) = unsafe { InstanceShared::new(factory, plugin_id)? }; + + Ok(Plugin { + shared, + main, + + _library: PhantomData, + _thread: PhantomData, + }) + } + + /// Get the raw pointer to the `clap_plugin` instance. + pub fn as_ptr(&self) -> *const clap_plugin { + self.shared.clap_plugin_ptr() + } + + /// Get this plugin's metadata descriptor. In theory this should be the same as the one + /// retrieved from the factory earlier. + pub fn descriptor(&self) -> Result { + let plugin = self.as_ptr(); + let descriptor = unsafe { (*plugin).desc }; + if descriptor.is_null() { + anyhow::bail!("The 'desc' field on the 'clap_plugin' struct is a null pointer."); + } + + PluginMetadata::from_descriptor(unsafe { &*descriptor }) + } + + /// The plugin's current initialization status. + pub fn status(&self) -> PluginStatus { + self.shared.status.load() + } + + /// Handle any pending main-thread callbacks for this plugin. + /// Returns an error if there is a callback error pending. + pub fn handle_callback(&self) -> Result<()> { + self.handle_callback_unchecked(); + + if let Some(error) = self.shared.callback_error.lock().unwrap().take() { + anyhow::bail!(error); + } + + // TODO: + // while let Ok(event) = self.shared.callback_receiver.lock().unwrap().recv() { + // println!("{:?}", event); + // } + + Ok(()) + } + + /// Get the _main thread_ extension abstraction for the extension `T`, if the plugin supports + /// this extension. Returns `None` if it does not. The plugin needs to be initialized using + /// [`init()`][Self::init()] before this may be called. + pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { + self.status().assert_is_not(PluginStatus::Uninitialized); + + let plugin = self.as_ptr(); + for id in T::IDS { + let extension_ptr = unsafe { + clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } + }; + + if !extension_ptr.is_null() { + return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; + } + } + + None + } + + /// Execute some code for this plugin from an audio thread context. The closure receives a + /// [`PluginAudioThread`], which disallows calling main thread functions, and permits calling + /// audio thread functions. + /// + /// If whatever happens on the audio thread caused main-thread callback requests to be emited, + /// then those will be handled concurrently. + pub fn on_audio_thread T + Send>(&self, f: F) -> T { + let result = std::thread::scope(|s| { + let shared = self.shared.clone(); + let thread = s.spawn(move || f(PluginAudioThread::new(shared))); + + // Handle callbacks requests on the main thread while the audio thread is running + while let Ok(task) = self.main.task_receiver.recv() { + match task { + MainThreadTask::Dispatch { func, data } => func(self, data), + MainThreadTask::CallbackRequest => self.handle_callback_unchecked(), + MainThreadTask::StopAudioThread => break, + } + } + + // Wait for the result, propagating panics + match thread.join() { + Ok(value) => value, + Err(panic_info) => resume_unwind(panic_info), + } + }); + + self.handle_callback_unchecked(); + result + } + + /// Initialize the plugin. This needs to be called before doing anything else. + pub fn init(&self) -> Result<()> { + self.status().assert_is(PluginStatus::Uninitialized); + + let plugin = self.as_ptr(); + let result = unsafe { + clap_call! { plugin=>init(plugin) } + }; + + if result { + // If the plugin never calls `request_callback`, the validator won't catch this + anyhow::ensure!( + unsafe { (*plugin).on_main_thread.is_some() }, + "clap_plugin::on_main_thread is null" + ); + + self.shared.status.store(PluginStatus::Deactivated); + Ok(()) + } else { + anyhow::bail!("'clap_plugin::init()' returned false.") + } + } + + /// Activate the plugin. Returns an error if the plugin returned `false`. See + /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the + /// preconditions. + pub fn activate(&self, sample_rate: f64, min_buffer_size: u32, max_buffer_size: u32) -> Result<()> { + self.status().assert_is(PluginStatus::Deactivated); + + // Apparently 0 is invalid here + assert!(min_buffer_size >= 1); + assert!(max_buffer_size >= min_buffer_size); + + // we need to track the `Activating` state to validate that we call clap_host_latency::changed only within the activation call. + self.shared.status.store(PluginStatus::Activating); + + let plugin = self.as_ptr(); + let result = unsafe { + clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size, max_buffer_size) } + }; + + if result { + self.shared.status.store(PluginStatus::Activated); + Ok(()) + } else { + self.shared.status.store(PluginStatus::Deactivated); + anyhow::bail!("'clap_plugin::activate()' returned false.") + } + } + + /// Deactivate the plugin. See + /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the + /// preconditions. + pub fn deactivate(&self) { + self.status().assert_is(PluginStatus::Activated); + + let plugin = self.as_ptr(); + unsafe { + clap_call! { plugin=>deactivate(plugin) } + } + + self.shared.status.store(PluginStatus::Deactivated); + } + + fn handle_callback_unchecked(&self) { + if self.shared.requested_callback.swap(false) { + let plugin = self.as_ptr(); + unsafe { + clap_call! { plugin=>on_main_thread(plugin) } + }; + } + } +} + +thread_local! { + static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +pub unsafe fn mark_current_thread_as_os_main_thread() { + IS_OS_MAIN_THREAD.with(|cell| { + cell.set(true); + }); +} diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs new file mode 100644 index 0000000..a78cbf0 --- /dev/null +++ b/src/plugin/instance/shared.rs @@ -0,0 +1,534 @@ +use crate::plugin::instance::{CallbackEvent, Plugin, PluginStatus}; +use crate::plugin::preset_discovery::LocationValue; +use crate::util::{self, check_null_ptr, clap_call, validator_version}; +use anyhow::{Context, Result}; +use clap_sys::ext::audio_ports::*; +use clap_sys::ext::latency::*; +use clap_sys::ext::note_ports::*; +use clap_sys::ext::params::*; +use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; +use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; +use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_host_tail}; +use clap_sys::ext::thread_check::{CLAP_EXT_THREAD_CHECK, clap_host_thread_check}; +use clap_sys::ext::voice_info::{CLAP_EXT_VOICE_INFO, clap_host_voice_info}; +use clap_sys::factory::plugin_factory::clap_plugin_factory; +use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; +use clap_sys::host::clap_host; +use clap_sys::id::clap_id; +use clap_sys::plugin::clap_plugin; +use clap_sys::version::CLAP_VERSION; +use crossbeam_utils::atomic::AtomicCell; +use std::ffi::{CStr, c_char, c_void}; +use std::pin::Pin; +use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::{Arc, Mutex}; +use std::thread::ThreadId; + +/// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. +/// This struct contains the `clap_host` and its extensions, as well as fields for tracking the plugin's state. +pub struct InstanceShared { + pub task_sender: Sender, + pub callback_sender: Sender, + pub callback_error: Mutex>, + + /// The plugin's current state in terms of activation and processing status. + pub status: AtomicCell, + + /// The plugin instance's main thread. Used for the main thread checks. + pub main_thread_id: ThreadId, + + /// The plugin instance's audio thread, if it has one. Used for the audio thread checks. + pub audio_thread_id: AtomicCell>, + + /// Whether the plugin has called `clap_host::request_callback()` and expects + /// `clap_plugin::on_main_thread()` to be called on the main thread. + pub requested_callback: AtomicCell, + + /// Whether the plugin has called `clap_host::request_restart()` and expects the plugin to be + /// deactivated and subsequently reactivated. + pub requested_restart: AtomicCell, + + clap_plugin: *const clap_plugin, + clap_host: clap_host, + clap_host_audio_ports: clap_host_audio_ports, + clap_host_note_ports: clap_host_note_ports, + clap_host_params: clap_host_params, + clap_host_preset_load: clap_host_preset_load, + clap_host_state: clap_host_state, + clap_host_thread_check: clap_host_thread_check, + clap_host_latency: clap_host_latency, + clap_host_tail: clap_host_tail, + clap_host_voice_info: clap_host_voice_info, +} + +/// Information about a plugin instance's main thread. +pub struct InstanceMainThread { + pub callback_receiver: Receiver, + pub task_receiver: Receiver, +} + +pub enum MainThreadTask { + Dispatch { func: fn(&Plugin, *mut ()), data: *mut () }, + CallbackRequest, + StopAudioThread, +} + +impl InstanceShared { + pub unsafe fn new(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result<(Pin>, InstanceMainThread)> { + let main_thread = std::thread::current().id(); + let (callback_sender, callback_receiver) = channel(); + let (task_sender, task_receiver) = channel(); + + let shared = Arc::pin(InstanceShared { + task_sender, + callback_sender, + callback_error: Mutex::new(None), + + status: AtomicCell::new(PluginStatus::Uninitialized), + main_thread_id: main_thread, + audio_thread_id: AtomicCell::new(None), + requested_callback: AtomicCell::new(false), + requested_restart: AtomicCell::new(false), + + clap_plugin: std::ptr::null(), + clap_host: clap_host { + clap_version: CLAP_VERSION, + // This is populated with a pointer to the `Arc`'s data after creating the Arc + host_data: std::ptr::null_mut(), + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), + version: validator_version().as_ptr(), + get_extension: Some(Self::get_extension), + request_restart: Some(Self::request_restart), + request_process: Some(Self::request_process), + request_callback: Some(Self::request_callback), + }, + + clap_host_audio_ports: clap_host_audio_ports { + is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), + rescan: Some(Self::ext_audio_ports_rescan), + }, + clap_host_note_ports: clap_host_note_ports { + supported_dialects: Some(Self::ext_note_ports_supported_dialects), + rescan: Some(Self::ext_note_ports_rescan), + }, + clap_host_preset_load: clap_host_preset_load { + on_error: Some(Self::ext_preset_load_on_error), + loaded: Some(Self::ext_preset_load_loaded), + }, + clap_host_params: clap_host_params { + rescan: Some(Self::ext_params_rescan), + clear: Some(Self::ext_params_clear), + request_flush: Some(Self::ext_params_request_flush), + }, + clap_host_state: clap_host_state { + mark_dirty: Some(Self::ext_state_mark_dirty), + }, + clap_host_thread_check: clap_host_thread_check { + is_main_thread: Some(Self::ext_thread_check_is_main_thread), + is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), + }, + clap_host_latency: clap_host_latency { + changed: Some(Self::ext_latency_changed), + }, + clap_host_tail: clap_host_tail { + changed: Some(Self::ext_tail_changed), + }, + clap_host_voice_info: clap_host_voice_info { + changed: Some(Self::ext_voice_info_changed), + }, + }); + + let main = InstanceMainThread { + callback_receiver, + task_receiver, + }; + + // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct + // so it can be retrieved in host callbacks + unsafe { + (&raw const shared.clap_host.host_data) + .cast_mut() + .write(&*shared as *const _ as *mut std::ffi::c_void); + } + + let clap_plugin = unsafe { + clap_call! { + factory=>create_plugin(factory, shared.clap_host_ptr(), plugin_id.as_ptr()) + } + }; + + if clap_plugin.is_null() { + anyhow::bail!("'clap_plugin_factory::create_plugin({plugin_id:?})' returned a null pointer."); + } + + unsafe { + (&raw const shared.clap_plugin).cast_mut().write(clap_plugin); + } + + Ok((shared, main)) + } + + pub fn clap_host_ptr(&self) -> *const clap_host { + &self.clap_host as *const clap_host + } + + pub fn clap_plugin_ptr(&self) -> *const clap_plugin { + self.clap_plugin + } + + #[track_caller] + unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { + unsafe { + let state = (*host).host_data as *const InstanceShared; + &*state + } + } + + /// Set the callback error field if it does not already contain a value. Earlier errors are not + /// overwritten. + fn set_callback_error(&self, error: impl Into) { + let mut guard = self.callback_error.lock().unwrap(); + if guard.is_none() { + *guard = Some(error.into()); + } + } + + /// Checks whether this is the main thread. If it is not, then an error indicating this can be + /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_main_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if current_thread_id != self.main_thread_id { + self.set_callback_error(format!( + "'{}' may only be called from the main thread (thread {:?}), but it was called from thread {:?}.", + function_name, self.main_thread_id, current_thread_id + )); + } + } + + /// Checks whether this is the audio thread. If it is not, then an error indicating this can be + /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_audio_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if self.audio_thread_id.load() != Some(current_thread_id) { + if current_thread_id == self.main_thread_id { + self.set_callback_error(format!( + "'{function_name}' may only be called from an audio thread, but it was called from the main \ + thread." + )); + } else { + self.set_callback_error(format!( + "'{function_name}' may only be called from an audio thread, but it was called from an unknown \ + thread." + )); + } + } + } + + /// Checks whether this is **not** the audio thread. If it is, then an error indicating this can + /// be retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread + /// safety errors will not overwrite earlier ones. + fn assert_not_audio_thread(&self, function_name: &str) { + let current_thread_id = std::thread::current().id(); + if self.audio_thread_id.load() == Some(current_thread_id) { + self.set_callback_error(format!( + "'{function_name}' was called from an audio thread, this is not allowed.", + )); + } + } + + unsafe extern "C" fn get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { + //check_null_ptr!(host, (*host).host_data, extension_id); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + // Right now there's no way to have the host only expose certain extensions. We can always + // add that when test cases need it. + let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; + if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { + &this.clap_host_audio_ports as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { + &this.clap_host_note_ports as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { + &this.clap_host_preset_load as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PARAMS { + &this.clap_host_params as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_STATE { + &this.clap_host_state as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { + &this.clap_host_thread_check as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_LATENCY { + &this.clap_host_latency as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_TAIL { + &this.clap_host_tail as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { + &this.clap_host_voice_info as *const _ as *const c_void + } else { + std::ptr::null() + } + } + + unsafe extern "C" fn request_restart(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and + // in the multi-iteration run function it will trigger a deactivate->reactivate cycle + log::trace!("'clap_host::request_restart()' was called by the plugin, setting the flag"); + this.requested_restart.store(true); + } + + unsafe extern "C" fn request_process(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + // Handling this within the context of the validator would be a bit messy. Do plugins use + // this? + log::trace!("'clap_host::request_process()' was called by the plugin"); + this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); + } + + unsafe extern "C" fn request_callback(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + // This this is either handled by `handle_callbacks_blocking()` while the audio thread is + // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the + // callback is not handled before the plugin is destroyed. + log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); + this.requested_callback.store(true); + this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); + } + + unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); + log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); + true + } + + unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_audio_ports::rescan()"); + log::trace!("'clap_host_audio_ports::rescan()' was called"); + + if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); + } + + if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error("'clap_host_audio_ports::rescan()' was called while the plugin was activated"); + } + + this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); + } + } + + unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_note_ports::supported_dialects()"); + log::trace!("'clap_host_note_ports::supported_dialects()' was called"); + + CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE + } + + unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_note_ports::rescan()"); + log::trace!("'clap_host_note_ports::rescan()' was called"); + + if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { + this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); + } + + if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error( + "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while the plugin was \ + activated", + ); + } + + this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); + } + } + + unsafe extern "C" fn ext_preset_load_on_error( + host: *const clap_host, + location_kind: clap_preset_discovery_location_kind, + location: *const c_char, + load_key: *const c_char, + os_error: i32, + msg: *const c_char, + ) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_preset_load::on_error()"); + + let location = unsafe { LocationValue::new(location_kind, location) } + .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'clap_host_preset_load::on_error()' called with an invalid load_key parameter"); + let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } + .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); + match (location, load_key, msg) { + (Ok(location), Ok(Some(load_key)), Ok(msg)) => { + this.set_callback_error(format!( + "'clap_host_preset_load::on_error()' called for {location} with load key {load_key}, OS error \ + code {os_error}, and the following error message: {msg}" + )); + } + (Ok(location), Ok(None), Ok(msg)) => { + this.set_callback_error(format!( + "'clap_host_preset_load::on_error()' called for {location} with no load key, OS error code \ + {os_error}, and the following error message: {msg}" + )); + } + (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { + this.set_callback_error(format!("{err:#}")); + } + } + } + + unsafe extern "C" fn ext_preset_load_loaded( + host: *const clap_host, + location_kind: clap_preset_discovery_location_kind, + location: *const c_char, + load_key: *const c_char, + ) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_preset_load::loaded()"); + + let location = unsafe { LocationValue::new(location_kind, location) } + .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); + + match (location, load_key) { + (Ok(_location), Ok(_load_key)) => { + log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); + } + (Err(err), _) | (_, Err(err)) => { + this.set_callback_error(format!("{err:#}")); + } + } + } + + unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_params::rescan()"); + log::trace!("'clap_host_params::rescan()' was called"); + + if flags & CLAP_PARAM_RESCAN_VALUES != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); + } + + if flags & CLAP_PARAM_RESCAN_TEXT != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanText).unwrap(); + } + + if flags & CLAP_PARAM_RESCAN_INFO != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanInfo).unwrap(); + } + + if flags & CLAP_PARAM_RESCAN_ALL != 0 { + if this.status.load() > PluginStatus::Activated { + this.set_callback_error( + "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is activated", + ); + } + + this.callback_sender.send(CallbackEvent::ParamsRescanAll).unwrap(); + } + } + + unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_params::clear()"); + log::debug!("TODO: Handle 'clap_host_params::clear()'"); + } + + unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_not_audio_thread("clap_host_params::request_flush()"); + log::trace!("'clap_host_params::request_flush()' was called"); + this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); + } + + unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_state::mark_dirty()"); + log::trace!("'clap_host_state::mark_dirty()' was called"); + this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); + } + + unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + this.main_thread_id == std::thread::current().id() + } + + unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + this.audio_thread_id.load() == Some(std::thread::current().id()) + } + + unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + if this.status.load() != PluginStatus::Activating { + this.set_callback_error( + "'clap_host_latency::changed()' must only be called within 'clap_plugin::activate()'", + ); + } + + this.assert_main_thread("clap_host_latency::changed()"); + log::trace!("'clap_host_latency::changed()' was called"); + this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); + } + + unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_audio_thread("clap_host_tail::changed()"); + log::trace!("'clap_host_tail::changed()' was called"); + this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); + } + + unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { InstanceShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_voice_info::changed()"); + log::trace!("'clap_host_voice_info::changed()' was called"); + this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); + } +} + +unsafe impl Send for InstanceShared {} +unsafe impl Sync for InstanceShared {} diff --git a/src/plugin/library.rs b/src/plugin/library.rs index df88caf..c05cb08 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -264,7 +264,7 @@ impl PluginLibrary { } let id_cstring = CString::new(id).context("Plugin ID contained null bytes")?; - unsafe { Plugin::new(&*plugin_factory, &id_cstring) } + unsafe { Plugin::create_plugin(&*plugin_factory, &id_cstring) } } /// Returns the plugin's preset discovery factory, if it has one. @@ -310,13 +310,3 @@ fn get_clap_entry_point(library: &libloading::Library) -> Result<&clap_plugin_en Ok(unsafe { &**entry_point }) } - -thread_local! { - static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; -} - -pub(crate) fn mark_current_thread_as_os_main_thread() { - IS_OS_MAIN_THREAD.with(|cell| { - cell.set(true); - }); -} diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 1538c86..ece382b 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -50,10 +50,6 @@ impl<'a> ProcessScope<'a> { self.buffer.len() } - pub fn sample_rate(&self) -> f64 { - self.sample_rate - } - pub fn input_queue(&self) -> &EventQueue { &self.events_input } @@ -120,7 +116,11 @@ impl<'a> ProcessScope<'a> { self.plugin.process(&clap_process { steady_time: self.transport.sample_pos, frames_count: samples, - transport: &transport, + transport: if self.transport.is_freerun { + std::ptr::null() + } else { + &transport as *const _ + }, audio_inputs: inputs.as_ptr(), audio_outputs: outputs.as_mut_ptr(), audio_inputs_count: inputs.len() as u32, @@ -134,7 +134,7 @@ impl<'a> ProcessScope<'a> { self.transport.advance(samples, self.sample_rate); // check output audio buffers for NaNs or infinities - check_process_call_consistency(self.buffer.buffers(), &original_buffers, &self.events_output, samples) + check_process_call_consistency(self.buffer.buffers(), &original_buffers, self.output_queue(), samples) } pub fn restart(&mut self) { @@ -160,6 +160,7 @@ impl Drop for ProcessScope<'_> { /// These are quiet NaNs with a specific payload to avoid accidental matches with other NaN values. /// The payload is chosen to be unlikely to appear in normal processing. const CHECK_NAN_F32: f32 = f32::from_bits(0x7FC0_1234); +/// See [`CHECK_NAN_F32`]. const CHECK_NAN_F64: f64 = f64::from_bits(0x7FF8_1234_5678_1234); /// The process for consistency. This verifies that the output buffer has been written to, doesn't contain any NaN, diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs index bb0bf97..36f5341 100644 --- a/src/plugin/process/buffer.rs +++ b/src/plugin/process/buffer.rs @@ -9,15 +9,16 @@ use std::ptr::null_mut; /// or out-of-place, single or double precision. #[derive(Clone)] pub struct AudioBuffers { - // These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be - // mutable because reborrwing them from here is the only way to modify them without - // reinitializing the pointers. + /// These are all indexed by `[port_idx][channel_idx][sample_idx]`. The inputs also need to be + /// mutable because reborrwing them from here is the only way to modify them without + /// reinitializing the pointers. buffers: Box<[AudioBuffer]>, - // These are point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a - // `*const *const f32` + /// These point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a + /// `*const *const f32` _pointers: Box<[Box<[*const ()]>]>, + /// The CLAP audio buffer representations for inputs and outputs. clap_inputs: Box<[clap_audio_buffer]>, clap_outputs: Box<[clap_audio_buffer]>, @@ -149,8 +150,6 @@ impl AudioBuffers { } } - /// Construct the out of place audio buffers. This allocates the channel pointers that are - /// handed to the plugin in the process function. pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { Self::new( config @@ -168,8 +167,6 @@ impl AudioBuffers { ) } - /// Construct the in place audio buffers. This allocates the channel pointers that are handed to - /// the plugin in the process function. pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { let mut buffers = vec![]; @@ -234,8 +231,6 @@ impl AudioBuffers { ) } - /// Construct the in place audio buffers. This allocates the channel pointers that are handed to - /// the plugin in the process function. pub fn new_in_place_f64(config: &AudioPortConfig, num_samples: u32) -> Self { let mut buffers = vec![]; @@ -321,6 +316,7 @@ impl AudioBuffers { } } + /// Fill the input buffers with silence (zeros), and mark all input channels as constant. pub fn silence_inputs(&mut self) { for buffer in self.buffers_mut() { if buffer.is_input() { @@ -333,10 +329,7 @@ impl AudioBuffers { } } - pub fn set_input_constant_mask(&mut self, bus: usize, mask: ConstantMask) { - self.clap_inputs[bus].constant_mask = mask.0; - } - + /// Get the constant mask for the given output bus. pub fn get_output_constant_mask(&self, bus: usize) -> ConstantMask { ConstantMask(self.clap_outputs[bus].constant_mask) } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index ec1ab5a..cdaac48 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -4,19 +4,33 @@ use clap_sys::{events::*, fixedpoint::*}; /// transport changes. #[derive(Debug, Clone, Default)] pub struct TransportState { + /// The current sample position. pub sample_pos: i64, + /// When true, `null` is passed as the transport pointer to the plugin. + pub is_freerun: bool, + + /// Whether playback is active. Sets `CLAP_TRANSPORT_IS_PLAYING` flag. pub is_playing: bool, + + /// Whether recording is active. Sets `CLAP_TRANSPORT_IS_RECORDING` flag. pub is_recording: bool, + /// Current tempo in BPM and its increment per sample. Sets `CLAP_TRANSPORT_HAS_TEMPO` flag. pub tempo: Option<(f64, f64)>, + + /// Current time signature as (numerator, denominator). Sets `CLAP_TRANSPORT_HAS_TIME_SIGNATURE` flag. pub time_signature: Option<(u16, u16)>, + /// Current position in beats. Sets `CLAP_TRANSPORT_HAS_BEATS_TIMELINE` flag. pub position_beats: Option, + + /// Current position in seconds. Sets `CLAP_TRANSPORT_HAS_SECONDS_TIMELINE` flag. pub position_seconds: Option, } impl TransportState { + /// Advance the transport state by the given number of samples at the specified sample rate. pub fn advance(&mut self, samples: u32, sample_rate: f64) { self.sample_pos += samples as i64; @@ -31,12 +45,12 @@ impl TransportState { if let Some(position_beats) = &mut self.position_beats { // Integrate tempo over the sample block using the trapezoidal rule - *position_beats += - (samples as f64 * (tempo_end + tempo_start) / 60.0 * 0.5) / sample_rate; + *position_beats += (samples as f64 * (tempo_end + tempo_start) / 60.0 * 0.5) / sample_rate; } } } + /// Convert the transport state to a CLAP transport event. pub fn as_clap_transport(&self, offset: u32) -> clap_event_transport { let mut flags = 0; flags |= self.is_playing as u32 * CLAP_TRANSPORT_IS_PLAYING; @@ -86,14 +100,8 @@ impl TransportState { pub struct ConstantMask(pub u64); impl ConstantMask { - pub const CONSTANT: Self = Self(u64::MAX); - pub const DYNAMIC: Self = Self(0); - + /// Check if the specified channel marked as constant. pub fn is_channel_constant(&self, channel: u32) -> bool { self.0 & 1u64.unbounded_shl(channel) != 0 } - - pub fn are_channels_constant(&self, channels: u32) -> bool { - self.0 & 1u64.unbounded_shl(channels).wrapping_sub(1) == 0 - } } diff --git a/src/util.rs b/src/util.rs index 2b955ff..85954b7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -169,31 +169,11 @@ pub fn validator_version() -> &'static CStr { .as_c_str() } -/// A helper struct used to send stuff across thread boundary. -pub struct AssertSendSync(T); - -impl AssertSendSync { - pub unsafe fn new(value: T) -> Self { - AssertSendSync(value) - } - - pub fn get(self) -> T { - self.0 - } -} - -unsafe impl Send for AssertSendSync {} -unsafe impl Sync for AssertSendSync {} - impl IteratorExt for T where T: Iterator {} pub trait IteratorExt: Iterator { /// Map the iterator in parallel if `parallel` is `true`, or sequentially if it is `false`. /// Returns an iterator over the mapped values, in arbitrary order. - fn map_parallel( - self, - parallel: bool, - f: impl Fn(Self::Item) -> R + Send + Sync, - ) -> impl Iterator + fn map_parallel(self, parallel: bool, f: impl Fn(Self::Item) -> R + Send + Sync) -> impl Iterator where Self: Sized + Send, Self::Item: Send, From a4f02a9989dc70fe1495ae44033a65dc3cd6d3dc Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 29 Jan 2026 19:17:26 +0400 Subject: [PATCH 045/114] add transport-* tests --- Cargo.lock | 61 +++++ Cargo.toml | 2 +- src/main.rs | 4 +- src/plugin/ext.rs | 1 + src/plugin/ext/audio_ports_activation.rs | 22 ++ src/plugin/process.rs | 11 +- src/plugin/process/events.rs | 34 +-- src/plugin/process/transport.rs | 8 +- src/tests/plugin.rs | 47 +++- src/tests/plugin/transport.rs | 185 +++++++++++++++ src/tests/rng.rs | 277 ++++++++++++++--------- src/validator.rs | 59 ++--- 12 files changed, 519 insertions(+), 192 deletions(-) create mode 100644 src/plugin/ext/audio_ports_activation.rs create mode 100644 src/tests/plugin/transport.rs diff --git a/Cargo.lock b/Cargo.lock index a97f731..5b7ed2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.0.2" @@ -87,6 +102,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -360,6 +390,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "heck" version = "0.4.1" @@ -484,6 +520,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" dependencies = [ + "backtrace", "log", ] @@ -508,6 +545,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -532,6 +578,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -670,6 +725,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustix" version = "0.37.23" diff --git a/Cargo.toml b/Cargo.toml index 2e2cde1..d18b7b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ colored = "3.0.0" crossbeam-utils = "0.8.21" libloading = "0.9.0" log = "0.4" -log-panics = "2.0" +log-panics = { version = "2.0", features = ["with-backtrace"] } midi-consts = "0.1.0" rand = "0.9.2" rand_pcg = "0.9.0" diff --git a/src/main.rs b/src/main.rs index e074f28..11b4df7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,9 @@ fn main() -> ExitCode { ) .expect("Could not initialize logger"); - log_panics::init(); + log_panics::Config::new() + .backtrace_mode(log_panics::BacktraceMode::Resolved) + .install_panic_hook(); // Mark the main thread as such for plugin instance creation checks. unsafe { diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index 5268b2d..01eaa56 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -7,6 +7,7 @@ use std::ptr::NonNull; pub mod ambisonic; pub mod audio_ports; +pub mod audio_ports_activation; pub mod audio_ports_config; pub mod configurable_audio_ports; pub mod latency; diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs new file mode 100644 index 0000000..023a071 --- /dev/null +++ b/src/plugin/ext/audio_ports_activation.rs @@ -0,0 +1,22 @@ +use crate::plugin::{ext::Extension, instance::Plugin}; +use clap_sys::ext::audio_ports_activation::*; +use std::{ffi::CStr, ptr::NonNull}; + +/// Abstraction for the `audio-ports-activation` extension covering the main thread functionality. +pub struct AudioPortsActivation<'a> { + plugin: &'a Plugin<'a>, + audio_ports_activation: NonNull, +} + +impl<'a> Extension<&'a Plugin<'a>> for AudioPortsActivation<'a> { + const IDS: &'static [&'static CStr] = &[CLAP_EXT_AUDIO_PORTS_ACTIVATION, CLAP_EXT_AUDIO_PORTS_ACTIVATION_COMPAT]; + + type Struct = clap_plugin_audio_ports_activation; + + unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + audio_ports_activation: extension_struct, + } + } +} diff --git a/src/plugin/process.rs b/src/plugin/process.rs index ece382b..9f27e50 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -46,6 +46,10 @@ impl<'a> ProcessScope<'a> { }) } + pub fn sample_rate(&self) -> f64 { + self.sample_rate + } + pub fn max_block_size(&self) -> u32 { self.buffer.len() } @@ -99,6 +103,9 @@ impl<'a> ProcessScope<'a> { // prepare output event queue for processing self.events_output.clear(); + // prepare input event queue for processing + self.events_input.sort_events(); + // prepare output audio buffers for processing // this is used to detect uninitialized output buffers for buffer in self.buffer.buffers_mut() { @@ -114,7 +121,7 @@ impl<'a> ProcessScope<'a> { let transport = self.transport.as_clap_transport(0); let (inputs, outputs) = self.buffer.clap_buffers(); self.plugin.process(&clap_process { - steady_time: self.transport.sample_pos, + steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), frames_count: samples, transport: if self.transport.is_freerun { std::ptr::null() @@ -131,7 +138,7 @@ impl<'a> ProcessScope<'a> { // clear input event queue and advance transport self.events_input.clear(); - self.transport.advance(samples, self.sample_rate); + self.transport.advance(samples as i64, self.sample_rate()); // check output audio buffers for NaNs or infinities check_process_call_consistency(self.buffer.buffers(), &original_buffers, self.output_queue(), samples) diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index a8f1834..21b1dbf 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -69,12 +69,12 @@ impl EventQueue { } pub fn add_events(&self, extend: impl IntoIterator) { + self.events.lock().unwrap().extend(extend); + } + + pub fn sort_events(&self) { let mut events = self.events.lock().unwrap(); - let should_sort = !events.is_empty(); - events.extend(extend); - if should_sort { - events.sort_by_key(|event| event.header().time); - } + events.sort_by_key(|event| event.header().time); } pub fn read(&self) -> Vec { @@ -99,10 +99,7 @@ impl EventQueue { } } - unsafe extern "C" fn get( - list: *const clap_input_events, - index: u32, - ) -> *const clap_event_header { + unsafe extern "C" fn get(list: *const clap_input_events, index: u32) -> *const clap_event_header { unsafe { check_null_ptr!(list, (*list).ctx); let this = &*((*list).ctx as *const Self); @@ -121,10 +118,7 @@ impl EventQueue { } } - unsafe extern "C" fn try_push( - list: *const clap_output_events, - event: *const clap_event_header, - ) -> bool { + unsafe extern "C" fn try_push(list: *const clap_output_events, event: *const clap_event_header) -> bool { unsafe { check_null_ptr!(list, (*list).ctx, event); let this = &*((*list).ctx as *const Self); @@ -141,19 +135,13 @@ impl EventQueue { impl Event { /// Parse an event from a plugin-provided pointer. Returns an error if the pointer as a null pointer pub unsafe fn from_raw(ptr: *const clap_event_header) -> Self { - assert!( - !ptr.is_null(), - "Null pointer provided for 'clap_event_header'." - ); + assert!(!ptr.is_null(), "Null pointer provided for 'clap_event_header'."); unsafe { match ((*ptr).space_id, ((*ptr).type_)) { ( CLAP_CORE_EVENT_SPACE_ID, - CLAP_EVENT_NOTE_ON - | CLAP_EVENT_NOTE_OFF - | CLAP_EVENT_NOTE_CHOKE - | CLAP_EVENT_NOTE_END, + CLAP_EVENT_NOTE_ON | CLAP_EVENT_NOTE_OFF | CLAP_EVENT_NOTE_CHOKE | CLAP_EVENT_NOTE_END, ) => Event::Note(*(ptr as *const clap_event_note)), (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_EXPRESSION) => { Event::NoteExpression(*(ptr as *const clap_event_note_expression)) @@ -164,9 +152,7 @@ impl Event { (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_MOD) => { Event::ParamMod(*(ptr as *const clap_event_param_mod)) } - (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI) => { - Event::Midi(*(ptr as *const clap_event_midi)) - } + (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI) => Event::Midi(*(ptr as *const clap_event_midi)), (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_TRANSPORT) => { Event::Transport(*(ptr as *const clap_event_transport)) } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index cdaac48..9b601bc 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -5,7 +5,7 @@ use clap_sys::{events::*, fixedpoint::*}; #[derive(Debug, Clone, Default)] pub struct TransportState { /// The current sample position. - pub sample_pos: i64, + pub sample_pos: Option, /// When true, `null` is passed as the transport pointer to the plugin. pub is_freerun: bool, @@ -31,8 +31,10 @@ pub struct TransportState { impl TransportState { /// Advance the transport state by the given number of samples at the specified sample rate. - pub fn advance(&mut self, samples: u32, sample_rate: f64) { - self.sample_pos += samples as i64; + pub fn advance(&mut self, samples: i64, sample_rate: f64) { + if let Some(sample_pos) = &mut self.sample_pos { + *sample_pos = sample_pos.saturating_add_signed(samples); + } if let Some(position_seconds) = &mut self.position_seconds { *position_seconds += samples as f64 / sample_rate; diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 3e0c2c9..9d64813 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -9,8 +9,9 @@ use std::path::Path; mod descriptor; mod layout; mod params; -pub mod processing; +mod processing; mod state; +mod transport; /// The tests for individual CLAP plugins. See the module's heading for more information, and the /// `description` function below for a description of each test case. @@ -74,6 +75,12 @@ pub enum PluginTestCase { StateReproducibilityFlush, #[strum(serialize = "state-buffered-streams")] StateBufferedStreams, + #[strum(serialize = "transport-null")] + TransportNull, + #[strum(serialize = "transport-fuzz")] + TransportFuzz, + #[strum(serialize = "transport-sample-accurate")] + TransportFuzzSampleAccurate, } impl<'a> TestCase<'a> for PluginTestCase { @@ -103,23 +110,23 @@ impl<'a> TestCase<'a> for PluginTestCase { that support it.", ), PluginTestCase::ProcessAudioOutOfPlaceDouble => format!( - "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ + "Same as '{}', but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ support it.", PluginTestCase::ProcessAudioOutOfPlaceBasic, ), PluginTestCase::ProcessAudioInPlaceDouble => format!( - "Same as {}, but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ + "Same as '{}', but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ support it.", PluginTestCase::ProcessAudioInPlaceBasic, ), PluginTestCase::LayoutConfigurableAudioPorts => format!( - "Performs the same test as {}, but this time it tries random configurations exposed via the \ + "Same as '{}', but this time it tries random configurations exposed via the \ 'configurable-audio-ports' extension.", PluginTestCase::ProcessAudioOutOfPlaceBasic, ), PluginTestCase::LayoutAudioPortsConfig => format!( - "Performs the same test as {}, but this time it tries all available port configurations exposed via \ - the 'audio-ports-config' extension.", + "Same as '{}', but this time it tries all available port configurations exposed via the \ + 'audio-ports-config' extension.", PluginTestCase::ProcessAudioInPlaceBasic, ), PluginTestCase::ProcessAudioConstantMask => String::from( @@ -169,8 +176,8 @@ impl<'a> TestCase<'a> for PluginTestCase { params::FUZZ_RUNS_PER_PERMUTATION ), PluginTestCase::ParamFuzzBounds => format!( - "The exact same test as {}, but this time the parameter values are snapped to the minimum and maximum \ - values.", + "The exact same test as '{}', but this time the parameter values are snapped to the minimum and \ + maximum values.", PluginTestCase::ParamFuzzBasic ), PluginTestCase::ParamFuzzSampleAccurate => String::from( @@ -205,7 +212,7 @@ impl<'a> TestCase<'a> for PluginTestCase { function.", ), PluginTestCase::StateReproducibilityNullCookies => format!( - "The exact same test as {}, but with all cookies in the parameter events set to null pointers. The \ + "The exact same test as '{}', but with all cookies in the parameter events set to null pointers. The \ plugin should handle this in the same way as the other test case.", PluginTestCase::StateReproducibilityBasic ), @@ -220,6 +227,23 @@ impl<'a> TestCase<'a> for PluginTestCase { only allowed to read a small prime number of bytes at a time when reloading and resaving the state.", PluginTestCase::StateReproducibilityBasic ), + + // TODO: fix these + PluginTestCase::TransportNull => String::from( + "Performs audio processing with a 'null' transport pointer, simulating a free-running transport \ + state. The plugin passes the test if it doesn't produce any infinite or NaN values, and doesn't \ + crash.", + ), + PluginTestCase::TransportFuzz => String::from( + "Performs audio processing while randomly changing the transport state on every block. The plugin \ + passes the test if it doesn't produce any infinite or NaN values, and doesn't crash.", + ), + PluginTestCase::TransportFuzzSampleAccurate => format!( + "Same as '{}', but this time the test sends 'clap_event_transport' events in ample-accurate fashion \ + while processing audio, generating them at fixed intervals (1, 100, 1000 samples). The plugin passes \ + the test if it doesn't produce any infinite or NaN values, and doesn't crash.", + PluginTestCase::TransportFuzz + ), } } @@ -282,6 +306,11 @@ impl<'a> TestCase<'a> for PluginTestCase { } PluginTestCase::StateReproducibilityFlush => state::test_state_reproducibility_flush(library, plugin_id), PluginTestCase::StateBufferedStreams => state::test_state_buffered_streams(library, plugin_id), + PluginTestCase::TransportNull => transport::test_transport_null(library, plugin_id), + PluginTestCase::TransportFuzz => transport::test_transport_fuzz(library, plugin_id), + PluginTestCase::TransportFuzzSampleAccurate => { + transport::test_transport_fuzz_sample_accurate(library, plugin_id) + } } } } diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs new file mode 100644 index 0000000..5ed87e5 --- /dev/null +++ b/src/tests/plugin/transport.rs @@ -0,0 +1,185 @@ +use crate::{ + plugin::{ + ext::{ + audio_ports::{AudioPortConfig, AudioPorts}, + note_ports::{NotePortConfig, NotePorts}, + }, + library::PluginLibrary, + process::{AudioBuffers, Event, ProcessScope, TransportState}, + }, + tests::{ + TestStatus, + rng::{NoteGenerator, TransportFuzzer, new_prng}, + }, +}; +use anyhow::{Context, Result}; + +const BUFFER_SIZE: u32 = 512; + +/// The test for `PluginTestCase::TransportNull` +pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + process.transport().is_freerun = true; + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.audio_buffers().randomize(&mut prng); + process.run()?; + } + + Ok(()) + })?; + + plugin.handle_callback().context("An error occured during a callback")?; + + Ok(TestStatus::Success { details: None }) +} + +/// The test for `PluginTestCase::TransportFuzz` +pub fn test_transport_fuzz(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + + plugin.on_audio_thread(|plugin| -> Result<()> { + let mut transport_fuzz = TransportFuzzer::new(); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..20 { + transport_fuzz.mutate(&mut prng, process.transport()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.audio_buffers().randomize(&mut prng); + process.run()?; + } + + Ok(()) + })?; + + plugin.handle_callback().context("An error occured during a callback")?; + + Ok(TestStatus::Success { details: None }) +} + +/// The test for `PluginTestCase::TransportFuzzSampleAccurate` +pub fn test_transport_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) -> Result { + const INTERVALS: &[u32] = &[1000, 100, 1]; + + let mut prng = new_prng(); + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; + + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + + for &interval in INTERVALS { + plugin + .on_audio_thread(|plugin| -> Result<()> { + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + let mut transport_fuzz = TransportFuzzer::new(); + let mut transport_state = TransportState::default(); + + for _ in 0..5 { + // set initial transport state for the block + *process.transport() = transport_state.clone(); + + // add sample-accurate transport events + let mut current_sample = 0; + while current_sample < BUFFER_SIZE { + // advance transport state to the event position, mutate it, and add the event + transport_state.advance(interval as i64, process.sample_rate()); + transport_fuzz.mutate(&mut prng, &mut transport_state); + + current_sample += interval; + process + .input_queue() + .add_events([Event::Transport(transport_state.as_clap_transport(current_sample))]); + } + + // set it to the start of the next block + transport_state.advance(-(current_sample as i64), process.sample_rate()); + + process.audio_buffers().randomize(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } + + Ok(()) + }) + .with_context(|| { + format!( + "Error during sample-accurate transport test with interval of {} samples", + interval + ) + })?; + } + + plugin.handle_callback().context("An error occured during a callback")?; + + Ok(TestStatus::Success { details: None }) +} diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 324953a..fb289ac 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -2,7 +2,7 @@ use crate::plugin::ext::note_ports::NotePortConfig; use crate::plugin::ext::params::{Param, ParamInfo}; -use crate::plugin::process::{Event, EventQueue}; +use crate::plugin::process::{Event, EventQueue, TransportState}; use clap_sys::events::*; use midi_consts::channel_event as midi; use rand::Rng; @@ -57,6 +57,9 @@ pub struct ParamFuzzer<'a> { sample_offset_range: RangeInclusive, } +/// A helper to generate random transport events in a couple different ways to stress test a plugin's transport handling. +pub struct TransportFuzzer {} + /// The description of an active note in the [`NoteGenerator`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct Note { @@ -85,6 +88,54 @@ enum NoteEventType { ParamModulation, } +impl NoteEventType { + const CLAP_EVENTS: &'static [NoteEventType] = &[ + NoteEventType::ClapNoteOn, + NoteEventType::ClapNoteOff, + NoteEventType::ClapNoteChoke, + NoteEventType::ClapNoteExpression, + ]; + const MIDI_EVENTS: &'static [NoteEventType] = &[ + NoteEventType::MidiNoteOn, + NoteEventType::MidiNoteOff, + NoteEventType::MidiChannelPressure, + NoteEventType::MidiPolyKeyPressure, + NoteEventType::MidiPitchBend, + NoteEventType::MidiCc, + NoteEventType::MidiProgramChange, + ]; + const PARAM_EVENTS: &'static [NoteEventType] = &[NoteEventType::ParamValue, NoteEventType::ParamModulation]; + + /// Get a slice containing the event types supported by a plugin. Returns None if the plugin + /// supports neither CLAP note events nor MIDI. + pub fn supported_types( + supports_clap_note_events: bool, + supports_midi_events: bool, + supports_param_events: bool, + ) -> impl Iterator { + let clap = if supports_clap_note_events { + Self::CLAP_EVENTS + } else { + &[] + }; + let midi = if supports_midi_events { Self::MIDI_EVENTS } else { &[] }; + let param = if supports_param_events { Self::PARAM_EVENTS } else { &[] }; + + clap.iter().chain(midi.iter()).chain(param.iter()).copied() + } +} + +impl Note { + fn random(prng: &mut Pcg32) -> Self { + Note { + key: prng.random_range(0..128), + channel: prng.random_range(0..16), + note_id: prng.random_range(0..100), + choked: false, + } + } +} + impl<'a> NoteGenerator<'a> { /// Create a new random note generator based on a plugin's note port configuration. By default /// these events are consistent, meaning that there are no things like note offs before a note @@ -274,8 +325,7 @@ impl<'a> NoteGenerator<'a> { Note::random(prng) }; - let expression_id = prng - .random_range(CLAP_NOTE_EXPRESSION_VOLUME..=CLAP_NOTE_EXPRESSION_PRESSURE); + let expression_id = prng.random_range(CLAP_NOTE_EXPRESSION_VOLUME..=CLAP_NOTE_EXPRESSION_PRESSURE); let value_range = match expression_id { CLAP_NOTE_EXPRESSION_VOLUME => 0.0..=4.0, CLAP_NOTE_EXPRESSION_TUNING => -128.0..=128.0, @@ -462,9 +512,7 @@ impl<'a> NoteGenerator<'a> { let Some((param_id, param)) = params .iter() - .filter(|(_, param)| { - !param.readonly() && !param.hidden() && param.poly_automatable() - }) + .filter(|(_, param)| !param.readonly() && !param.hidden() && param.poly_automatable()) .choose(prng) else { continue; @@ -505,9 +553,7 @@ impl<'a> NoteGenerator<'a> { let Some((param_id, param)) = params .iter() - .filter(|(_, param)| { - !param.readonly() && !param.hidden() && param.poly_modulatable() - }) + .filter(|(_, param)| !param.readonly() && !param.hidden() && param.poly_modulatable()) .choose(prng) else { continue; @@ -544,10 +590,7 @@ impl<'a> NoteGenerator<'a> { } } - panic!( - "Unable to generate a random note event after 1024 tries, this is a bug in the \ - validator" - ); + panic!("Unable to generate a random note event after 1024 tries, this is a bug in the validator"); } #[allow(unused)] @@ -600,63 +643,6 @@ impl<'a> NoteGenerator<'a> { } } -impl NoteEventType { - const CLAP_EVENTS: &'static [NoteEventType] = &[ - NoteEventType::ClapNoteOn, - NoteEventType::ClapNoteOff, - NoteEventType::ClapNoteChoke, - NoteEventType::ClapNoteExpression, - ]; - const MIDI_EVENTS: &'static [NoteEventType] = &[ - NoteEventType::MidiNoteOn, - NoteEventType::MidiNoteOff, - NoteEventType::MidiChannelPressure, - NoteEventType::MidiPolyKeyPressure, - NoteEventType::MidiPitchBend, - NoteEventType::MidiCc, - NoteEventType::MidiProgramChange, - ]; - const PARAM_EVENTS: &'static [NoteEventType] = - &[NoteEventType::ParamValue, NoteEventType::ParamModulation]; - - /// Get a slice containing the event types supported by a plugin. Returns None if the plugin - /// supports neither CLAP note events nor MIDI. - pub fn supported_types( - supports_clap_note_events: bool, - supports_midi_events: bool, - supports_param_events: bool, - ) -> impl Iterator { - let clap = if supports_clap_note_events { - Self::CLAP_EVENTS - } else { - &[] - }; - let midi = if supports_midi_events { - Self::MIDI_EVENTS - } else { - &[] - }; - let param = if supports_param_events { - Self::PARAM_EVENTS - } else { - &[] - }; - - clap.iter().chain(midi.iter()).chain(param.iter()).copied() - } -} - -impl Note { - fn random(prng: &mut Pcg32) -> Self { - Note { - key: prng.random_range(0..128), - channel: prng.random_range(0..16), - note_id: prng.random_range(0..100), - choked: false, - } - } -} - impl<'a> ParamFuzzer<'a> { /// Create a new parameter fuzzer. This ignores parameters that are readonly or hidden. pub fn new(params: &'a ParamInfo) -> Self { @@ -744,52 +730,46 @@ impl<'a> ParamFuzzer<'a> { /// Randomize _all_ parameters at a certain sample index using **automation**, returning an /// iterator yielding automation events for all parameters. - pub fn randomize_params_at( - &'a self, - prng: &'a mut Pcg32, - time_offset: u32, - ) -> impl Iterator + 'a { - self.params - .iter() - .filter_map(move |(param_id, param_info)| { - // We can send parameter changes for parameters that are not automatable: - // - // > The host can send live user changes for this parameter regardless of this flag. - if param_info.readonly() || param_info.hidden() { - return None; + pub fn randomize_params_at(&'a self, prng: &'a mut Pcg32, time_offset: u32) -> impl Iterator + 'a { + self.params.iter().filter_map(move |(param_id, param_info)| { + // We can send parameter changes for parameters that are not automatable: + // + // > The host can send live user changes for this parameter regardless of this flag. + if param_info.readonly() || param_info.hidden() { + return None; + } + + let value = if self.snap_to_bounds { + if prng.random_bool(0.5) { + *param_info.range.start() + } else { + *param_info.range.end() } + } else { + ParamFuzzer::random_value(param_info, prng) + }; - let value = if self.snap_to_bounds { - if prng.random_bool(0.5) { - *param_info.range.start() + Some(Event::ParamValue(clap_event_param_value { + header: clap_event_header { + size: std::mem::size_of::() as u32, + time: time_offset, + space_id: CLAP_CORE_EVENT_SPACE_ID, + type_: CLAP_EVENT_PARAM_VALUE, + flags: if param_info.automatable() { + 0 } else { - *param_info.range.end() - } - } else { - ParamFuzzer::random_value(param_info, prng) - }; - - Some(Event::ParamValue(clap_event_param_value { - header: clap_event_header { - size: std::mem::size_of::() as u32, - time: time_offset, - space_id: CLAP_CORE_EVENT_SPACE_ID, - type_: CLAP_EVENT_PARAM_VALUE, - flags: if param_info.automatable() { - 0 - } else { - CLAP_EVENT_IS_LIVE - }, + CLAP_EVENT_IS_LIVE }, - param_id: *param_id, - cookie: param_info.cookie, - note_id: -1, - port_index: -1, - channel: -1, - key: -1, - value, - })) - }) + }, + param_id: *param_id, + cookie: param_info.cookie, + note_id: -1, + port_index: -1, + channel: -1, + key: -1, + value, + })) + }) } pub fn random_value(param: &Param, prng: &mut Pcg32) -> f64 { @@ -812,3 +792,76 @@ impl<'a> ParamFuzzer<'a> { } } } + +impl TransportFuzzer { + /// Create a new transport fuzzer. + pub fn new() -> Self { + TransportFuzzer {} + } + + /// Mutates an existing transport state. + pub fn mutate(&mut self, prng: &mut Pcg32, transport: &mut TransportState) { + // toggle playback state with 10% probability + if prng.random_bool(0.1) { + transport.is_playing = !transport.is_playing; + } + + // toggle recording state with 10% probability + if prng.random_bool(0.1) { + transport.is_recording = !transport.is_recording; + } + + // change time signature with 10% probability + if prng.random_bool(0.1) { + if prng.random_bool(0.5) { + transport.time_signature = None; + } else { + transport.time_signature = Some((prng.random_range(1..=16), prng.random_range(1..=4))); + } + } + + // change tempo (instanteous) with 10% probability + if prng.random_bool(0.1) { + if prng.random_bool(0.5) { + transport.tempo = None; + } else { + transport.tempo = Some((prng.random_range(40.0..=480.0), 0.0)); + } + } + + // change tempo (ramp) with 20% probability + if prng.random_bool(0.2) { + if let Some((tempo, ramp)) = &mut transport.tempo { + // safeguard to prevent extremely low tempos + if *tempo < 40.0 { + *tempo = 40.0; + *ramp = prng.random_range(0.0..=0.01); + } + + *ramp = prng.random_range(-0.01..=0.01); + } + } + + // seek to a new position with 5% probability + if prng.random_bool(0.05) { + if prng.random_bool(0.5) { + transport.position_seconds = None; + } else { + transport.position_seconds = Some(prng.random_range(0.0..=60.0)); + } + + if prng.random_bool(0.5) { + transport.position_beats = None; + } else { + transport.position_beats = Some(prng.random_range(0.0..=240.0)); + } + + if prng.random_bool(0.5) { + transport.sample_pos = None; + } else { + // we can only seek forward + transport.sample_pos = Some(transport.sample_pos.unwrap_or(0) + prng.random_range(0..=100_000) as u64); + } + } + } +} diff --git a/src/validator.rs b/src/validator.rs index e695418..b1e3231 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -4,9 +4,7 @@ use crate::Verbosity; use crate::commands::validate::{SingleTestSettings, ValidatorSettings}; use crate::plugin::library::{PluginLibrary, PluginMetadata}; -use crate::tests::{ - PluginLibraryTestCase, PluginTestCase, SerializedTest, TestCase, TestResult, TestStatus, -}; +use crate::tests::{PluginLibraryTestCase, PluginTestCase, SerializedTest, TestCase, TestResult, TestStatus}; use crate::util::{self, IteratorExt}; use anyhow::{Context, Result}; use clap::ValueEnum; @@ -90,9 +88,7 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result>>()?, ); @@ -100,12 +96,9 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result>>()?, - )) + Ok((plugin_metadata.id.clone(), tests.collect::>>()?)) }) .collect::>>()?; @@ -158,8 +143,8 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result<()> { /// The filter function for determining whether or not a test should be run based on the validator's /// settings settings. -fn test_filter<'a, T: TestCase<'a>>( - test: &T, - settings: &ValidatorSettings, - test_filter_re: &Option, -) -> bool { +fn test_filter<'a, T: TestCase<'a>>(test: &T, settings: &ValidatorSettings, test_filter_re: &Option) -> bool { let test_name = test.to_string(); match (&test_filter_re, settings.invert_filter) { (Some(test_filter_re), false) if !test_filter_re.is_match(&test_name) => false, @@ -280,9 +261,8 @@ fn run_test_out_of_process<'a, T: TestCase<'a>>( .context("Could not create a temporary file path")? .into_temp_path(); - let mut command = Command::new( - std::env::current_exe().context("Could not find the path to the current executable")?, - ); + let mut command = + Command::new(std::env::current_exe().context("Could not find the path to the current executable")?); command .arg("--verbosity") @@ -314,14 +294,13 @@ fn run_test_out_of_process<'a, T: TestCase<'a>>( // At this point, the child process _should_ have written its output to `output_file_path`, // and we can just parse it from there - let result = - serde_json::from_str(&fs::read_to_string(&output_file_path).with_context(|| { - format!( - "Could not read the child process output from '{}'", - output_file_path.display() - ) - })?) - .context("Could not parse the child process output to JSON")?; + let result = serde_json::from_str(&fs::read_to_string(&output_file_path).with_context(|| { + format!( + "Could not read the child process output from '{}'", + output_file_path.display() + ) + })?) + .context("Could not parse the child process output to JSON")?; Ok(result) } From 87fd981028afc3de25dc317c3f13d404a1388b09 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Fri, 30 Jan 2026 01:54:20 +0400 Subject: [PATCH 046/114] fix transport-sample-accurate test; cargo fmt run; add 'default' transport info; extra checks for audio-ports-config; --- Cargo.lock | 67 ++-- Cargo.toml | 4 +- src/commands.rs | 10 +- src/commands/list.rs | 63 +--- src/commands/validate.rs | 3 +- src/index.rs | 42 +-- src/plugin/ext/ambisonic.rs | 4 +- src/plugin/ext/audio_ports.rs | 142 ++++---- src/plugin/ext/audio_ports_activation.rs | 27 +- src/plugin/ext/audio_ports_config.rs | 52 ++- src/plugin/ext/configurable_audio_ports.rs | 14 +- src/plugin/ext/note_ports.rs | 41 +-- src/plugin/ext/params.rs | 80 ++--- src/plugin/ext/preset_load.rs | 4 +- src/plugin/ext/state.rs | 27 +- src/plugin/instance.rs | 12 +- src/plugin/instance/main_thread.rs | 16 +- src/plugin/instance/shared.rs | 2 +- src/plugin/preset_discovery.rs | 17 +- src/plugin/preset_discovery/indexer.rs | 56 +-- .../preset_discovery/metadata_receiver.rs | 174 ++++------ src/plugin/preset_discovery/provider.rs | 53 +-- src/plugin/process.rs | 10 +- src/plugin/process/events.rs | 5 + src/plugin/process/transport.rs | 19 +- src/tests.rs | 19 +- src/tests/plugin.rs | 44 ++- src/tests/plugin/descriptor.rs | 12 +- src/tests/plugin/layout.rs | 120 ++++--- src/tests/plugin/params.rs | 190 ++++------- src/tests/plugin/state.rs | 320 +++++------------- src/tests/plugin/transport.rs | 27 +- src/tests/plugin_library.rs | 38 +-- src/tests/plugin_library/factories.rs | 30 +- src/tests/plugin_library/preset_discovery.rs | 94 ++--- src/tests/plugin_library/scanning.rs | 21 +- src/tests/rng.rs | 54 +-- src/util.rs | 8 +- src/validator.rs | 12 +- tests/clack-synth/Cargo.toml | 4 +- tests/clack-synth/src/lib.rs | 87 ++--- tests/clack-synth/src/params.rs | 19 +- tests/clack-synth/src/poly_oscillator.rs | 18 +- 43 files changed, 852 insertions(+), 1209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b7ed2a..c700d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "clack-common" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?branch=configurable-audio-ports#14286696600ef76b9f58f374c0e26973a1bbf591" +source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" dependencies = [ "bitflags 2.10.0", "clap-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "clack-extensions" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?branch=configurable-audio-ports#14286696600ef76b9f58f374c0e26973a1bbf591" +source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" dependencies = [ "bitflags 2.10.0", "clack-common", @@ -189,7 +189,7 @@ dependencies = [ [[package]] name = "clack-plugin" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?branch=configurable-audio-ports#14286696600ef76b9f58f374c0e26973a1bbf591" +source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" dependencies = [ "clack-common", "clap-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -235,7 +235,7 @@ dependencies = [ "clap-sys 0.5.0 (git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156)", "colored", "core-foundation", - "crossbeam-utils", + "crossbeam", "either", "libloading", "log", @@ -317,28 +317,54 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] @@ -530,15 +556,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "midi-consts" version = "0.1.0" @@ -786,12 +803,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index d18b7b8..7dd2a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "clap-validator" version = "0.3.2" edition = "2024" license = "MIT" -rust-version = "1.87.0" # MSRV +rust-version = "1.89.0" # MSRV description = "A validator and automatic test suite for CLAP plugins" readme = "README.md" @@ -24,7 +24,7 @@ clap = { version = "4.1.8", features = ["derive", "wrap_help"] } # For CLAP 1.2.2 support clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } colored = "3.0.0" -crossbeam-utils = "0.8.21" +crossbeam = "0.8.4" libloading = "0.9.0" log = "0.4" log-panics = { version = "2.0", features = ["with-backtrace"] } diff --git a/src/commands.rs b/src/commands.rs index 2c4b6cc..1300c0a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -49,10 +49,7 @@ impl TextWrapper { .indent_strings .entry(subsequent_indent_width) .or_insert_with(|| " ".repeat(subsequent_indent_width)); - let wrapping_options = self - .wrapping_options - .clone() - .subsequent_indent(indent_string); + let wrapping_options = self.wrapping_options.clone().subsequent_indent(indent_string); println!("{}", textwrap::fill(text.as_ref(), wrapping_options)); } @@ -74,9 +71,6 @@ impl TextWrapper { /// The number of characters until the start of the string, ignoring spaces and dashes. fn auto_indent_width(text: impl AsRef) -> usize { - text.as_ref() - .chars() - .take_while(|&c| c == ' ' || c == '-') - .count() + text.as_ref().chars().take_while(|&c| c == ' ' || c == '-').count() } } diff --git a/src/commands/list.rs b/src/commands/list.rs index d921091..eae0e53 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -179,10 +179,7 @@ where wrapper, " - {} ({}) (contains {} {}, {} {}):", provider_result.provider_name, - provider_result - .provider_vendor - .as_deref() - .unwrap_or("unknown vendor"), + provider_result.provider_vendor.as_deref().unwrap_or("unknown vendor"), provider_result.soundpacks.len(), if provider_result.soundpacks.len() == 1 { "soundpack" @@ -237,12 +234,7 @@ where println_wrapped!(wrapper, " - {}", preset_uri); println!(); - println_wrapped!( - wrapper, - " {} ({})", - preset.name, - preset.plugin_ids_string() - ); + println_wrapped!(wrapper, " {} ({})", preset.name, preset.plugin_ids_string()); if let Some(description) = preset.description { println_wrapped_no_indent!(wrapper, " {}", description); } @@ -270,18 +262,10 @@ where } println_wrapped!(wrapper, " flags: {}", preset.flags); if !preset.features.is_empty() { - println_wrapped!( - wrapper, - " features: [{}]", - preset.features.join(", ") - ); + println_wrapped!(wrapper, " features: [{}]", preset.features.join(", ")); } if !preset.extra_info.is_empty() { - println_wrapped!( - wrapper, - " extra info: {:#?}", - preset.extra_info - ); + println_wrapped!(wrapper, " extra info: {:#?}", preset.extra_info); } } PresetFile::Container(presets) => { @@ -290,11 +274,7 @@ where " - {} (contains {} {})", preset_uri, presets.len(), - if presets.len() == 1 { - "preset" - } else { - "presets" - } + if presets.len() == 1 { "preset" } else { "presets" } ); for (load_key, preset) in presets { @@ -307,11 +287,7 @@ where preset.plugin_ids_string() ); if let Some(description) = preset.description { - println_wrapped_no_indent!( - wrapper, - " {}", - description - ); + println_wrapped_no_indent!(wrapper, " {}", description); } println!(); if !preset.creators.is_empty() { @@ -327,37 +303,20 @@ where ); } if let Some(soundpack_id) = preset.soundpack_id { - println_wrapped!( - wrapper, - " soundpack: {soundpack_id}" - ); + println_wrapped!(wrapper, " soundpack: {soundpack_id}"); } if let Some(creation_time) = preset.creation_time { - println_wrapped!( - wrapper, - " created: {creation_time}" - ); + println_wrapped!(wrapper, " created: {creation_time}"); } if let Some(modification_time) = preset.modification_time { - println_wrapped!( - wrapper, - " modified: {modification_time}" - ); + println_wrapped!(wrapper, " modified: {modification_time}"); } println_wrapped!(wrapper, " flags: {}", preset.flags); if !preset.features.is_empty() { - println_wrapped!( - wrapper, - " features: [{}]", - preset.features.join(", ") - ); + println_wrapped!(wrapper, " features: [{}]", preset.features.join(", ")); } if !preset.extra_info.is_empty() { - println_wrapped!( - wrapper, - " extra info: {:#?}", - preset.extra_info - ); + println_wrapped!(wrapper, " extra info: {:#?}", preset.extra_info); } } } diff --git a/src/commands/validate.rs b/src/commands/validate.rs index fb41215..9ae9a48 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -75,8 +75,7 @@ pub struct SingleTestSettings { /// The main validator command. This will validate one or more plugins and print the results. pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { - let mut result = - validator::validate(verbosity, settings).context("Could not run the validator")?; + let mut result = validator::validate(verbosity, settings).context("Could not run the validator")?; let tally = result.tally(); // Filtering out tests should be done after we did the tally for consistency's sake diff --git a/src/index.rs b/src/index.rs index 9333328..9a8de75 100644 --- a/src/index.rs +++ b/src/index.rs @@ -109,12 +109,9 @@ where let library = crate::plugin::library::PluginLibrary::load(path) .with_context(|| format!("Could not load '{}'", path.display()))?; - let preset_discovery_factory = library.preset_discovery_factory().with_context(|| { - format!( - "Could not get the preset discovery factory for '{}", - path.display() - ) - }); + let preset_discovery_factory = library + .preset_discovery_factory() + .with_context(|| format!("Could not get the preset discovery factory for '{}", path.display())); if preset_discovery_factory.is_err() && skip_unsupported { continue; } @@ -128,24 +125,16 @@ where for provider_metadata in metadata { let provider = factory .create_provider(&provider_metadata) - .with_context(|| { - format!( - "Could not create the provider with ID '{}'", - provider_metadata.id - ) - })?; + .with_context(|| format!("Could not create the provider with ID '{}'", provider_metadata.id))?; let declared_data = provider.declared_data(); let mut presets = BTreeMap::new(); for location in &declared_data.locations { presets.extend(provider.crawl_location(location).with_context(|| { format!( - "Error occurred while crawling presets for the location '{}' with {} \ - using provider '{}' with ID '{}'", - location.name, - location.value, - provider_metadata.name, - provider_metadata.id, + "Error occurred while crawling presets for the location '{}' with {} using provider '{}' \ + with ID '{}'", + location.name, location.value, provider_metadata.name, provider_metadata.id, ) })?); } @@ -163,16 +152,14 @@ where match result { Ok(provider_results) => { - index.0.insert( - path.to_owned(), - PresetIndexResult::Success(provider_results), - ); + index + .0 + .insert(path.to_owned(), PresetIndexResult::Success(provider_results)); } Err(err) => { - index.0.insert( - path.to_owned(), - PresetIndexResult::Error(format!("{err:#}")), - ); + index + .0 + .insert(path.to_owned(), PresetIndexResult::Error(format!("{err:#}"))); } } } @@ -212,8 +199,7 @@ pub fn clap_directories() -> Result> { /// error if the paths could not be parsed correctly. #[cfg(windows)] pub fn clap_directories() -> Result> { - let common_files = - std::env::var("COMMONPROGRAMFILES").context("'$COMMONPROGRAMFILES' is not set")?; + let common_files = std::env::var("COMMONPROGRAMFILES").context("'$COMMONPROGRAMFILES' is not set")?; let local_appdata = std::env::var("LOCALAPPDATA").context("'$LOCALAPPDATA' is not set")?; // TODO: Does this work reliably? There are dedicated Win32 API functions for getting these diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index ddbb540..5f084e0 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -1,7 +1,5 @@ use crate::plugin::{ext::Extension, instance::Plugin}; -use clap_sys::ext::ambisonic::{ - CLAP_EXT_AMBISONIC, CLAP_EXT_AMBISONIC_COMPAT, clap_plugin_ambisonic, -}; +use clap_sys::ext::ambisonic::{CLAP_EXT_AMBISONIC, CLAP_EXT_AMBISONIC_COMPAT, clap_plugin_ambisonic}; use std::{ffi::CStr, ptr::NonNull}; #[allow(unused)] diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 68deb81..ab1e3f3 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -7,12 +7,7 @@ use crate::plugin::instance::Plugin; use crate::util::clap_call; use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; -use clap_sys::ext::audio_ports::{ - CLAP_AUDIO_PORT_IS_MAIN, CLAP_AUDIO_PORT_PREFERS_64BITS, - CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE, CLAP_AUDIO_PORT_SUPPORTS_64BITS, - CLAP_EXT_AUDIO_PORTS, CLAP_PORT_MONO, CLAP_PORT_STEREO, clap_audio_port_info, - clap_plugin_audio_ports, -}; +use clap_sys::ext::audio_ports::*; use clap_sys::ext::surround::CLAP_PORT_SURROUND; use clap_sys::id::CLAP_INVALID_ID; use std::collections::HashMap; @@ -107,20 +102,25 @@ impl AudioPorts<'_> { if !success { anyhow::bail!( - "Plugin returned an error when querying input audio port {i} ({num_inputs} \ - total input ports)." + "Plugin returned an error when querying input audio port {i} ({num_inputs} total input ports)." ); } - is_audio_port_type_consistent(&info, has_ambisonic, has_surround) - .with_context(|| format!("Inconsistent type for output port {i}"))?; + is_audio_port_type_consistent( + if info.port_type.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(info.port_type) }) + }, + info.channel_count, + has_ambisonic, + has_surround, + ) + .with_context(|| format!("Inconsistent channel count for output port {i}"))?; // We'll convert these stable IDs to vector indices later if input_stable_index_pairs.contains_key(&info.id) { - anyhow::bail!( - "The stable ID of input audio port {i} (id={}) is a duplicate.", - info.id - ); + anyhow::bail!("The stable ID of input audio port {i} (id={}) is a duplicate.", info.id); } input_stable_index_pairs.insert(info.id, (i as usize, info.in_place_pair)); @@ -128,21 +128,18 @@ impl AudioPorts<'_> { let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; if is_main && i != 0 { anyhow::bail!( - "Input audio port {i} (id={}) is marked as main, but it is not the first port \ - in the list.", + "Input audio port {i} (id={}) is marked as main, but it is not the first port in the list.", info.id ); } let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; - let requires_common_sample_size = - (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; if prefers_double_sample_size && !supports_double_sample_size { anyhow::bail!( - "Input audio port {i} (id={}) prefers 64-bit sample size, but does not \ - support it.", + "Input audio port {i} (id={}) prefers 64-bit sample size, but does not support it.", info.id ); } @@ -175,13 +172,21 @@ impl AudioPorts<'_> { if !success { anyhow::bail!( - "Plugin returned an error when querying output audio port {i} ({num_outputs} \ - total output ports)." + "Plugin returned an error when querying output audio port {i} ({num_outputs} total output ports)." ); } - is_audio_port_type_consistent(&info, has_ambisonic, has_surround) - .with_context(|| format!("Inconsistent channel count for output port {i}"))?; + is_audio_port_type_consistent( + if info.port_type.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(info.port_type) }) + }, + info.channel_count, + has_ambisonic, + has_surround, + ) + .with_context(|| format!("Inconsistent channel count for output port {i}"))?; if output_stable_index_pairs.contains_key(&info.id) { anyhow::bail!( @@ -194,21 +199,18 @@ impl AudioPorts<'_> { let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; if is_main && i != 0 { anyhow::bail!( - "Output audio port {i} (id={}) is marked as main, but it is not the first \ - port in the list.", + "Output audio port {i} (id={}) is marked as main, but it is not the first port in the list.", info.id ); } let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; - let requires_common_sample_size = - (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; if prefers_double_sample_size && !supports_double_sample_size { anyhow::bail!( - "Output audio port {i} (id={}) prefers 64-bit sample size, but does not \ - support it.", + "Output audio port {i} (id={}) prefers 64-bit sample size, but does not support it.", info.id ); } @@ -235,8 +237,8 @@ impl AudioPorts<'_> { // 32bit sample size) and nullifies the 64 bit support of the other ports if has_single_precision_requires_common_port && has_double_precision_requires_common_port { anyhow::bail!( - "The plugin has audio ports that require common sample size, but some of these \ - ports only support 32-bit sample size while others support 64-bit sample size." + "The plugin has audio ports that require common sample size, but some of these ports only support \ + 32-bit sample size while others support 64-bit sample size." ); } @@ -258,17 +260,15 @@ impl AudioPorts<'_> { } Some((output_stable_id, (pair_output_port_idx, output_pair_stable_id))) => { anyhow::bail!( - "Input port {input_port_idx} with stable ID {input_stable_id} is \ - connected to output port {pair_output_port_idx} with stable ID \ - {output_stable_id} through an in-place pair, but the relation is not \ - symmetrical. The output port reports to have an in-place pair with \ - stable ID {output_pair_stable_id}." + "Input port {input_port_idx} with stable ID {input_stable_id} is connected to output port \ + {pair_output_port_idx} with stable ID {output_stable_id} through an in-place pair, but the \ + relation is not symmetrical. The output port reports to have an in-place pair with stable ID \ + {output_pair_stable_id}." ) } None => anyhow::bail!( - "Input port {input_port_idx} with stable ID {input_stable_id} claims to be \ - connected to an output port with stable ID {pair_stable_id} through an \ - in-place pair, but this port does not exist." + "Input port {input_port_idx} with stable ID {input_stable_id} claims to be connected to an output \ + port with stable ID {pair_stable_id} through an in-place pair, but this port does not exist." ), } } @@ -283,9 +283,7 @@ impl AudioPorts<'_> { .iter() .find(|(input_stable_id, (_, _))| *input_stable_id == pair_stable_id) { - Some((_, (pair_input_port_idx, input_pair_stable_id))) - if input_pair_stable_id == output_stable_id => - { + Some((_, (pair_input_port_idx, input_pair_stable_id))) if input_pair_stable_id == output_stable_id => { // We should have already done this. If this is not the case, then this is an // error in the validator assert_eq!( @@ -299,17 +297,16 @@ impl AudioPorts<'_> { } Some((input_stable_id, (pair_input_port_idx, input_pair_stable_id))) => { anyhow::bail!( - "Output port {output_port_idx} with stable ID {output_stable_id} is \ - connected to input port {pair_input_port_idx} with stable ID \ - {input_stable_id} through an in-place pair, but the relation is not \ - symmetrical. The input port reports to have an in-place pair with stable \ - ID {input_pair_stable_id}." + "Output port {output_port_idx} with stable ID {output_stable_id} is connected to input port \ + {pair_input_port_idx} with stable ID {input_stable_id} through an in-place pair, but the \ + relation is not symmetrical. The input port reports to have an in-place pair with stable ID \ + {input_pair_stable_id}." ) } None => anyhow::bail!( - "Output port {output_port_idx} with stable ID {output_stable_id} claims to be \ - connected to an input port with stable ID {pair_stable_id} through an \ - in-place pair, but this port does not exist." + "Output port {output_port_idx} with stable ID {output_stable_id} claims to be connected to an \ + input port with stable ID {pair_stable_id} through an in-place pair, but this port does not \ + exist." ), } } @@ -320,57 +317,50 @@ impl AudioPorts<'_> { /// Check whether the number of channels matches an audio port's type string, if that is set. /// Returns an error if the port type is not consistent -fn is_audio_port_type_consistent( - info: &clap_audio_port_info, +pub fn is_audio_port_type_consistent( + port_type: Option<&CStr>, + channel_count: u32, has_ambisonic: bool, has_surround: bool, ) -> Result<()> { - if info.port_type.is_null() { + if port_type.is_none() { return Ok(()); } - let port_type = unsafe { CStr::from_ptr(info.port_type) }; - if port_type == CLAP_PORT_MONO { - if info.channel_count == 1 { + if port_type == Some(CLAP_PORT_MONO) { + if channel_count == 1 { Ok(()) } else { - anyhow::bail!( - "Expected 1 channel, but the audio port has {} channels.", - info.channel_count - ); + anyhow::bail!("Expected 1 channel, but the audio port has {} channels.", channel_count); } - } else if port_type == CLAP_PORT_STEREO { - if info.channel_count == 2 { + } else if port_type == Some(CLAP_PORT_STEREO) { + if channel_count == 2 { Ok(()) } else { anyhow::bail!( "Expected 2 channels, but the audio port has {} channel(s).", - info.channel_count + channel_count ); } - } else if port_type == CLAP_PORT_SURROUND { + } else if port_type == Some(CLAP_PORT_SURROUND) { if !has_surround { - anyhow::bail!( - "Audio port type is 'surround', but the plugin does not implement the 'surround' \ - extension." - ); + anyhow::bail!("Audio port type is 'surround', but the plugin does not implement the 'surround' extension."); } Ok(()) - } else if port_type == CLAP_PORT_AMBISONIC { + } else if port_type == Some(CLAP_PORT_AMBISONIC) { if !has_ambisonic { anyhow::bail!( - "Audio port type is 'ambisonic', but the plugin does not implement the \ - 'ambisonic' extension." + "Audio port type is 'ambisonic', but the plugin does not implement the 'ambisonic' extension." ); } // ambisonic audio requires (N^2) channels where N is the ambisonics order - if info.channel_count.isqrt().pow(2) != info.channel_count { + if channel_count.isqrt().pow(2) != channel_count { anyhow::bail!( - "Expected a perfect square (1, 4, 9, ...) number of channels for ambisonic audio \ - port, but the audio port has {} channels.", - info.channel_count + "Expected a perfect square (N^2 where N is the ambisonics order) number of channels for ambisonic \ + audio port, but the audio port has {} channels.", + channel_count ); } diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index 023a071..a8e9b05 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -1,4 +1,7 @@ -use crate::plugin::{ext::Extension, instance::Plugin}; +use crate::{ + plugin::{ext::Extension, instance::Plugin}, + util::clap_call, +}; use clap_sys::ext::audio_ports_activation::*; use std::{ffi::CStr, ptr::NonNull}; @@ -20,3 +23,25 @@ impl<'a> Extension<&'a Plugin<'a>> for AudioPortsActivation<'a> { } } } + +impl<'a> AudioPortsActivation<'a> { + /// TODO: extra test where we do this while processing + pub fn can_activate_while_processing(&self) -> bool { + let audio_ports_activation = self.audio_ports_activation.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { audio_ports_activation=>can_activate_while_processing(plugin) } + } + } + + /// Activates or deactivates audio ports while inactive. + pub fn set_active(&mut self, is_input: bool, port_index: u32, is_active: bool, sample_size: u32) -> bool { + self.plugin.status().assert_inactive(); + + let audio_ports_activation = self.audio_ports_activation.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { audio_ports_activation=>set_active(plugin, is_input, port_index, is_active, sample_size) } + } + } +} diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 4e490d0..dec9680 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -1,12 +1,11 @@ use crate::plugin::ext::Extension; +use crate::plugin::ext::ambisonic::Ambisonic; +use crate::plugin::ext::audio_ports::is_audio_port_type_consistent; +use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; use crate::util::{c_char_slice_to_string, clap_call}; -use anyhow::Result; -use clap_sys::ext::audio_ports_config::{ - CLAP_EXT_AUDIO_PORTS_CONFIG, CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, - CLAP_EXT_AUDIO_PORTS_CONFIG_INFO_COMPAT, clap_audio_ports_config, - clap_plugin_audio_ports_config, clap_plugin_audio_ports_config_info, -}; +use anyhow::{Context, Result}; +use clap_sys::ext::audio_ports_config::*; use clap_sys::id::clap_id; use std::ffi::CStr; use std::mem::zeroed; @@ -66,6 +65,9 @@ impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfigInfo<'a> { impl AudioPortsConfig<'_> { pub fn enumerate(&self) -> Result> { + let has_ambisonic = self.plugin.get_extension::().is_some(); + let has_surround = self.plugin.get_extension::().is_some(); + let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); let count = unsafe { @@ -80,17 +82,41 @@ impl AudioPortsConfig<'_> { anyhow::bail!("audio_ports_config::get({}) returned false", i); } + if dst.has_main_input { + is_audio_port_type_consistent( + if dst.main_input_port_type.is_null() { + None + } else { + Some(CStr::from_ptr(dst.main_input_port_type)) + }, + dst.main_input_channel_count, + has_ambisonic, + has_surround, + ) + .with_context(|| format!("Inconsistent channel count for main input port for config {i}"))?; + } + + if dst.has_main_output { + is_audio_port_type_consistent( + if dst.main_output_port_type.is_null() { + None + } else { + Some(CStr::from_ptr(dst.main_output_port_type)) + }, + dst.main_output_channel_count, + has_ambisonic, + has_surround, + ) + .with_context(|| format!("Inconsistent channel count for main output port for config {i}"))?; + } + Ok(AudioPortsConfigConfig { id: dst.id, name: c_char_slice_to_string(&dst.name)?, input_port_count: dst.input_port_count, output_port_count: dst.output_port_count, - main_input_channel_count: dst - .has_main_input - .then_some(dst.main_input_channel_count), - main_output_channel_count: dst - .has_main_output - .then_some(dst.main_output_channel_count), + main_input_channel_count: dst.has_main_input.then_some(dst.main_input_channel_count), + main_output_channel_count: dst.has_main_output.then_some(dst.main_output_channel_count), }) }) .collect() @@ -120,6 +146,4 @@ impl AudioPortsConfigInfo<'_> { clap_call! { audio_ports_config_info=>current_config(plugin) } } } - - // TODO: } diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index ec1fc58..b48ddc6 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -3,8 +3,8 @@ use crate::plugin::instance::Plugin; use crate::util::clap_call; use clap_sys::ext::audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}; use clap_sys::ext::configurable_audio_ports::{ - CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, - clap_audio_port_configuration_request, clap_plugin_configurable_audio_ports, + CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, clap_audio_port_configuration_request, + clap_plugin_configurable_audio_ports, }; use std::ffi::CStr; use std::ptr::{NonNull, null}; @@ -39,10 +39,7 @@ impl<'a> Extension<&'a Plugin<'a>> for ConfigurableAudioPorts<'a> { } impl<'a> ConfigurableAudioPorts<'a> { - pub fn can_apply_configuration( - &self, - requests: impl IntoIterator, - ) -> bool { + pub fn can_apply_configuration(&self, requests: impl IntoIterator) -> bool { self.plugin.status().assert_inactive(); let requests = requests @@ -72,10 +69,7 @@ impl<'a> ConfigurableAudioPorts<'a> { } } - pub fn apply_configuration( - &self, - requests: impl IntoIterator, - ) -> bool { + pub fn apply_configuration(&self, requests: impl IntoIterator) -> bool { self.plugin.status().assert_inactive(); let requests = requests diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index dd1a584..376f101 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -4,10 +4,7 @@ use super::Extension; use crate::plugin::instance::Plugin; use crate::util::clap_call; use anyhow::Result; -use clap_sys::ext::note_ports::{ - CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, - CLAP_NOTE_DIALECT_MIDI_MPE, clap_note_dialect, clap_note_port_info, clap_plugin_note_ports, -}; +use clap_sys::ext::note_ports::*; use std::collections::HashSet; use std::ffi::CStr; use std::mem; @@ -78,32 +75,26 @@ impl NotePorts<'_> { }; if !success { anyhow::bail!( - "Plugin returned an error when querying input note port {i} ({num_inputs} \ - total input ports)." + "Plugin returned an error when querying input note port {i} ({num_inputs} total input ports)." ); } let num_preferred_dialects = info.preferred_dialect.count_ones(); if num_preferred_dialects != 1 { - anyhow::bail!( - "Plugin prefers {num_preferred_dialects} dialects for input note port {i}." - ); + anyhow::bail!("Plugin prefers {num_preferred_dialects} dialects for input note port {i}."); } if (info.supported_dialects & info.preferred_dialect) == 0 { anyhow::bail!( - "Plugin prefers note dialect {:#b} for input note port {i} which is not \ - contained within the supported note dialects field ({:#b}).", + "Plugin prefers note dialect {:#b} for input note port {i} which is not contained within the \ + supported note dialects field ({:#b}).", info.preferred_dialect, info.supported_dialects ); } if !input_stable_indices.insert(info.id) { - anyhow::bail!( - "The stable ID of input note port {i} ({}) is a duplicate.", - info.id - ); + anyhow::bail!("The stable ID of input note port {i} ({}) is a duplicate.", info.id); } config.inputs.push(NotePort { @@ -122,32 +113,26 @@ impl NotePorts<'_> { }; if !success { anyhow::bail!( - "Plugin returned an error when querying output note port {i} ({num_outputs} \ - total output ports)." + "Plugin returned an error when querying output note port {i} ({num_outputs} total output ports)." ); } let num_preferred_dialects = info.preferred_dialect.count_ones(); if num_preferred_dialects != 1 { - anyhow::bail!( - "Plugin prefers {num_preferred_dialects} dialects for output note port {i}." - ); + anyhow::bail!("Plugin prefers {num_preferred_dialects} dialects for output note port {i}."); } if (info.supported_dialects & info.preferred_dialect) == 0 { anyhow::bail!( - "Plugin prefers note dialect {:#b} for output note port {i} which is not \ - contained within the supported note dialects field ({:#b}).", + "Plugin prefers note dialect {:#b} for output note port {i} which is not contained within the \ + supported note dialects field ({:#b}).", info.preferred_dialect, info.supported_dialects ); } if !output_stable_indices.insert(info.id) { - anyhow::bail!( - "The stable ID of output note port {i} ({}) is a duplicate.", - info.id - ); + anyhow::bail!("The stable ID of output note port {i} ({}) is a duplicate.", info.id); } config.outputs.push(NotePort { @@ -170,8 +155,6 @@ impl NotePort { pub fn supports_midi(&self) -> bool { self.supported_dialects.contains(&CLAP_NOTE_DIALECT_MIDI) - || self - .supported_dialects - .contains(&CLAP_NOTE_DIALECT_MIDI_MPE) + || self.supported_dialects.contains(&CLAP_NOTE_DIALECT_MIDI_MPE) } } diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 910ff30..a54cb79 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -71,9 +71,7 @@ impl Params<'_> { if result { Ok(value) } else { - anyhow::bail!( - "'clap_plugin_params::get_value()' returned false for parameter ID {param_id}." - ); + anyhow::bail!("'clap_plugin_params::get_value()' returned false for parameter ID {param_id}."); } } @@ -97,14 +95,11 @@ impl Params<'_> { }; if result { - c_char_slice_to_string(&string_buffer) - .map(Some) - .with_context(|| { - format!( - "Could not convert the string representation of {value} for parameter \ - {param_id} to a UTF-8 string" - ) - }) + c_char_slice_to_string(&string_buffer).map(Some).with_context(|| { + format!( + "Could not convert the string representation of {value} for parameter {param_id} to a UTF-8 string" + ) + }) } else { Ok(None) } @@ -154,18 +149,11 @@ impl Params<'_> { }; if !success { - anyhow::bail!( - "Plugin returned an error when querying parameter {i} ({num_params} total \ - parameters)." - ); + anyhow::bail!("Plugin returned an error when querying parameter {i} ({num_params} total parameters)."); } - let name = util::c_char_slice_to_string(&info.name).with_context(|| { - format!( - "Could not read the name for parameter with stable ID {}", - info.id - ) - })?; + let name = util::c_char_slice_to_string(&info.name) + .with_context(|| format!("Could not read the name for parameter with stable ID {}", info.id))?; // We don't use the module string, but we'll still check it for consistency. Basically // anything goes here as long as there are no trailing, leading, or multiple subsequent @@ -178,24 +166,21 @@ impl Params<'_> { })?; if module.starts_with('/') { anyhow::bail!( - "The module name for parameter '{}' (stable ID {}) starts with a leading \ - slash: '{}'.", + "The module name for parameter '{}' (stable ID {}) starts with a leading slash: '{}'.", &name, info.id, module ) } else if module.ends_with('/') { anyhow::bail!( - "The module name for parameter '{}' (stable ID {}) ends with a trailing \ - slash: '{}'.", + "The module name for parameter '{}' (stable ID {}) ends with a trailing slash: '{}'.", &name, info.id, module ) } else if module.contains("//") { anyhow::bail!( - "The module name for parameter '{}' (stable ID {}) contains multiple \ - subsequent slashes: '{}'.", + "The module name for parameter '{}' (stable ID {}) contains multiple subsequent slashes: '{}'.", &name, info.id, module @@ -205,8 +190,8 @@ impl Params<'_> { let range = info.min_value..=info.max_value; if info.min_value > info.max_value { anyhow::bail!( - "Parameter '{}' (stable ID {}) has a minimum value ({:?}) that's higher than \ - it's maximum value ({:?}).", + "Parameter '{}' (stable ID {}) has a minimum value ({:?}) that's higher than it's maximum value \ + ({:?}).", &name, info.id, info.min_value, @@ -215,8 +200,8 @@ impl Params<'_> { } if !range.contains(&info.default_value) { anyhow::bail!( - "Parameter '{}' (stable ID {}) has a default value ({:?}) that falls outside \ - of its value range ({:?}).", + "Parameter '{}' (stable ID {}) has a default value ({:?}) that falls outside of its value range \ + ({:?}).", &name, info.id, info.default_value, @@ -226,8 +211,8 @@ impl Params<'_> { if (info.flags & CLAP_PARAM_IS_STEPPED) != 0 { if info.min_value != info.min_value.trunc() { anyhow::bail!( - "Parameter '{}' (stable ID {}) is a stepped parameter, but its minimum \ - value ({:?}) is not an integer.", + "Parameter '{}' (stable ID {}) is a stepped parameter, but its minimum value ({:?}) is not an \ + integer.", &name, info.id, info.min_value, @@ -235,8 +220,8 @@ impl Params<'_> { } if info.max_value != info.max_value.trunc() { anyhow::bail!( - "Parameter '{}' (stable ID {}) is a stepped parameter, but its maximum \ - value ({:?}) is not an integer.", + "Parameter '{}' (stable ID {}) is a stepped parameter, but its maximum value ({:?}) is not an \ + integer.", &name, info.id, info.max_value, @@ -255,8 +240,7 @@ impl Params<'_> { if (info.flags & CLAP_PARAM_IS_STEPPED) == 0 { anyhow::bail!( - "Parameter '{}' (stable ID {}) is a bypass parameter, but it is not \ - stepped.", + "Parameter '{}' (stable ID {}) is a bypass parameter, but it is not stepped.", &name, info.id ) @@ -275,8 +259,8 @@ impl Params<'_> { != 0 { anyhow::bail!( - "Parameter '{}' (stable ID {}) is automatable per note ID, key, channel, or \ - port, but does not have CLAP_PARAM_IS_AUTOMATABLE. This is likely a bug.", + "Parameter '{}' (stable ID {}) is automatable per note ID, key, channel, or port, but does not \ + have CLAP_PARAM_IS_AUTOMATABLE. This is likely a bug.", &name, info.id ) @@ -290,19 +274,18 @@ impl Params<'_> { != 0 { anyhow::bail!( - "Parameter '{}' (stable ID {}) is modulatable per note ID, key, channel, or \ - port, but does not have CLAP_PARAM_IS_MODULATABLE. This is likely a bug.", + "Parameter '{}' (stable ID {}) is modulatable per note ID, key, channel, or port, but does not \ + have CLAP_PARAM_IS_MODULATABLE. This is likely a bug.", &name, info.id ) } if ((info.flags & CLAP_PARAM_IS_READONLY) != 0) - && ((info.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0 - || (info.flags & CLAP_PARAM_IS_MODULATABLE) != 0) + && ((info.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0 || (info.flags & CLAP_PARAM_IS_MODULATABLE) != 0) { anyhow::bail!( - "Parameter '{}' (stable ID {}) has the CLAP_PARAM_IS_READONLY flag set, but \ - it is also marked as automatable or modulatable. This is likely a bug.", + "Parameter '{}' (stable ID {}) has the CLAP_PARAM_IS_READONLY flag set, but it is also marked as \ + automatable or modulatable. This is likely a bug.", &name, info.id ) @@ -316,10 +299,7 @@ impl Params<'_> { flags: info.flags, }; if result.insert(info.id, processed_info).is_some() { - anyhow::bail!( - "The plugin contains multiple parameters with stable ID {}.", - info.id - ); + anyhow::bail!("The plugin contains multiple parameters with stable ID {}.", info.id); } } @@ -336,6 +316,8 @@ impl Params<'_> { // main thread interface for the parameters extension. self.status().assert_inactive(); + assert!(input_events.is_sorted(), "Input event queue must be sorted."); + let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); unsafe { diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 558adce..1f3818e 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -39,9 +39,7 @@ impl PresetLoad<'_> { pub fn from_location(&self, location: &LocationValue, load_key: Option<&str>) -> Result<()> { let (location_kind, location_ptr) = location.to_raw(); let load_key_cstring = load_key - .map(|load_key| { - CString::new(load_key).context("Load key contained internal null bytes") - }) + .map(|load_key| CString::new(load_key).context("Load key contained internal null bytes")) .transpose()?; let preset_load = self.preset_load.as_ptr(); diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 8da15ef..3a15bde 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -97,8 +97,8 @@ impl State<'_> { Ok(stream.into_vec()) } else { anyhow::bail!( - "'clap_plugin_state::save()' returned false when only allowing the plugin to \ - write {max_bytes} bytes at a time." + "'clap_plugin_state::save()' returned false when only allowing the plugin to write {max_bytes} bytes \ + at a time." ); } } @@ -135,8 +135,8 @@ impl State<'_> { Ok(()) } else { anyhow::bail!( - "'clap_plugin_state::load()' returned false when only allowing the plugin to read \ - {max_bytes} bytes at a time." + "'clap_plugin_state::load()' returned false when only allowing the plugin to read {max_bytes} bytes \ + at a time." ); } } @@ -186,8 +186,7 @@ impl<'a> InputStream<'a> { let current_pos = this.read_position.load(Ordering::Relaxed); let bytes_to_read = (this.buffer.len() - current_pos).min(size as usize); - this.read_position - .fetch_add(bytes_to_read, Ordering::Relaxed); + this.read_position.fetch_add(bytes_to_read, Ordering::Relaxed); std::slice::from_raw_parts_mut(buffer as *mut u8, bytes_to_read) .copy_from_slice(&this.buffer[current_pos..current_pos + bytes_to_read]); @@ -231,17 +230,10 @@ impl OutputStream { /// Get the byte buffer from this stream. pub fn into_vec(self: Pin>) -> Vec { // SAFETY: We can safely grab this inner buffer because this consumes the Box - unsafe { Pin::into_inner_unchecked(self) } - .buffer - .into_inner() - .unwrap() + unsafe { Pin::into_inner_unchecked(self) }.buffer.into_inner().unwrap() } - unsafe extern "C" fn write( - stream: *const clap_ostream, - buffer: *const c_void, - size: u64, - ) -> i64 { + unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { unsafe { check_null_ptr!(stream, (*stream).ctx, buffer); let this = &*((*stream).ctx as *const Self); @@ -255,10 +247,7 @@ impl OutputStream { this.buffer .lock() .unwrap() - .extend_from_slice(std::slice::from_raw_parts( - buffer as *const u8, - size as usize, - )); + .extend_from_slice(std::slice::from_raw_parts(buffer as *const u8, size as usize)); size as i64 } diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index 818103c..ae50118 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -60,8 +60,7 @@ impl PluginStatus { pub fn assert_is(&self, expected: PluginStatus) { if *self != expected { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be {:?}). This is \ - a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be {:?})", self, expected ) } @@ -71,8 +70,7 @@ impl PluginStatus { pub fn assert_is_not(&self, unexpected: PluginStatus) { if *self == unexpected { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must not be {:?}). \ - This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must not be {:?})", self, unexpected ) } @@ -82,8 +80,7 @@ impl PluginStatus { pub fn assert_active(&self) { if *self < PluginStatus::Activated { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be activated). \ - This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be activated)", self ) } @@ -93,8 +90,7 @@ impl PluginStatus { pub fn assert_inactive(&self) { if *self >= PluginStatus::Activated { panic!( - "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be deactivated). \ - This is a bug in the validator.", + "Invalid plugin function call while the plugin is in an incorrect state ({:?}, must be deactivated)", self ) } diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index ed0d2ab..bee9d35 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -153,9 +153,14 @@ impl<'lib> Plugin<'lib> { /// If whatever happens on the audio thread caused main-thread callback requests to be emited, /// then those will be handled concurrently. pub fn on_audio_thread T + Send>(&self, f: F) -> T { - let result = std::thread::scope(|s| { + let result = crossbeam::scope(|s| { let shared = self.shared.clone(); - let thread = s.spawn(move || f(PluginAudioThread::new(shared))); + + let thread = s + .builder() + .name("audio_thread".into()) + .spawn(move |_| f(PluginAudioThread::new(shared))) + .unwrap(); // Handle callbacks requests on the main thread while the audio thread is running while let Ok(task) = self.main.task_receiver.recv() { @@ -167,14 +172,11 @@ impl<'lib> Plugin<'lib> { } // Wait for the result, propagating panics - match thread.join() { - Ok(value) => value, - Err(panic_info) => resume_unwind(panic_info), - } + thread.join() }); self.handle_callback_unchecked(); - result + result.flatten().unwrap_or_else(|e| resume_unwind(e)) } /// Initialize the plugin. This needs to be called before doing anything else. diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index a78cbf0..dd0d3e1 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -17,7 +17,7 @@ use clap_sys::host::clap_host; use clap_sys::id::clap_id; use clap_sys::plugin::clap_plugin; use clap_sys::version::CLAP_VERSION; -use crossbeam_utils::atomic::AtomicCell; +use crossbeam::atomic::AtomicCell; use std::ffi::{CStr, c_char, c_void}; use std::pin::Pin; use std::sync::mpsc::{Receiver, Sender, channel}; diff --git a/src/plugin/preset_discovery.rs b/src/plugin/preset_discovery.rs index 4d2511a..dbbf981 100644 --- a/src/plugin/preset_discovery.rs +++ b/src/plugin/preset_discovery.rs @@ -1,9 +1,7 @@ //! An abstraction for the preset discovery factory. use anyhow::{Context, Result}; -use clap_sys::factory::preset_discovery::{ - clap_preset_discovery_factory, clap_preset_discovery_provider_descriptor, -}; +use clap_sys::factory::preset_discovery::{clap_preset_discovery_factory, clap_preset_discovery_provider_descriptor}; use clap_sys::version::{clap_version, clap_version_is_compatible}; use std::collections::HashSet; use std::ptr::NonNull; @@ -81,10 +79,7 @@ impl ProviderMetadata { impl<'lib> PresetDiscoveryFactory<'lib> { /// Create a wrapper around a preset discovery factory instance returned from a CLAP plugin's /// entry point. - pub fn new( - library: &'lib PluginLibrary, - factory: NonNull, - ) -> Self { + pub fn new(library: &'lib PluginLibrary, factory: NonNull) -> Self { PresetDiscoveryFactory { handle: PresetDiscoveryHandle(factory), _library: library, @@ -113,8 +108,8 @@ impl<'lib> PresetDiscoveryFactory<'lib> { if descriptor.is_null() { anyhow::bail!( - "The preset discovery factory returned a null pointer for the descriptor at \ - index {i} (expected {num_providers} total providers)." + "The preset discovery factory returned a null pointer for the descriptor at index {i} (expected \ + {num_providers} total providers)." ); } @@ -127,9 +122,7 @@ impl<'lib> PresetDiscoveryFactory<'lib> { .map(|provider_metadata| provider_metadata.id.as_str()) .collect(); if unique_ids.len() != metadata.len() { - anyhow::bail!( - "The preset discovery factory contains multiple entries for the same provider ID." - ); + anyhow::bail!("The preset discovery factory contains multiple entries for the same provider ID."); } Ok(metadata) diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 335df6d..820829d 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -5,9 +5,8 @@ use crate::util::{self, check_null_ptr, validator_version}; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use clap_sys::factory::preset_discovery::{ - CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, - CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, - CLAP_PRESET_DISCOVERY_LOCATION_FILE, CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN, + CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, + CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, CLAP_PRESET_DISCOVERY_LOCATION_FILE, CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN, clap_preset_discovery_filetype, clap_preset_discovery_indexer, clap_preset_discovery_location, clap_preset_discovery_location_kind, clap_preset_discovery_soundpack, }; @@ -150,8 +149,7 @@ impl Location { pub fn from_descriptor(descriptor: &clap_preset_discovery_location) -> Result { Ok(Location { flags: Flags { - is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) - != 0, + is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, is_user_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_USER_CONTENT) != 0, is_demo_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT) != 0, is_favorite: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FAVORITE) != 0, @@ -220,23 +218,15 @@ impl LocationValue { /// Constructs an new [`LocationValue`] from a location kind and a location field. Whether this /// succeeds or not depends on the location kind and whether or not the location is a null /// pointer or not. See the preset discovery factory definition for more information. - pub unsafe fn new( - location_kind: clap_preset_discovery_location_kind, - location: *const c_char, - ) -> Result { + pub unsafe fn new(location_kind: clap_preset_discovery_location_kind, location: *const c_char) -> Result { match location_kind { CLAP_PRESET_DISCOVERY_LOCATION_FILE => { if location.is_null() { - anyhow::bail!( - "The location may not be a null pointer with \ - CLAP_PRESET_DISCOVERY_LOCATION_FILE." - ) + anyhow::bail!("The location may not be a null pointer with CLAP_PRESET_DISCOVERY_LOCATION_FILE.") } let path = unsafe { CStr::from_ptr(location) }; - let path_str = path - .to_str() - .context("Invalid UTF-8 in preset discovery location")?; + let path_str = path.to_str().context("Invalid UTF-8 in preset discovery location")?; if !path_str.starts_with('/') { anyhow::bail!("'{path_str}' should be an absolute path, i.e. '/{path_str}'."); } @@ -245,10 +235,7 @@ impl LocationValue { } CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN => { if !location.is_null() { - anyhow::bail!( - "The location must be a null pointer with \ - CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN." - ) + anyhow::bail!("The location must be a null pointer with CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN.") } Ok(LocationValue::Internal) @@ -310,8 +297,7 @@ impl Soundpack { pub fn from_descriptor(descriptor: &clap_preset_discovery_soundpack) -> Result { Ok(Soundpack { flags: Flags { - is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) - != 0, + is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, is_user_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_USER_CONTENT) != 0, is_demo_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT) != 0, is_favorite: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FAVORITE) != 0, @@ -340,22 +326,17 @@ impl Drop for Indexer { // The results will have been moved out of `self.results` when initializing the provider, so // if this does contain values then the plugin did something shady let results = self.results.borrow(); - if !results.file_types.is_empty() - || !results.locations.is_empty() - || !results.soundpacks.is_empty() - { + if !results.file_types.is_empty() || !results.locations.is_empty() || !results.soundpacks.is_empty() { log::warn!( - "The plugin declared more file types, locations, or soundpacks after its \ - initialization. This is invalid behavior, but there is currently no test to \ - check for this." + "The plugin declared more file types, locations, or soundpacks after its initialization. This is \ + invalid behavior, but there is currently no test to check for this." ) } if let Some(error) = self.callback_error.borrow_mut().take() { log::error!( - "The validator's 'clap_preset_indexer' has detected an error during a callback \ - that is going to be thrown away. This is a clap-validator bug. The error message \ - is: {error}" + "The validator's 'clap_preset_indexer' has detected an error during a callback that is going to be \ + thrown away. This is a clap-validator bug. The error message is: {error}" ) } } @@ -384,17 +365,14 @@ impl Indexer { }, }); - indexer.clap_preset_discovery_indexer.indexer_data = - &*indexer as *const Self as *mut c_void; + indexer.clap_preset_discovery_indexer.indexer_data = &*indexer as *const Self as *mut c_void; indexer } /// Get a `clap_preset_discovery_indexer` vtable pointer that can be passed to the /// `clap_preset_discovery_factory` when creating a provider. - pub fn clap_preset_discovery_indexer_ptr( - self: &Pin>, - ) -> *const clap_preset_discovery_indexer { + pub fn clap_preset_discovery_indexer_ptr(self: &Pin>) -> *const clap_preset_discovery_indexer { &self.clap_preset_discovery_indexer } @@ -428,8 +406,8 @@ impl Indexer { let current_thread_id = std::thread::current().id(); if current_thread_id != self.expected_thread_id { self.set_callback_error(format!( - "'{}' may only be called from the same thread the 'clap_preset_indexer' was \ - created on (thread {:?}), but it was called from thread {:?}", + "'{}' may only be called from the same thread the 'clap_preset_indexer' was created on (thread {:?}), \ + but it was called from thread {:?}", function_name, self.expected_thread_id, current_thread_id )); } diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index 28466c0..548c9bb 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -7,9 +7,8 @@ use crate::util::{self, check_null_ptr}; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use clap_sys::factory::preset_discovery::{ - CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, - CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, - clap_preset_discovery_metadata_receiver, + CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, + CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, clap_preset_discovery_metadata_receiver, }; use clap_sys::timestamp::clap_timestamp; use clap_sys::universal_plugin_id::clap_universal_plugin_id; @@ -122,10 +121,7 @@ impl PartialPreset { /// no flags set for this preset, then the location's flags will be used. pub fn finalize(self, location_flags: &Flags) -> Result { if self.plugin_ids.is_empty() { - anyhow::bail!( - "The preset '{}' was defined without setting a plugin ID.", - self.name - ); + anyhow::bail!("The preset '{}' was defined without setting a plugin ID.", self.name); } Ok(Preset { @@ -299,9 +295,8 @@ impl<'a> MetadataReceiver<'a> { }, }); - metadata_receiver - .clap_preset_discovery_metadata_receiver - .receiver_data = &*metadata_receiver as *const Self as *mut c_void; + metadata_receiver.clap_preset_discovery_metadata_receiver.receiver_data = + &*metadata_receiver as *const Self as *mut c_void; metadata_receiver } @@ -322,8 +317,8 @@ impl<'a> MetadataReceiver<'a> { let current_thread_id = std::thread::current().id(); if current_thread_id != self.expected_thread_id { self.set_callback_error(format!( - "'{}' may only be called from the same thread the 'clap_preset_indexer' was \ - created on (thread {:?}), but it was called from thread {:?}", + "'{}' may only be called from the same thread the 'clap_preset_indexer' was created on (thread {:?}), \ + but it was called from thread {:?}", function_name, self.expected_thread_id, current_thread_id )); } @@ -356,9 +351,7 @@ impl<'a> MetadataReceiver<'a> { // should be written to the Result if there wasn't already one (Some(Err(_)), _, _) => (), (_, Err(err), _) => self.set_callback_error(format!("{err:#}")), - (result @ None, Ok(preset), None) => { - **result = Some(Ok(PresetFile::Single(preset))) - } + (result @ None, Ok(preset), None) => **result = Some(Ok(PresetFile::Single(preset))), (result @ None, Ok(preset), Some(load_key)) => { let mut presets = BTreeMap::new(); presets.insert(load_key, preset); @@ -371,11 +364,9 @@ impl<'a> MetadataReceiver<'a> { // These situations have been caught in `begin_preset()`. If a second preset has // been started when the first preset didn't have a load key this is a validator // bug. - (Some(Ok(PresetFile::Single(_))), Ok(_), _) - | (Some(Ok(PresetFile::Container(_))), Ok(_), None) => unreachable!( - "Inconsistent state in the validator's metadata receiver found, this is a \ - clap-validator bug." - ), + (Some(Ok(PresetFile::Single(_))), Ok(_), _) | (Some(Ok(PresetFile::Container(_))), Ok(_), None) => { + unreachable!("Inconsistent state in the validator's metadata receiver found.") + } } } } @@ -391,14 +382,12 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::on_error()"); - let error_message = unsafe { util::cstr_ptr_to_mandatory_string(error_message) }.context( - "'clap_preset_discovery_metadata_receiver::on_error()' called with an invalid error \ - message", - ); + let error_message = unsafe { util::cstr_ptr_to_mandatory_string(error_message) } + .context("'clap_preset_discovery_metadata_receiver::on_error()' called with an invalid error message"); match error_message { Ok(error_message) => this.set_callback_error(format!( - "'clap_preset_discovery_metadata_receiver::on_error()' called for OS error code \ - {os_error} with the following error message: {error_message}" + "'clap_preset_discovery_metadata_receiver::on_error()' called for OS error code {os_error} with the \ + following error message: {error_message}" )), // This would be quite ironic Err(err) => this.set_callback_error(format!("{err:#}")), @@ -415,13 +404,10 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::begin_preset()"); - let name = unsafe { util::cstr_ptr_to_optional_string(name) }.context( - "'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid \ - name parameter", - ); + let name = unsafe { util::cstr_ptr_to_optional_string(name) } + .context("'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid name parameter"); let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) }.context( - "'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid \ - load_key parameter", + "'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid load_key parameter", ); match (name, load_key) { (Ok(name), Ok(load_key)) => { @@ -433,16 +419,16 @@ impl<'a> MetadataReceiver<'a> { // If there was an error then just immediately exit since nothing will change that (Some(Err(_)), _) => return false, (Some(Ok(PresetFile::Single(_))), None) => Some( - "calling 'begin_preset()' a second time for a non-container preset \ - file with no load key is not allowed.", + "calling 'begin_preset()' a second time for a non-container preset file with no load key \ + is not allowed.", ), (Some(Ok(PresetFile::Single(_))), Some(_)) => Some( - "'begin_preset()' was called without a load key for the first time, \ - and with a load key the second time. This is invalid behavior.", + "'begin_preset()' was called without a load key for the first time, and with a load key \ + the second time. This is invalid behavior.", ), (Some(Ok(PresetFile::Container(_))), None) => Some( - "'begin_preset()' was called with a load key for the first time, and \ - without a load key the second time. This is invalid behavior.", + "'begin_preset()' was called with a load key for the first time, and without a load key \ + the second time. This is invalid behavior.", ), // If this is the first call and there are no errors then everything's fine (None, _) | (Some(Ok(PresetFile::Container(_))), Some(_)) => None, @@ -450,8 +436,7 @@ impl<'a> MetadataReceiver<'a> { if let Some(error_message) = error_message { this.set_callback_error(format!( - "Error in 'clap_preset_discovery_metadata_receiver::begin_preset()' \ - call: {error_message}" + "Error in 'clap_preset_discovery_metadata_receiver::begin_preset()' call: {error_message}" )); return false; } @@ -473,9 +458,7 @@ impl<'a> MetadataReceiver<'a> { }), (Some(name), _) => PresetName::Explicit(name), (None, Some(_)) => { - this.set_callback_error( - "Container presets must specify a preset name.".to_string(), - ); + this.set_callback_error("Container presets must specify a preset name.".to_string()); return false; } }; @@ -511,14 +494,10 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_plugin_id()"); - let abi = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).abi) }.context( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid \ - abi field", - ); - let id = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).id) }.context( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid id \ - field", - ); + let abi = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).abi) } + .context("'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid abi field"); + let id = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).id) } + .context("'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid id field"); match (abi, id) { (Ok(abi), Ok(id)) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); @@ -526,8 +505,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -542,8 +521,8 @@ impl<'a> MetadataReceiver<'a> { // Let's just assume noone comes up with a painfully sarcastic 'ClAp' standard this.set_callback_error(format!( "'{abi}' was provided as an ABI argument to \ - 'clap_preset_discovery_metadata_receiver::add_plugin_id()'. This is \ - probably a typo. The expected value is 'clap' in all lowercase." + 'clap_preset_discovery_metadata_receiver::add_plugin_id()'. This is probably a typo. The \ + expected value is 'clap' in all lowercase." )); } else { next_preset_data.plugin_ids.push(PluginId { @@ -565,10 +544,8 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_soundpack_id()"); - let soundpack_id = unsafe { util::cstr_ptr_to_mandatory_string(soundpack_id) }.context( - "'clap_preset_discovery_metadata_receiver::set_soundpack_id()' called with an invalid \ - parameter", - ); + let soundpack_id = unsafe { util::cstr_ptr_to_mandatory_string(soundpack_id) } + .context("'clap_preset_discovery_metadata_receiver::set_soundpack_id()' called with an invalid parameter"); match soundpack_id { Ok(soundpack_id) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); @@ -576,8 +553,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_soundpack_id()' with \ - no preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::set_soundpack_id()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -589,10 +566,7 @@ impl<'a> MetadataReceiver<'a> { } } - unsafe extern "C" fn set_flags( - receiver: *const clap_preset_discovery_metadata_receiver, - flags: u32, - ) { + unsafe extern "C" fn set_flags(receiver: *const clap_preset_discovery_metadata_receiver, flags: u32) { check_null_ptr!(receiver, (*receiver).receiver_data); let this = unsafe { &*((*receiver).receiver_data as *const Self) }; @@ -603,8 +577,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_flags()' with no preceding \ - 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::set_flags()' with no preceding 'begin_preset()' call. \ + This is not valid.", ); return; } @@ -618,19 +592,14 @@ impl<'a> MetadataReceiver<'a> { }); } - unsafe extern "C" fn add_creator( - receiver: *const clap_preset_discovery_metadata_receiver, - creator: *const c_char, - ) { + unsafe extern "C" fn add_creator(receiver: *const clap_preset_discovery_metadata_receiver, creator: *const c_char) { check_null_ptr!(receiver, (*receiver).receiver_data); let this = unsafe { &*((*receiver).receiver_data as *const Self) }; this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_creator()"); - let creator = unsafe { util::cstr_ptr_to_mandatory_string(creator) }.context( - "'clap_preset_discovery_metadata_receiver::set_creator()' called with an invalid \ - parameter", - ); + let creator = unsafe { util::cstr_ptr_to_mandatory_string(creator) } + .context("'clap_preset_discovery_metadata_receiver::set_creator()' called with an invalid parameter"); match creator { Ok(creator) => { @@ -639,8 +608,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_creator()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::set_creator()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -661,10 +630,8 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_description()"); - let description = unsafe { util::cstr_ptr_to_mandatory_string(description) }.context( - "'clap_preset_discovery_metadata_receiver::set_description()' called with an invalid \ - parameter", - ); + let description = unsafe { util::cstr_ptr_to_mandatory_string(description) } + .context("'clap_preset_discovery_metadata_receiver::set_description()' called with an invalid parameter"); match description { Ok(description) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); @@ -672,8 +639,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_description()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::set_description()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -697,18 +664,18 @@ impl<'a> MetadataReceiver<'a> { // These are parsed to `None` values if the timestamp is 0/CLAP_TIMESTAMP_UNKNOWN let creation_time = util::parse_timestamp(creation_time).context( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid \ - creation_time parameter", + "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid creation_time \ + parameter", ); let modification_time = util::parse_timestamp(modification_time).context( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid \ - modification_time parameter", + "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid modification_time \ + parameter", ); match (creation_time, modification_time) { // Calling the function like htis doesn't make any sense, so we'll point that out (Ok(None), Ok(None)) => this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with both \ - arguments set to 'CLAP_TIMESTAMP_UNKNOWN'.", + "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with both arguments set to \ + 'CLAP_TIMESTAMP_UNKNOWN'.", ), (Ok(creation_time), Ok(modification_time)) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); @@ -716,8 +683,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::set_timestamps()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -730,19 +697,14 @@ impl<'a> MetadataReceiver<'a> { } } - unsafe extern "C" fn add_feature( - receiver: *const clap_preset_discovery_metadata_receiver, - feature: *const c_char, - ) { + unsafe extern "C" fn add_feature(receiver: *const clap_preset_discovery_metadata_receiver, feature: *const c_char) { check_null_ptr!(receiver, (*receiver).receiver_data); let this = unsafe { &*((*receiver).receiver_data as *const Self) }; this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_feature()"); - let feature = unsafe { util::cstr_ptr_to_mandatory_string(feature) }.context( - "'clap_preset_discovery_metadata_receiver::add_feature()' called with an invalid \ - parameter", - ); + let feature = unsafe { util::cstr_ptr_to_mandatory_string(feature) } + .context("'clap_preset_discovery_metadata_receiver::add_feature()' called with an invalid parameter"); match feature { Ok(feature) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); @@ -750,8 +712,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } @@ -774,12 +736,10 @@ impl<'a> MetadataReceiver<'a> { this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_extra_info()"); let key = unsafe { util::cstr_ptr_to_mandatory_string(key) }.context( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid \ - key parameter", + "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid key parameter", ); let value = unsafe { util::cstr_ptr_to_mandatory_string(value) }.context( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid \ - value parameter", + "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid value parameter", ); match (key, value) { (Ok(key), Ok(value)) => { @@ -788,8 +748,8 @@ impl<'a> MetadataReceiver<'a> { Some(next_preset_data) => next_preset_data, None => { this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' with no \ - preceding 'begin_preset()' call. This is not valid.", + "'clap_preset_discovery_metadata_receiver::add_extra_info()' with no preceding \ + 'begin_preset()' call. This is not valid.", ); return; } diff --git a/src/plugin/preset_discovery/provider.rs b/src/plugin/preset_discovery/provider.rs index d573061..704c4aa 100644 --- a/src/plugin/preset_discovery/provider.rs +++ b/src/plugin/preset_discovery/provider.rs @@ -46,8 +46,7 @@ impl<'a> Provider<'a> { pub fn new(factory: &'a PresetDiscoveryFactory, provider_id: &str) -> Result { let indexer = Indexer::new(); - let provider_id_cstring = - CString::new(provider_id).expect("The provider ID contained internal null bytes"); + let provider_id_cstring = CString::new(provider_id).expect("The provider ID contained internal null bytes"); let provider = { let factory = factory.as_ptr(); let provider = unsafe { @@ -63,8 +62,8 @@ impl<'a> Provider<'a> { match NonNull::new(provider as *mut clap_preset_discovery_provider) { Some(provider) => provider, None => anyhow::bail!( - "'clap_preset_discovery_factory::create()' returned a null pointer for the \ - provider with ID '{provider_id}'.", + "'clap_preset_discovery_factory::create()' returned a null pointer for the provider with ID \ + '{provider_id}'.", ), } }; @@ -77,8 +76,7 @@ impl<'a> Provider<'a> { if !result { anyhow::bail!( - "'clap_preset_discovery_factory::init()' returned false for the provider with \ - ID '{provider_id}'." + "'clap_preset_discovery_factory::init()' returned false for the provider with ID '{provider_id}'." ); } @@ -86,8 +84,8 @@ impl<'a> Provider<'a> { // currently test for this. indexer.results().with_context(|| { format!( - "Errors produced during 'clap_preset_discovery_indexer' callbacks made by the \ - provider with ID '{provider_id}'" + "Errors produced during 'clap_preset_discovery_indexer' callbacks made by the provider with ID \ + '{provider_id}'" ) })? }; @@ -109,9 +107,7 @@ impl<'a> Provider<'a> { let provider = self.as_ptr(); let descriptor = unsafe { (*provider).desc }; if descriptor.is_null() { - anyhow::bail!( - "The 'desc' field on the 'clap_preset_provider' struct is a null pointer." - ); + anyhow::bail!("The 'desc' field on the 'clap_preset_provider' struct is a null pointer."); } ProviderMetadata::from_descriptor(unsafe { &*descriptor }) @@ -133,10 +129,7 @@ impl<'a> Provider<'a> { /// plugin triggered any kind of error. The returned map contains a [`PresetFile`] for each of /// the crawled locations that the plugin declared presets for, which can be either a single /// preset or a container of multiple presets. - pub fn crawl_location( - &self, - location: &Location, - ) -> Result> { + pub fn crawl_location(&self, location: &Location) -> Result> { let mut results = BTreeMap::new(); let location_flags = location.flags; @@ -149,8 +142,7 @@ impl<'a> Provider<'a> { // theere. This can happen during the drop. let mut result = None; { - let metadata_receiver = - MetadataReceiver::new(&mut result, &location, location_flags); + let metadata_receiver = MetadataReceiver::new(&mut result, &location, location_flags); let provider = self.as_ptr(); let success = unsafe { @@ -167,16 +159,13 @@ impl<'a> Provider<'a> { if !success { // TODO: Is the plugin allowed to return false here? If it doesn't have any // presets it should just not declare any, right? - anyhow::bail!( - "The preset provider returned false when fetching metadata for {location}.", - ); + anyhow::bail!("The preset provider returned false when fetching metadata for {location}.",); } } if let Some(preset_file) = result { - let preset_file = preset_file.with_context(|| { - format!("Error while fetching fetching metadata for {location}") - })?; + let preset_file = + preset_file.with_context(|| format!("Error while fetching fetching metadata for {location}"))?; results.insert(location, preset_file); } @@ -188,12 +177,9 @@ impl<'a> Provider<'a> { LocationValue::File(file_path) => { // Single files are queried as is, directories are crawled. If the declared location // does not exist, then that results in a hard error. - let file_path_str = file_path - .to_str() - .context("Invalid UTF-8 in location path")?; - let metadata = std::fs::metadata(file_path_str).with_context(|| { - "Could not query metadata for the declared file location '{file_path_str}'" - })?; + let file_path_str = file_path.to_str().context("Invalid UTF-8 in location path")?; + let metadata = std::fs::metadata(file_path_str) + .with_context(|| "Could not query metadata for the declared file location '{file_path_str}'")?; if metadata.is_dir() { // If the plugin declared valid file extensions, then we'll filter by those file // extensions @@ -224,13 +210,8 @@ impl<'a> Provider<'a> { // directories. If the plugin doesn't return an error but also doesn't // declare any presets then that gets handled gracefully crawl(LocationValue::File( - CString::new( - candidate - .path() - .to_str() - .context("Invalid UTF-8 in file path")?, - ) - .expect("File path contained null bytes"), + CString::new(candidate.path().to_str().context("Invalid UTF-8 in file path")?) + .expect("File path contained null bytes"), ))?; } } else { diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 9f27e50..2d8d670 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -41,7 +41,7 @@ impl<'a> ProcessScope<'a> { events_input: EventQueue::new(), events_output: EventQueue::new(), - transport: TransportState::default(), + transport: TransportState::dummy(), sample_rate, }) } @@ -106,6 +106,14 @@ impl<'a> ProcessScope<'a> { // prepare input event queue for processing self.events_input.sort_events(); + // check if the input events are within the block size + if let Some(event) = self.events_input.read().last() { + assert!( + event.header().time <= samples, + "Input event timestamp larger than block size", + ); + } + // prepare output audio buffers for processing // this is used to detect uninitialized output buffers for buffer in self.buffer.buffers_mut() { diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index 21b1dbf..bf8f292 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -77,6 +77,11 @@ impl EventQueue { events.sort_by_key(|event| event.header().time); } + pub fn is_sorted(&self) -> bool { + let events = self.events.lock().unwrap(); + events.is_sorted_by_key(|event| event.header().time) + } + pub fn read(&self) -> Vec { self.events.lock().unwrap().clone() } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index 9b601bc..8671a35 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -30,6 +30,23 @@ pub struct TransportState { } impl TransportState { + /// Create a dummy transport state with reasonable default values. + /// Used for most tests as "default" transport state. + /// + /// Use [`TransportState::default()`] if you want an "empty" transport state instead. + pub fn dummy() -> Self { + TransportState { + sample_pos: Some(0), + is_freerun: false, + is_playing: false, + is_recording: false, + tempo: Some((120.0, 0.0)), + time_signature: Some((4, 4)), + position_beats: Some(0.0), + position_seconds: Some(0.0), + } + } + /// Advance the transport state by the given number of samples at the specified sample rate. pub fn advance(&mut self, samples: i64, sample_rate: f64) { if let Some(sample_pos) = &mut self.sample_pos { @@ -63,6 +80,7 @@ impl TransportState { flags |= self.time_signature.is_some() as u32 * CLAP_TRANSPORT_HAS_TIME_SIGNATURE; clap_event_transport { + flags, header: clap_event_header { size: std::mem::size_of::() as u32, time: offset, @@ -70,7 +88,6 @@ impl TransportState { type_: CLAP_EVENT_TRANSPORT, flags: 0, }, - flags, // sending intentional invalid values when the info is not available // the plugin **must** check the flags to see what info is valid diff --git a/src/tests.rs b/src/tests.rs index 8813c2e..313b830 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -10,7 +10,7 @@ //! be converted to and from a string representation. use crate::util; -use anyhow::{Context, Result}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::any::TypeId; use std::collections::BTreeMap; @@ -100,18 +100,17 @@ pub trait TestCase<'a>: Display + FromStr + Sized + 'static { .join(plugin_id) .join(self.to_string()) .join(name); + if path.exists() { panic!( - "Tried to create a temporary file at '{}', but this file already exists. This is \ - a bug in clap-validator.", + "Tried to create a temporary file at '{}', but this file already exists", path.display() ) } fs::create_dir_all(path.parent().unwrap()) - .context("Could not create the directory for the test's temporary files")?; - let file = - fs::File::create(&path).context("Could not create a temporary file for the test")?; + .expect("Could not create the directory for the test's temporary files"); + let file = fs::File::create(&path).expect("Could not create a temporary file for the test"); Ok((path, file)) } @@ -123,9 +122,7 @@ impl TestStatus { pub fn failed_or_warning(&self) -> bool { match self { TestStatus::Success { .. } | TestStatus::Skipped { .. } => false, - TestStatus::Warning { .. } | TestStatus::Crashed { .. } | TestStatus::Failed { .. } => { - true - } + TestStatus::Warning { .. } | TestStatus::Crashed { .. } | TestStatus::Failed { .. } => true, } } @@ -176,7 +173,7 @@ impl SerializedTest { data: serde_json::to_string(&data)?, }) } else { - anyhow::bail!("Unsupported test case type for serialization."); + panic!("Unsupported test case type for serialization."); } } @@ -188,7 +185,7 @@ impl SerializedTest { let test: PluginLibraryTestCase = self.test_name.parse()?; test.run(serde_json::from_str(&self.data)?) } else { - anyhow::bail!("Unsupported test type '{}'", self.test_type); + panic!("Unsupported test type '{}'", self.test_type); } } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 9d64813..f941b99 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -27,14 +27,14 @@ pub enum PluginTestCase { LayoutAudioPortsConfig, #[strum(serialize = "layout-configurable-audio-ports")] LayoutConfigurableAudioPorts, - #[strum(serialize = "process-audio-out-of-place-basic")] - ProcessAudioOutOfPlaceBasic, - #[strum(serialize = "process-audio-in-place-basic")] - ProcessAudioInPlaceBasic, - #[strum(serialize = "process-audio-out-of-place-double")] - ProcessAudioOutOfPlaceDouble, - #[strum(serialize = "process-audio-in-place-double")] - ProcessAudioInPlaceDouble, + #[strum(serialize = "process-audio-basic-out-of-place")] + ProcessAudioBasicOutOfPlace, + #[strum(serialize = "process-audio-basic-in-place")] + ProcessAudioBasicInPlace, + #[strum(serialize = "process-audio-double-out-of-place")] + ProcessAudioDoubleOutOfPlace, + #[strum(serialize = "process-audio-double-in-place")] + ProcessAudioDoubleInPlace, #[strum(serialize = "process-audio-constant-mask")] ProcessAudioConstantMask, #[strum(serialize = "process-audio-reset-determinism")] @@ -100,34 +100,34 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::FeaturesDuplicates => { String::from("The plugin's features array should not contain any duplicates.") } - PluginTestCase::ProcessAudioOutOfPlaceBasic => String::from( + PluginTestCase::ProcessAudioBasicOutOfPlace => String::from( "Processes random audio through the plugin with its default parameter values and tests whether the \ output does not contain any non-finite or subnormal values. Uses out-of-place audio processing.", ), - PluginTestCase::ProcessAudioInPlaceBasic => String::from( + PluginTestCase::ProcessAudioBasicInPlace => String::from( "Processes random audio through the plugin with its default parameter values and tests whether the \ output does not contain any non-finite or subnormal values. Uses in-place audio processing for buses \ that support it.", ), - PluginTestCase::ProcessAudioOutOfPlaceDouble => format!( + PluginTestCase::ProcessAudioDoubleOutOfPlace => format!( "Same as '{}', but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ support it.", - PluginTestCase::ProcessAudioOutOfPlaceBasic, + PluginTestCase::ProcessAudioBasicOutOfPlace, ), - PluginTestCase::ProcessAudioInPlaceDouble => format!( + PluginTestCase::ProcessAudioDoubleInPlace => format!( "Same as '{}', but uses 64-bit floating point audio buffers instead of 32-bit ones for ports that \ support it.", - PluginTestCase::ProcessAudioInPlaceBasic, + PluginTestCase::ProcessAudioBasicInPlace, ), PluginTestCase::LayoutConfigurableAudioPorts => format!( "Same as '{}', but this time it tries random configurations exposed via the \ 'configurable-audio-ports' extension.", - PluginTestCase::ProcessAudioOutOfPlaceBasic, + PluginTestCase::ProcessAudioBasicOutOfPlace, ), PluginTestCase::LayoutAudioPortsConfig => format!( "Same as '{}', but this time it tries all available port configurations exposed via the \ 'audio-ports-config' extension.", - PluginTestCase::ProcessAudioInPlaceBasic, + PluginTestCase::ProcessAudioBasicInPlace, ), PluginTestCase::ProcessAudioConstantMask => String::from( "Processes random audio through the plugin with its default parameter values while setting the \ @@ -227,8 +227,6 @@ impl<'a> TestCase<'a> for PluginTestCase { only allowed to read a small prime number of bytes at a time when reloading and resaving the state.", PluginTestCase::StateReproducibilityBasic ), - - // TODO: fix these PluginTestCase::TransportNull => String::from( "Performs audio processing with a 'null' transport pointer, simulating a free-running transport \ state. The plugin passes the test if it doesn't produce any infinite or NaN values, and doesn't \ @@ -239,7 +237,7 @@ impl<'a> TestCase<'a> for PluginTestCase { passes the test if it doesn't produce any infinite or NaN values, and doesn't crash.", ), PluginTestCase::TransportFuzzSampleAccurate => format!( - "Same as '{}', but this time the test sends 'clap_event_transport' events in ample-accurate fashion \ + "Same as '{}', but this time the test sends 'clap_event_transport' events in sample-accurate fashion \ while processing audio, generating them at fixed intervals (1, 100, 1000 samples). The plugin passes \ the test if it doesn't produce any infinite or NaN values, and doesn't crash.", PluginTestCase::TransportFuzz @@ -260,14 +258,14 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::LayoutConfigurableAudioPorts => { layout::test_layout_configurable_audio_ports(library, plugin_id) } - PluginTestCase::ProcessAudioOutOfPlaceBasic => { + PluginTestCase::ProcessAudioBasicOutOfPlace => { processing::test_process_audio_basic(library, plugin_id, false) } - PluginTestCase::ProcessAudioInPlaceBasic => processing::test_process_audio_basic(library, plugin_id, true), - PluginTestCase::ProcessAudioOutOfPlaceDouble => { + PluginTestCase::ProcessAudioBasicInPlace => processing::test_process_audio_basic(library, plugin_id, true), + PluginTestCase::ProcessAudioDoubleOutOfPlace => { processing::test_process_audio_double(library, plugin_id, false) } - PluginTestCase::ProcessAudioInPlaceDouble => { + PluginTestCase::ProcessAudioDoubleInPlace => { processing::test_process_audio_double(library, plugin_id, true) } PluginTestCase::ProcessAudioConstantMask => { diff --git a/src/tests/plugin/descriptor.rs b/src/tests/plugin/descriptor.rs index ecae821..24b4145 100644 --- a/src/tests/plugin/descriptor.rs +++ b/src/tests/plugin/descriptor.rs @@ -22,7 +22,7 @@ pub fn test_consistency(library: &PluginLibrary, plugin_id: &str) -> Result Result Res Ok(TestStatus::Success { details: None }) } else { anyhow::bail!( - "The plugin needs to have at least one of thw following plugin category features: \ + "The plugin needs to have at least one of the following plugin category features: \ \"{instrument_feature}\", \"{audio_effect_feature}\", \"{note_effect_feature}\", or \ \"{analyzer_feature}\"." ) @@ -99,12 +99,12 @@ fn plugin_features(library: &PluginLibrary, plugin_id: &str) -> Result Result { +pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -87,48 +85,42 @@ pub fn test_layout_audio_ports_config( .map(|x| x.num_channels); anyhow::ensure!( - config_audio_ports.inputs.len() as u32 - == config_audio_ports_config.input_port_count, - "The number of input audio ports for configuration '{}' ({}) does not match the \ - number reported by 'audio-ports' ({})", + config_audio_ports.inputs.len() as u32 == config_audio_ports_config.input_port_count, + "The number of input audio ports for configuration '{}' ({}) does not match the number reported by \ + 'audio-ports' ({})", config_audio_ports_config.name, config_audio_ports_config.input_port_count, config_audio_ports.inputs.len() as u32, ); anyhow::ensure!( - config_audio_ports.outputs.len() as u32 - == config_audio_ports_config.output_port_count, - "The number of output audio ports for configuration '{}' ({}) does not match the \ - number reported by 'audio-ports' ({})", + config_audio_ports.outputs.len() as u32 == config_audio_ports_config.output_port_count, + "The number of output audio ports for configuration '{}' ({}) does not match the number reported by \ + 'audio-ports' ({})", config_audio_ports_config.name, config_audio_ports_config.output_port_count, config_audio_ports.outputs.len() as u32, ); - match ( - main_input_channels, - config_audio_ports_config.main_input_channel_count, - ) { + match (main_input_channels, config_audio_ports_config.main_input_channel_count) { (None, None) => {} (Some(a), Some(b)) => anyhow::ensure!( a == b, - "The number of channels in the main input port for the '{}' configuration \ - info ({}) does not match the number reported by 'audio-ports' ({})", + "The number of channels in the main input port for the '{}' configuration info ({}) does not \ + match the number reported by 'audio-ports' ({})", config_audio_ports_config.name, b, a, ), (None, Some(_)) => { anyhow::bail!( - "The configuration '{}' reports that a main input port exists, but \ - 'audio-ports' does not.", + "The configuration '{}' reports that a main input port exists, but 'audio-ports' does not.", config_audio_ports_config.name, ) } (Some(_), None) => anyhow::bail!( - "The configuration '{}' reports that main input port does not exist, but \ - according to 'audio-ports' it does.", + "The configuration '{}' reports that main input port does not exist, but according to \ + 'audio-ports' it does.", config_audio_ports_config.name, ), } @@ -140,22 +132,21 @@ pub fn test_layout_audio_ports_config( (None, None) => {} (Some(a), Some(b)) => anyhow::ensure!( a == b, - "The number of channels in the main output port for the '{}' configuration \ - info ({}) does not match the number reported by 'audio-ports' ({})", + "The number of channels in the main output port for the '{}' configuration info ({}) does not \ + match the number reported by 'audio-ports' ({})", config_audio_ports_config.name, b, a, ), (None, Some(_)) => { anyhow::bail!( - "The configuration '{}' reports that a main output port exists, but \ - 'audio-ports' does not.", + "The configuration '{}' reports that a main output port exists, but 'audio-ports' does not.", config_audio_ports_config.name, ) } (Some(_), None) => anyhow::bail!( - "The configuration '{}' reports that main output port does not exist, but \ - according to 'audio-ports' it does.", + "The configuration '{}' reports that main output port does not exist, but according to \ + 'audio-ports' it does.", config_audio_ports_config.name, ), } @@ -165,8 +156,8 @@ pub fn test_layout_audio_ports_config( if let Some(audio_ports_config_info) = &audio_ports_config_info { anyhow::ensure!( audio_ports_config_info.current() == config_audio_ports_config.id, - "The current configuration ID reported by 'audio-ports-config-info' ({}) does not \ - match the last selected configuration ID ({})", + "The current configuration ID reported by 'audio-ports-config-info' ({}) does not match the last \ + selected configuration ID ({})", audio_ports_config_info.current(), config_audio_ports_config.id, ); @@ -176,8 +167,7 @@ pub fn test_layout_audio_ports_config( plugin .on_audio_thread(|plugin| -> Result<()> { - let mut audio_buffers = - AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; @@ -199,22 +189,14 @@ pub fn test_layout_audio_ports_config( })?; } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `PluginTestCase::LayoutConfigurableAudioPorts`. -pub fn test_layout_configurable_audio_ports( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { - fn random_layout_requests( - prng: &mut Pcg32, - config: &AudioPortConfig, - ) -> Vec { +pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: &str) -> Result { + fn random_layout_requests(prng: &mut Pcg32, config: &AudioPortConfig) -> Vec { let mut requests = Vec::new(); for (i, _) in config.inputs.iter().enumerate() { @@ -323,8 +305,7 @@ pub fn test_layout_configurable_audio_ports( plugin .on_audio_thread(|plugin| -> Result<()> { - let mut audio_buffers = - AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; @@ -346,9 +327,7 @@ pub fn test_layout_configurable_audio_ports( })?; } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if checks_passed == 0 { return Ok(TestStatus::Warning { @@ -360,3 +339,46 @@ pub fn test_layout_configurable_audio_ports( Ok(TestStatus::Success { details: None }) } + +/// The test for `PluginTestCase::LayoutAudioPortsActivation`. +pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not implement the 'audio-ports' extension.", + )), + }); + } + }; + + let audio_ports_activation = match plugin.get_extension::() { + Some(extension) => extension, + None => { + return Ok(TestStatus::Skipped { + details: Some(String::from( + "The plugin does not implement the 'audio-ports-activation' extension.", + )), + }); + } + }; + + let note_ports = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + + Ok(TestStatus::Success { details: None }) +} diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index dda34d2..3f85a3f 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -47,7 +47,7 @@ impl<'a> ParamValue<'a> { name: ¶m_infos[&event.param_id].name, value: event.value, }, - _ => panic!("Unexpected event type. This is a clap-validator bug."), + _ => panic!("Unexpected event type"), }) .collect() } @@ -64,16 +64,12 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let param_infos = params .info() @@ -92,8 +88,8 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul let param_name = ¶m_info.name; 'value_loop: for i in 0..=100 { - let starting_value = param_info.range.start() - + (param_info.range.end() - param_info.range.start()) * (i as f64 / 100.0); + let starting_value = + param_info.range.start() + (param_info.range.end() - param_info.range.start()) * (i as f64 / 100.0); // If the plugin rounds string representations then `value` may very // will not roundtrip correctly, so we'll start at the string @@ -119,72 +115,56 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul }; num_supported_text_to_value += 1; - let reconverted_text = params - .value_to_text(param_id, reconverted_value)? - .with_context(|| { - format!( - "Failure in repeated value to text conversion for parameter {param_id} \ - ('{param_name}')" - ) - })?; + let reconverted_text = params.value_to_text(param_id, reconverted_value)?.with_context(|| { + format!("Failure in repeated value to text conversion for parameter {param_id} ('{param_name}')") + })?; // Both of these are produced by the plugin, so they should be equal if starting_text != reconverted_text { anyhow::bail!( - "Converting {starting_value:?} to a string, back to a value, and then back to \ - a string again for parameter {param_id} ('{param_name}') results in \ - '{starting_text}' -> {reconverted_value:?} -> '{reconverted_text}', which is \ - not consistent." + "Converting {starting_value:?} to a string, back to a value, and then back to a string again for \ + parameter {param_id} ('{param_name}') results in '{starting_text}' -> {reconverted_value:?} -> \ + '{reconverted_text}', which is not consistent." ); } // And one last hop back for good measure - let final_value = params - .text_to_value(param_id, &reconverted_text)? - .with_context(|| { - format!( - "Failure in repeated text to value conversion for parameter {param_id} \ - ('{param_name}')" - ) - })?; + let final_value = params.text_to_value(param_id, &reconverted_text)?.with_context(|| { + format!("Failure in repeated text to value conversion for parameter {param_id} ('{param_name}')") + })?; if final_value != reconverted_value { anyhow::bail!( - "Converting {starting_value:?} to a string, back to a value, back to a \ - string, and then back to a value again for parameter {param_id} \ - ('{param_name}') results in '{starting_text}' -> {reconverted_value:?} -> \ - '{reconverted_text}' -> {final_value:?}, which is not consistent." + "Converting {starting_value:?} to a string, back to a value, back to a string, and then back to a \ + value again for parameter {param_id} ('{param_name}') results in '{starting_text}' -> \ + {reconverted_value:?} -> '{reconverted_text}' -> {final_value:?}, which is not consistent." ); } } } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's parameters need to support both value to text and text to value \ - conversions for this test.", + "The plugin's parameters need to support both value to text and text to value conversions for this \ + test.", )), }); } if num_supported_value_to_text != expected_conversions { anyhow::bail!( - "'clap_plugin_params::value_to_text()' returned true for \ - {num_supported_value_to_text} out of {expected_conversions} calls. This function is \ - expected to be supported for either none of the parameters or for all of them. \ - Examples of failing conversions were: {failed_value_to_text_calls:#?}" + "'clap_plugin_params::value_to_text()' returned true for {num_supported_value_to_text} out of \ + {expected_conversions} calls. This function is expected to be supported for either none of the \ + parameters or for all of them. Examples of failing conversions were: {failed_value_to_text_calls:#?}" ); } if num_supported_text_to_value != expected_conversions { anyhow::bail!( - "'clap_plugin_params::text_to_value()' returned true for \ - {num_supported_text_to_value} out of {expected_conversions} calls. This function is \ - expected to be supported for either none of the parameters or for all of them. \ - Examples of failing conversions were: {failed_text_to_value_calls:#?}" + "'clap_plugin_params::text_to_value()' returned true for {num_supported_text_to_value} out of \ + {expected_conversions} calls. This function is expected to be supported for either none of the \ + parameters or for all of them. Examples of failing conversions were: {failed_text_to_value_calls:#?}" ); } @@ -192,11 +172,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } /// The test for `ProcessingTest::ParamFuzzBasic` and `ProcessingTest::ParamFuzzBounds`. -pub fn test_param_fuzz_basic( - library: &PluginLibrary, - plugin_id: &str, - snap_to_bounds: bool, -) -> Result { +pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_bounds: bool) -> Result { let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) @@ -210,16 +186,12 @@ pub fn test_param_fuzz_basic( Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -231,9 +203,7 @@ pub fn test_param_fuzz_basic( .transpose() .context("Could not fetch the plugin's note port config")? .unwrap_or_default(); - let param_infos = params - .info() - .context("Could not fetch the plugin's parameters")?; + let param_infos = params.info().context("Could not fetch the plugin's parameters")?; // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. @@ -259,9 +229,7 @@ pub fn test_param_fuzz_basic( for _ in 0..FUZZ_RUNS_PER_PERMUTATION { if !have_set_parameters { - process - .input_queue() - .add_events(current_events.clone().unwrap()); + process.input_queue().add_events(current_events.clone().unwrap()); have_set_parameters = true; } @@ -278,11 +246,9 @@ pub fn test_param_fuzz_basic( // If the run failed we'll want to write the parameter values to a file first if run_result.is_err() { let (previous_param_values_file_path, previous_param_values_file) = - PluginTestCase::ParamFuzzBasic - .temporary_file(plugin_id, PREVIOUS_PARAM_VALUES_FILE_NAME)?; + PluginTestCase::ParamFuzzBasic.temporary_file(plugin_id, PREVIOUS_PARAM_VALUES_FILE_NAME)?; let (current_param_values_file_path, current_param_values_file) = - PluginTestCase::ParamFuzzBasic - .temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; + PluginTestCase::ParamFuzzBasic.temporary_file(plugin_id, CURRENT_PARAM_VALUES_FILE_NAME)?; serde_json::to_writer_pretty( previous_param_values_file, @@ -299,8 +265,8 @@ pub fn test_param_fuzz_basic( return Err(run_result .with_context(|| { format!( - "Invalid output detected in parameter value permutation {} of {} ('{}' \ - and '{}' contain the current and previous parameter values)", + "Invalid output detected in parameter value permutation {} of {} ('{}' and '{}' contain the \ + current and previous parameter values)", permutation_no, FUZZ_NUM_PERMUTATIONS, current_param_values_file_path.display(), @@ -313,18 +279,13 @@ pub fn test_param_fuzz_basic( std::mem::swap(&mut previous_events, &mut current_events); } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `ProcessingTest::ParamFuzzSampleAccurate`. -pub fn test_param_fuzz_sample_accurate( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) -> Result { const INTERVALS: &[u32] = &[1000, 100, 1]; let mut prng = new_prng(); @@ -340,16 +301,12 @@ pub fn test_param_fuzz_sample_accurate( Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -361,9 +318,7 @@ pub fn test_param_fuzz_sample_accurate( .transpose() .context("Could not fetch the plugin's note port config")? .unwrap_or_default(); - let param_infos = params - .info() - .context("Could not fetch the plugin's parameters")?; + let param_infos = params.info().context("Could not fetch the plugin's parameters")?; // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. @@ -381,9 +336,7 @@ pub fn test_param_fuzz_sample_accurate( let mut current_sample = 0; for _ in 0..num_steps { while current_sample < BUFFER_SIZE { - let events: Vec = param_fuzzer - .randomize_params_at(&mut prng, current_sample) - .collect(); + let events: Vec = param_fuzzer.randomize_params_at(&mut prng, current_sample).collect(); process.input_queue().add_events(events.clone()); current_events = Some(events); @@ -405,9 +358,7 @@ pub fn test_param_fuzz_sample_accurate( })?; } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -438,16 +389,12 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - let param_infos = params - .info() - .context("Could not fetch the plugin's parameters")?; + let param_infos = params.info().context("Could not fetch the plugin's parameters")?; let param_fuzzer = ParamFuzzer::new(¶m_infos); let mut note_rng = NoteGenerator::new(¬e_ports).with_params(¶m_infos); @@ -466,18 +413,13 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R Ok(()) })?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } /// The test for `ProcessingTest::ParamSetWrongNamespace`. -pub fn test_param_set_wrong_namespace( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) @@ -494,16 +436,12 @@ pub fn test_param_set_wrong_namespace( Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let param_infos = params .info() @@ -517,13 +455,12 @@ pub fn test_param_set_wrong_namespace( // else. The plugin's parameter values should thus not update its parameter values. const INCORRECT_NAMESPACE_ID: u16 = 0xb33f; let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut random_param_set_events: Vec<_> = - param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + let mut random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); for event in random_param_set_events.iter_mut() { match event { Event::ParamValue(event) => event.header.space_id = INCORRECT_NAMESPACE_ID, - event => panic!("Unexpected event {event:?}, this is a clap-validator bug"), + event => panic!("Unexpected event {event:?}"), } } @@ -544,19 +481,16 @@ pub fn test_param_set_wrong_namespace( .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if actual_param_values == initial_param_values { Ok(TestStatus::Success { details: None }) } else { Ok(TestStatus::Failed { details: Some(format!( - "Sending events with type ID {CLAP_EVENT_PARAM_VALUE} (CLAP_EVENT_PARAM_VALUE) \ - and namespace ID {INCORRECT_NAMESPACE_ID:#x} to the plugin caused its parameter \ - values to change. This should not happen. The plugin may not be checking the \ - event's namespace ID." + "Sending events with type ID {CLAP_EVENT_PARAM_VALUE} (CLAP_EVENT_PARAM_VALUE) and namespace ID \ + {INCORRECT_NAMESPACE_ID:#x} to the plugin caused its parameter values to change. This should not \ + happen. The plugin may not be checking the event's namespace ID." )), }) } @@ -573,16 +507,12 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let param_infos = params .info() @@ -595,8 +525,8 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re if !param_compare_approx(default_value, param_info.default) { anyhow::bail!( - "The default value for parameter {param_id} ('{}') is {}, but the actual \ - parameter value after initialization is {}.", + "The default value for parameter {param_id} ('{}') is {}, but the actual parameter value after \ + initialization is {}.", param_info.name, param_info.default, default_value @@ -604,9 +534,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index c659240..344adfc 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -26,10 +26,7 @@ const PARAM_DIFF_FILE_NAME: &str = "param-diff.csv"; const BUFFER_SIZE: u32 = 512; /// The test for `PluginTestCase::StateInvalidEmpty`. -pub fn test_state_invalid_empty( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Result { let plugin = library .create_plugin(plugin_id) .context("Could not create the plugin instance")?; @@ -39,24 +36,20 @@ pub fn test_state_invalid_empty( Some(state) => state, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'state' extension.", - )), + details: Some(String::from("The plugin does not implement the 'state' extension.")), }); } }; let result = state.load(&[]); - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; match result { Ok(_) => Ok(TestStatus::Warning { details: Some(String::from( - "The plugin returned true when 'clap_plugin_state::load()' \ - was called when an empty state, this is likely a bug.", + "The plugin returned true when 'clap_plugin_state::load()' was called when an empty state, this is \ + likely a bug.", )), }), Err(_) => Ok(TestStatus::Success { details: None }), @@ -64,10 +57,7 @@ pub fn test_state_invalid_empty( } /// The test for `PluginTestCase::StateInvalidRandom`. -pub fn test_state_invalid_random( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -80,16 +70,12 @@ pub fn test_state_invalid_random( Some(state) => state, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'state' extension.", - )), + details: Some(String::from("The plugin does not implement the 'state' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let mut random_data = vec![0u8; 1024 * 1024]; let mut succeeded = false; @@ -99,16 +85,13 @@ pub fn test_state_invalid_random( succeeded |= state.load(&random_data).is_ok(); } - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; match succeeded { false => Ok(TestStatus::Success { details: None }), true => Ok(TestStatus::Warning { details: Some(String::from( - "The plugin loaded random bytes successfully, which is \ - unexpected, but the plugin did not crash.", + "The plugin loaded random bytes successfully, which is unexpected, but the plugin did not crash.", )), }), } @@ -137,9 +120,9 @@ pub fn test_state_reproducibility_basic( plugin.init().context("Error during initialization")?; let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports.config().context( - "Error while querying 'audio-ports' IO configuration", - )?, + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, None => AudioPortConfig::default(), }; @@ -147,9 +130,7 @@ pub fn test_state_reproducibility_basic( Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; @@ -157,16 +138,12 @@ pub fn test_state_reproducibility_basic( Some(state) => state, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'state' extension.", - )), + details: Some(String::from("The plugin does not implement the 'state' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let param_infos = params .info() @@ -175,8 +152,7 @@ pub fn test_state_reproducibility_basic( // We can't compare the values from these events direclty as the plugin // may round the values during the parameter set let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut random_param_set_events: Vec<_> = - param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + let mut random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); // This is a variation on the test that checks whether the plugin handles null // pointer cookies correctly @@ -187,20 +163,14 @@ pub fn test_state_reproducibility_basic( event.cookie = std::ptr::null_mut(); } event => { - panic!( - "Unexpected event {event:?}, this is a \ - clap-validator bug" - ) + panic!("Unexpected event {event:?}") } } } } plugin.on_audio_thread(|plugin| { - let mut buffers = AudioBuffers::new_out_of_place_f32( - &audio_ports_config, - BUFFER_SIZE, - ); + let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().randomize(&mut prng); @@ -213,16 +183,12 @@ pub fn test_state_reproducibility_basic( // deserializatoin process. let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| { - params.get(*param_id).map(|value| (*param_id, value)) - }) + .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; let expected_state = state.save()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -245,9 +211,7 @@ pub fn test_state_reproducibility_basic( None => { // I sure hope that no plugin will ever hit this return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; @@ -257,22 +221,17 @@ pub fn test_state_reproducibility_basic( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the \ - 'state' extension.", + "The plugin's second instance does not implement the 'state' extension.", )), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; state.load(&expected_state)?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -285,19 +244,13 @@ pub fn test_state_reproducibility_basic( PluginTestCase::StateReproducibilityBasic }; - if let Some(diff) = generate_param_diff( - &actual_param_values, - &expected_param_values, - ¶ms, - )? { - let (param_diff_file_path, mut param_diff_file) = - test.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; + if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? { + let (param_diff_file_path, mut param_diff_file) = test.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( - "After reloading the state, the plugin's parameter values do not \ - match the old values when queried through \ - 'clap_plugin_params::get()'. \nDiff: '{}'.", + "After reloading the state, the plugin's parameter values do not match the old values when queried \ + through 'clap_plugin_params::get()'. \nDiff: '{}'.", param_diff_file_path.display(), ); } @@ -305,25 +258,22 @@ pub fn test_state_reproducibility_basic( // Now for the moment of truth let actual_state = state.save()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { let (expected_state_file_path, mut expected_state_file) = test.temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; - let (actual_state_file_path, mut actual_state_file) = - test.temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; + let (actual_state_file_path, mut actual_state_file) = test.temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; Ok(TestStatus::Warning { details: Some(format!( - "The saved state after loading differs from the original \ - saved state. \nExpected: '{}'. \nActual: '{}'.", + "The saved state after loading differs from the original saved state. \nExpected: '{}'. \nActual: \ + '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -332,10 +282,7 @@ pub fn test_state_reproducibility_basic( } /// The test for `PluginTestCase::StateReproducibilityFlush`. -pub fn test_state_reproducibility_flush( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -353,9 +300,7 @@ pub fn test_state_reproducibility_flush( Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; @@ -363,16 +308,12 @@ pub fn test_state_reproducibility_flush( Some(state) => state, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'state' extension.", - )), + details: Some(String::from("The plugin does not implement the 'state' extension.")), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let param_infos = params .info() @@ -382,16 +323,13 @@ pub fn test_state_reproducibility_flush( // implemented flush. let initial_param_values: BTreeMap = param_infos .keys() - .map(|param_id| { - params.get(*param_id).map(|value| (*param_id, value)) - }) + .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; // The same param set events will be passed to the flush function in this pass and to the // process fuction in the second pass let param_fuzzer = ParamFuzzer::new(¶m_infos); - let random_param_set_events: Vec<_> = - param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); let input_events = EventQueue::new(); let output_events = EventQueue::new(); @@ -399,39 +337,26 @@ pub fn test_state_reproducibility_flush( input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; // We'll compare against these values in that second pass let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| { - params.get(*param_id).map(|value| (*param_id, value)) - }) + .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; let expected_state = state.save()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; // Plugins with no parameters at all should of course not trigger this error - if expected_param_values == initial_param_values - && !random_param_set_events.is_empty() - { + if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { anyhow::bail!( - "'clap_plugin_params::flush()' has been called with random \ - parameter values, but the plugin's reported parameter values \ - have not changed." + "'clap_plugin_params::flush()' has been called with random parameter values, but the plugin's \ + reported parameter values have not changed." ) } - ( - expected_state, - random_param_set_events, - expected_param_values, - ) + (expected_state, random_param_set_events, expected_param_values) }; // This works the same as the basic state reproducibility test, except that we load the values @@ -458,8 +383,7 @@ pub fn test_state_reproducibility_flush( // I sure hope that no plugin will eer hit this return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the \ - 'params' extension.", + "The plugin's second instance does not implement the 'params' extension.", )), }); } @@ -470,16 +394,13 @@ pub fn test_state_reproducibility_flush( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the \ - 'state' extension.", + "The plugin's second instance does not implement the 'state' extension.", )), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; // NOTE: We can reuse random parameter set events, except that the cookie pointers may be // different if the plugin uses those. So we need to update these cookies first. @@ -494,31 +415,23 @@ pub fn test_state_reproducibility_flush( .get(&event.param_id) .with_context(|| { format!( - "Expected the plugin to have a parameter with ID \ - {}, but the parameter is missing", + "Expected the plugin to have a parameter with ID {}, but the parameter is missing", event.param_id, ) })? .cookie; } - event => panic!( - "Unexpected event {event:?}, this is a clap-validator bug" - ), + event => panic!("Unexpected event {event:?}"), } } // In the previous pass we used flush, and here we use the process funciton plugin.on_audio_thread(|plugin| { - let mut buffers = AudioBuffers::new_out_of_place_f32( - &audio_ports_config, - BUFFER_SIZE, - ); + let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().randomize(&mut prng); - process - .input_queue() - .add_events(new_random_param_set_events); + process.input_queue().add_events(new_random_param_set_events); process.run() })?; @@ -527,50 +440,39 @@ pub fn test_state_reproducibility_flush( .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if let Some(diff) = generate_param_diff( - &actual_param_values, - &expected_param_values, - ¶ms, - )? { + if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? { let (param_diff_file_path, mut param_diff_file) = - PluginTestCase::StateReproducibilityFlush - .temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; + PluginTestCase::StateReproducibilityFlush.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( - "Setting the same parameter values through \ - 'clap_plugin_params::flush()' and through the process function \ - results in different reported values when queried through \ - 'clap_plugin_params::get_value()'. \nDiff: '{}'.", + "Setting the same parameter values through 'clap_plugin_params::flush()' and through the process function \ + results in different reported values when queried through 'clap_plugin_params::get_value()'. \nDiff: \ + '{}'.", param_diff_file_path.display(), ); } let actual_state = state.save()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { let (expected_state_file_path, mut expected_state_file) = - PluginTestCase::StateReproducibilityFlush - .temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; + PluginTestCase::StateReproducibilityFlush.temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; let (actual_state_file_path, mut actual_state_file) = - PluginTestCase::StateReproducibilityFlush - .temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; + PluginTestCase::StateReproducibilityFlush.temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; Ok(TestStatus::Warning { details: Some(format!( - "Sending the same parameter values to two different instances \ - of the plugin resulted in different state files. \nExpected: \ - '{}'. \nActual: '{}'.", + "Sending the same parameter values to two different instances of the plugin resulted in different \ + state files. \nExpected: '{}'. \nActual: '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -579,10 +481,7 @@ pub fn test_state_reproducibility_flush( } /// The test for `PluginTestCase::StateBufferedStreams`. -pub fn test_state_buffered_streams( - library: &PluginLibrary, - plugin_id: &str, -) -> Result { +pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -593,18 +492,16 @@ pub fn test_state_buffered_streams( plugin.init().context("Error during initialization")?; let audio_ports_config = match plugin.get_extension::() { - Some(audio_ports) => audio_ports.config().context( - "Error while querying 'audio-ports' IO configuration", - )?, + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, None => AudioPortConfig::default(), }; let params = match plugin.get_extension::() { Some(params) => params, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'params' extension.", - )), + details: Some(String::from("The plugin does not implement the 'params' extension.")), }); } }; @@ -612,9 +509,7 @@ pub fn test_state_buffered_streams( Some(state) => state, None => { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'state' extension.", - )), + details: Some(String::from("The plugin does not implement the 'state' extension.")), }); } }; @@ -623,14 +518,10 @@ pub fn test_state_buffered_streams( .info() .context("Failure while fetching the plugin's parameters")?; let param_fuzzer = ParamFuzzer::new(¶m_infos); - let random_param_set_events: Vec<_> = - param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); plugin.on_audio_thread(|plugin| { - let mut buffers = AudioBuffers::new_out_of_place_f32( - &audio_ports_config, - BUFFER_SIZE, - ); + let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().randomize(&mut prng); @@ -640,9 +531,7 @@ pub fn test_state_buffered_streams( let expected_param_values: BTreeMap = param_infos .keys() - .map(|param_id| { - params.get(*param_id).map(|value| (*param_id, value)) - }) + .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; // This state file is saved without buffered writes. It's expected that the plugin @@ -650,9 +539,7 @@ pub fn test_state_buffered_streams( // treating this as the ground truth. let expected_state = state.save()?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -673,8 +560,7 @@ pub fn test_state_buffered_streams( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the \ - 'params' extension.", + "The plugin's second instance does not implement the 'params' extension.", )), }); } @@ -685,45 +571,34 @@ pub fn test_state_buffered_streams( None => { return Ok(TestStatus::Skipped { details: Some(String::from( - "The plugin's second instance does not implement the \ - 'state' extension.", + "The plugin's second instance does not implement the 'state' extension.", )), }); } }; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - if let Some(diff) = generate_param_diff( - &actual_param_values, - &expected_param_values, - ¶ms, - )? { + if let Some(diff) = generate_param_diff(&actual_param_values, &expected_param_values, ¶ms)? { let (param_diff_file_path, mut param_diff_file) = - PluginTestCase::StateBufferedStreams - .temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; + PluginTestCase::StateBufferedStreams.temporary_file(plugin_id, PARAM_DIFF_FILE_NAME)?; param_diff_file.write_all(diff.as_bytes())?; anyhow::bail!( - "After reloading the state by allowing the plugin to read at most \ - {BUFFERED_LOAD_MAX_BYTES} bytes at a time, the plugin's \ - parameter values do not match the old values when queried \ - through 'clap_plugin_params::get()'. \nDiff: '{}'.", + "After reloading the state by allowing the plugin to read at most {BUFFERED_LOAD_MAX_BYTES} bytes at a \ + time, the plugin's parameter values do not match the old values when queried through \ + 'clap_plugin_params::get()'. \nDiff: '{}'.", param_diff_file_path.display() ); } @@ -732,32 +607,25 @@ pub fn test_state_buffered_streams( const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; - plugin - .handle_callback() - .context("An error occured during a callback")?; + plugin.handle_callback().context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) } else { let (expected_state_file_path, mut expected_state_file) = - PluginTestCase::StateBufferedStreams - .temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; + PluginTestCase::StateBufferedStreams.temporary_file(plugin_id, EXPECTED_STATE_FILE_NAME)?; let (actual_state_file_path, mut actual_state_file) = - PluginTestCase::StateBufferedStreams - .temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; + PluginTestCase::StateBufferedStreams.temporary_file(plugin_id, ACTUAL_STATE_FILE_NAME)?; expected_state_file.write_all(&expected_state)?; actual_state_file.write_all(&actual_state)?; Ok(TestStatus::Warning { details: Some(format!( - "Re-saving the loaded state resulted in a different state \ - file. The original state file being compared to was written \ - unbuffered, reloaded by allowing the plugin to read only \ - {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written \ - again by allowing the plugin to write only \ - {BUFFERED_SAVE_MAX_BYTES} bytes at a time.\n Expected: \ - '{}'.\n Actual: '{}'.", + "Re-saving the loaded state resulted in a different state file. The original state file being \ + compared to was written unbuffered, reloaded by allowing the plugin to read only \ + {BUFFERED_LOAD_MAX_BYTES} bytes at a time, and then written again by allowing the plugin to write \ + only {BUFFERED_SAVE_MAX_BYTES} bytes at a time.\n Expected: '{}'.\n Actual: '{}'.", expected_state_file_path.display(), actual_state_file_path.display(), )), @@ -795,12 +663,7 @@ fn generate_param_diff( Some(format!( "{}, {:?}, {:?}, {:.4}, {:?}, {:.4}", - param_id, - param_name, - string_actual, - actual_value, - string_expected, - expected_value, + param_id, param_name, string_actual, actual_value, string_expected, expected_value, )) }) .collect::>(); @@ -809,8 +672,7 @@ fn generate_param_diff( Ok(None) } else { Ok(Some(format!( - "param-id, param-name, actual-string, actual-value, \ - expected-string, expected-value\n{}", + "param-id, param-name, actual-string, actual-value, expected-string, expected-value\n{}", diff.join("\n") ))) } diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index 5ed87e5..8321515 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -14,7 +14,7 @@ use crate::{ }; use anyhow::{Context, Result}; -const BUFFER_SIZE: u32 = 512; +const BUFFER_SIZE: u32 = 128; /// The test for `PluginTestCase::TransportNull` pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result { @@ -90,8 +90,9 @@ pub fn test_transport_fuzz(library: &PluginLibrary, plugin_id: &str) -> Result= BUFFER_SIZE { + transport_start = transport_state.clone(); + transport_start.advance((BUFFER_SIZE - current_sample) as i64, process.sample_rate()); + } + // advance transport state to the event position, mutate it, and add the event transport_state.advance(interval as i64, process.sample_rate()); transport_fuzz.mutate(&mut prng, &mut transport_state); - current_sample += interval; + // this will also send the event at current_sample == 0 + // but that's fine, the plugin should handle that correctly process .input_queue() .add_events([Event::Transport(transport_state.as_clap_transport(current_sample))]); + current_sample += interval; } - // set it to the start of the next block - transport_state.advance(-(current_sample as i64), process.sample_rate()); + current_sample -= BUFFER_SIZE; process.audio_buffers().randomize(&mut prng); process diff --git a/src/tests/plugin_library.rs b/src/tests/plugin_library.rs index 5addd9a..554c038 100644 --- a/src/tests/plugin_library.rs +++ b/src/tests/plugin_library.rs @@ -37,17 +37,17 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { fn description(&self) -> String { match self { PluginLibraryTestCase::PresetDiscoveryCrawl => String::from( - "If the plugin supports the preset discovery mechanism, then this test ensures \ - that all of the plugin's declared locations can be indexed successfully.", + "If the plugin supports the preset discovery mechanism, then this test ensures that all of the \ + plugin's declared locations can be indexed successfully.", ), PluginLibraryTestCase::PresetDiscoveryDescriptorConsistency => String::from( - "Ensures that all preset provider descriptors from a preset discovery factory \ - match those stored in the providers created by the factory.", + "Ensures that all preset provider descriptors from a preset discovery factory match those stored in \ + the providers created by the factory.", ), PluginLibraryTestCase::PresetDiscoveryLoad => format!( - "The same as '{}', but also tries to load all found presets for plugins supported \ - the CLAP plugin library. A single plugin instance is reused for loading multiple \ - presets, and the process function is called after loading each preset.", + "The same as '{}', but also tries to load all found presets for plugins supported the CLAP plugin \ + library. A single plugin instance is reused for loading multiple presets, and the process function \ + is called after loading each preset.", PluginLibraryTestCase::PresetDiscoveryCrawl ), PluginLibraryTestCase::ScanTime => format!( @@ -55,36 +55,30 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { scanning::SCAN_TIME_LIMIT.as_millis() ), PluginLibraryTestCase::ScanRtldNow => String::from( - "Checks whether the plugin loads correctly when loaded using 'dlopen(..., \ - RTLD_LOCAL | RTLD_NOW)'. Only run on Unix-like platforms.", + "Checks whether the plugin loads correctly when loaded using 'dlopen(..., RTLD_LOCAL | RTLD_NOW)'. \ + Only run on Unix-like platforms.", ), PluginLibraryTestCase::QueryNonexistentFactory => String::from( - "Tries to query a factory from the plugin's entry point with a non-existent ID. \ - This should return a null pointer.", + "Tries to query a factory from the plugin's entry point with a non-existent ID. This should return a \ + null pointer.", ), PluginLibraryTestCase::CreateIdWithTrailingGarbage => String::from( - "Attempts to create a plugin instance using an existing plugin ID with some extra \ - text appended to the end. This should return a null pointer.", + "Attempts to create a plugin instance using an existing plugin ID with some extra text appended to \ + the end. This should return a null pointer.", ), } } fn run(&self, library_path: Self::TestArgs) -> Result { match self { - PluginLibraryTestCase::PresetDiscoveryCrawl => { - preset_discovery::test_crawl(library_path, false) - } + PluginLibraryTestCase::PresetDiscoveryCrawl => preset_discovery::test_crawl(library_path, false), PluginLibraryTestCase::PresetDiscoveryDescriptorConsistency => { preset_discovery::test_descriptor_consistency(library_path) } - PluginLibraryTestCase::PresetDiscoveryLoad => { - preset_discovery::test_crawl(library_path, true) - } + PluginLibraryTestCase::PresetDiscoveryLoad => preset_discovery::test_crawl(library_path, true), PluginLibraryTestCase::ScanTime => scanning::test_scan_time(library_path), PluginLibraryTestCase::ScanRtldNow => scanning::test_scan_rtld_now(library_path), - PluginLibraryTestCase::QueryNonexistentFactory => { - factories::test_query_nonexistent_factory(library_path) - } + PluginLibraryTestCase::QueryNonexistentFactory => factories::test_query_nonexistent_factory(library_path), PluginLibraryTestCase::CreateIdWithTrailingGarbage => { factories::test_create_id_with_trailing_garbage(library_path) } diff --git a/src/tests/plugin_library/factories.rs b/src/tests/plugin_library/factories.rs index 8ad0f91..3a304f7 100644 --- a/src/tests/plugin_library/factories.rs +++ b/src/tests/plugin_library/factories.rs @@ -8,8 +8,8 @@ use std::path::Path; /// The test for `PluginLibraryTestCase::QueryNonexistentFactory`. pub fn test_query_nonexistent_factory(library_path: &Path) -> Result { - let library = PluginLibrary::load(library_path) - .with_context(|| format!("Could not load '{}'", library_path.display()))?; + let library = + PluginLibrary::load(library_path).with_context(|| format!("Could not load '{}'", library_path.display()))?; // This should be actually random instead of using a fixed seed like the other tests. This // factory ID may not be used by anything. @@ -19,9 +19,9 @@ pub fn test_query_nonexistent_factory(library_path: &Path) -> Result // Since this factory doesn't exist, the plugin should always return a null pointer. if nonexistent_factory_exists { anyhow::bail!( - "Querying a factory with the non-existent factory ID '{nonexistent_factory_id} should \ - return a null pointer, but the plugin returned a non-null pointer instead. The \ - plugin may be unconditionally returning the plugin factory." + "Querying a factory with the non-existent factory ID '{nonexistent_factory_id} should return a null \ + pointer, but the plugin returned a non-null pointer instead. The plugin may be unconditionally returning \ + the plugin factory." ); } else { Ok(TestStatus::Success { details: None }) @@ -30,12 +30,10 @@ pub fn test_query_nonexistent_factory(library_path: &Path) -> Result /// The test for `PluginLibraryTestCase::CreateIdWithTrailingGarbage`. pub fn test_create_id_with_trailing_garbage(library_path: &Path) -> Result { - let library = PluginLibrary::load(library_path) - .with_context(|| format!("Could not load '{}'", library_path.display()))?; + let library = + PluginLibrary::load(library_path).with_context(|| format!("Could not load '{}'", library_path.display()))?; - let metadata = library - .metadata() - .context("Could not query the plugin's metadata")?; + let metadata = library.metadata().context("Could not query the plugin's metadata")?; if !clap_version_is_compatible(metadata.clap_version()) { return Ok(TestStatus::Skipped { details: Some(format!( @@ -66,8 +64,8 @@ pub fn test_create_id_with_trailing_garbage(library_path: &Path) -> Result { return Ok(TestStatus::Skipped { details: Some(String::from( - "All of the coolest plugins already exists. In other words, could not \ - come up a fake unused plugin ID.", + "All of the coolest plugins already exists. In other words, could not come up a fake \ + unused plugin ID.", )), }); } @@ -75,9 +73,7 @@ pub fn test_create_id_with_trailing_garbage(library_path: &Path) -> Result { return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin library does not expose any plugins", - )), + details: Some(String::from("The plugin library does not expose any plugins")), }); } }; @@ -86,8 +82,8 @@ pub fn test_create_id_with_trailing_garbage(library_path: &Path) -> Result Result { - let library = PluginLibrary::load(library_path) - .with_context(|| format!("Could not load '{}'", library_path.display()))?; + let library = + PluginLibrary::load(library_path).with_context(|| format!("Could not load '{}'", library_path.display()))?; let preset_discovery_factory = match library.preset_discovery_factory() { Ok(preset_discovery_factory) => preset_discovery_factory, Err(_) => { @@ -40,17 +40,12 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result for provider_metadata in metadata { let provider = preset_discovery_factory .create_provider(&provider_metadata) - .with_context(|| { - format!( - "Could not create the provider with ID '{}'", - provider_metadata.id - ) - })?; + .with_context(|| format!("Could not create the provider with ID '{}'", provider_metadata.id))?; for location in &provider.declared_data().locations { let presets = provider.crawl_location(location).with_context(|| { format!( - "Error occurred while crawling presets for the location '{}' with {} using \ - provider '{}' with ID '{}'", + "Error occurred while crawling presets for the location '{}' with {} using provider '{}' with ID \ + '{}'", location.name, location.value, provider_metadata.name, provider_metadata.id, ) })?; @@ -72,27 +67,25 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // Stores `PresetFile`s with their associated locations for all CLAP plugin IDs in // `found_presets` - let mut loadable_presets_by_plugin_id: BTreeMap> = - BTreeMap::new(); - let mut maybe_add_preset = - |location: &LocationValue, load_key: Option, preset: Preset| { - for plugin_id in &preset.plugin_ids { - if plugin_id.abi == PluginAbi::Clap { - if !loadable_presets_by_plugin_id.contains_key(&plugin_id.id) { - loadable_presets_by_plugin_id.insert(plugin_id.id.clone(), Vec::new()); - } - - loadable_presets_by_plugin_id - .get_mut(&plugin_id.id) - .unwrap() - .push(LoadablePreset { - location: location.clone(), - load_key: load_key.clone(), - preset: preset.clone(), - }) + let mut loadable_presets_by_plugin_id: BTreeMap> = BTreeMap::new(); + let mut maybe_add_preset = |location: &LocationValue, load_key: Option, preset: Preset| { + for plugin_id in &preset.plugin_ids { + if plugin_id.abi == PluginAbi::Clap { + if !loadable_presets_by_plugin_id.contains_key(&plugin_id.id) { + loadable_presets_by_plugin_id.insert(plugin_id.id.clone(), Vec::new()); } + + loadable_presets_by_plugin_id + .get_mut(&plugin_id.id) + .unwrap() + .push(LoadablePreset { + location: location.clone(), + load_key: load_key.clone(), + preset: preset.clone(), + }) } - }; + } + }; for (location, preset_file) in found_presets { match preset_file { @@ -152,12 +145,7 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // this. let load_result = preset_load .from_location(&location, load_key.as_deref()) - .with_context(|| { - format!( - "Could not load the preset '{}' for plugin '{}'", - preset.name, plugin_id - ) - }); + .with_context(|| format!("Could not load the preset '{}' for plugin '{}'", preset.name, plugin_id)); // In case the plugin uses `clap_host_preset_load::on_error()` to report an error, // we will check that first before making sure the preset loaded correctly. This @@ -179,20 +167,17 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result process.run() }) .with_context(|| { - format!( - "Error while processing an audio buffer after loading a preset for \ - '{plugin_id}'" - ) + format!("Error while processing an audio buffer after loading a preset for '{plugin_id}'") })?; - plugin.handle_callback().with_context(|| { - format!("An error occured during a host callback made by '{plugin_id}'") - })?; + plugin + .handle_callback() + .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } - plugin.handle_callback().with_context(|| { - format!("An error occured during a host callback made by '{plugin_id}'") - })?; + plugin + .handle_callback() + .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } } @@ -202,8 +187,8 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result /// The test for `PluginLibraryTestCase::PresetDiscoveryDescriptorConsistency`. Verifies that the /// descriptors stored in a plugin's preset providers match those returned by the factory. pub fn test_descriptor_consistency(library_path: &Path) -> Result { - let library = PluginLibrary::load(library_path) - .with_context(|| format!("Could not load '{}'", library_path.display()))?; + let library = + PluginLibrary::load(library_path).with_context(|| format!("Could not load '{}'", library_path.display()))?; let preset_discovery_factory = match library.preset_discovery_factory() { Ok(preset_discovery_factory) => preset_discovery_factory, Err(_) => { @@ -222,25 +207,18 @@ pub fn test_descriptor_consistency(library_path: &Path) -> Result { for factory_metadata in metadata { let provider = preset_discovery_factory .create_provider(&factory_metadata) - .with_context(|| { - format!( - "Could not create the provider with ID '{}'", - factory_metadata.id - ) - })?; + .with_context(|| format!("Could not create the provider with ID '{}'", factory_metadata.id))?; let provider_metadata = provider.descriptor().with_context(|| { format!( - "Could not grab the descriptor from the 'clap_preset_discovery_provider''s 'desc' \ - field for '{}'", + "Could not grab the descriptor from the 'clap_preset_discovery_provider''s 'desc' field for '{}'", &factory_metadata.id ) })?; if provider_metadata != factory_metadata { anyhow::bail!( - "The 'clap_preset_discovery_provider_descriptor' stored on '{}'s \ - 'clap_preset_discovery_provider' object contains different values than the one \ - returned by the factory.", + "The 'clap_preset_discovery_provider_descriptor' stored on '{}'s 'clap_preset_discovery_provider' \ + object contains different values than the one returned by the factory.", factory_metadata.id ); } diff --git a/src/tests/plugin_library/scanning.rs b/src/tests/plugin_library/scanning.rs index 6eff72c..868857d 100644 --- a/src/tests/plugin_library/scanning.rs +++ b/src/tests/plugin_library/scanning.rs @@ -17,8 +17,8 @@ pub fn test_scan_time(library_path: &Path) -> Result { { // The library will be unloaded when this object is dropped, so that is part of the // measurement - let library = PluginLibrary::load(library_path) - .with_context(|| format!("Could not load '{}'", library_path.display())); + let library = + PluginLibrary::load(library_path).with_context(|| format!("Could not load '{}'", library_path.display())); // This goes through all plugins and builds a data structure containing information for all // of those plugins, mimicing most of a DAW's plugin scanning process @@ -48,11 +48,7 @@ pub fn test_scan_time(library_path: &Path) -> Result { details: Some(format!( "The plugin can be scanned in {} {}.", millis, - if millis == 1 { - "millisecond" - } else { - "milliseconds" - } + if millis == 1 { "millisecond" } else { "milliseconds" } )), }) } else { @@ -81,12 +77,7 @@ pub fn test_scan_rtld_now(library_path: &Path) -> Result { .map(libloading::Library::from) .context("Could not load the plugin library using 'RTLD_LOCAL | RTLD_NOW'") }) - .with_context(|| { - format!( - "Could not load '{}' using 'RTLD_NOW", - library_path.display() - ) - })?; + .with_context(|| format!("Could not load '{}' using 'RTLD_NOW", library_path.display()))?; Ok(TestStatus::Success { details: None }) } @@ -94,8 +85,6 @@ pub fn test_scan_rtld_now(library_path: &Path) -> Result { #[cfg(not(unix))] pub fn test_scan_rtld_now(_: &Path) -> Result { Ok(TestStatus::Skipped { - details: Some(String::from( - "This test is only relevant to Unix-like platforms", - )), + details: Some(String::from("This test is only relevant to Unix-like platforms")), }) } diff --git a/src/tests/rng.rs b/src/tests/rng.rs index fb289ac..aa09b27 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -58,7 +58,9 @@ pub struct ParamFuzzer<'a> { } /// A helper to generate random transport events in a couple different ways to stress test a plugin's transport handling. -pub struct TransportFuzzer {} +pub struct TransportFuzzer { + probability_change: f64, +} /// The description of an active note in the [`NoteGenerator`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -590,7 +592,7 @@ impl<'a> NoteGenerator<'a> { } } - panic!("Unable to generate a random note event after 1024 tries, this is a bug in the validator"); + panic!("Unable to generate a random note event after 1024 tries"); } #[allow(unused)] @@ -796,23 +798,25 @@ impl<'a> ParamFuzzer<'a> { impl TransportFuzzer { /// Create a new transport fuzzer. pub fn new() -> Self { - TransportFuzzer {} + TransportFuzzer { + probability_change: 0.2, + } } /// Mutates an existing transport state. pub fn mutate(&mut self, prng: &mut Pcg32, transport: &mut TransportState) { - // toggle playback state with 10% probability - if prng.random_bool(0.1) { + // toggle playback state with 20% probability + if prng.random_bool(self.probability_change) { transport.is_playing = !transport.is_playing; } - // toggle recording state with 10% probability - if prng.random_bool(0.1) { + // toggle recording state with 20% probability + if prng.random_bool(self.probability_change) { transport.is_recording = !transport.is_recording; } - // change time signature with 10% probability - if prng.random_bool(0.1) { + // change time signature with 20% probability + if prng.random_bool(self.probability_change) { if prng.random_bool(0.5) { transport.time_signature = None; } else { @@ -820,8 +824,8 @@ impl TransportFuzzer { } } - // change tempo (instanteous) with 10% probability - if prng.random_bool(0.1) { + // change tempo (instanteous) with 20% probability + if prng.random_bool(self.probability_change) { if prng.random_bool(0.5) { transport.tempo = None; } else { @@ -829,21 +833,21 @@ impl TransportFuzzer { } } - // change tempo (ramp) with 20% probability - if prng.random_bool(0.2) { - if let Some((tempo, ramp)) = &mut transport.tempo { - // safeguard to prevent extremely low tempos - if *tempo < 40.0 { - *tempo = 40.0; - *ramp = prng.random_range(0.0..=0.01); - } - - *ramp = prng.random_range(-0.01..=0.01); + // change tempo (ramp) with 40% probability + if let Some((tempo, ramp)) = &mut transport.tempo + && prng.random_bool(self.probability_change) + { + // safeguard to prevent extremely low tempos + if *tempo < 40.0 { + *tempo = 40.0; + *ramp = prng.random_range(0.0..=0.01); } + + *ramp = prng.random_range(-0.01..=0.01); } - // seek to a new position with 5% probability - if prng.random_bool(0.05) { + // seek to a new position with 10% probability + if prng.random_bool(self.probability_change) { if prng.random_bool(0.5) { transport.position_seconds = None; } else { @@ -863,5 +867,9 @@ impl TransportFuzzer { transport.sample_pos = Some(transport.sample_pos.unwrap_or(0) + prng.random_range(0..=100_000) as u64); } } + + if transport.tempo.is_none() { + transport.position_beats = None; + } } } diff --git a/src/util.rs b/src/util.rs index 85954b7..38fa89e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -150,10 +150,10 @@ pub fn validator_temp_dir() -> PathBuf { /// [`std::env::temp_dir`], but taking `XDG_RUNTIME_DIR` on Linux into account. fn temp_dir() -> PathBuf { #[cfg(all(unix, not(target_os = "macos")))] - if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR").map(PathBuf::from) { - if dir.is_dir() { - return dir; - } + if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR").map(PathBuf::from) + && dir.is_dir() + { + return dir; } std::env::temp_dir() diff --git a/src/validator.rs b/src/validator.rs index b1e3231..3c89345 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -162,10 +162,10 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result) -> TestStatus "A panic occurred".to_string() }; - TestStatus::Crashed { details: message } + TestStatus::Crashed { + details: format!("{message}. This is a bug in clap-validator"), + } } } } diff --git a/tests/clack-synth/Cargo.toml b/tests/clack-synth/Cargo.toml index d3894ec..9ed6649 100644 --- a/tests/clack-synth/Cargo.toml +++ b/tests/clack-synth/Cargo.toml @@ -9,5 +9,5 @@ publish = false crate-type = ["cdylib"] [dependencies] -clack-plugin = { git = "https://github.com/Quant1um/clack", branch = "configurable-audio-ports" } -clack-extensions = { git = "https://github.com/Quant1um/clack", branch = "configurable-audio-ports", features = ["audio-ports", "audio-ports-config", "configurable-audio-ports", "clack-plugin", "note-ports", "params", "state"] } +clack-plugin = { git = "https://github.com/Quant1um/clack", rev = "bd6f37959270153cf2118923275cd54ad20db958" } +clack-extensions = { git = "https://github.com/Quant1um/clack", rev = "bd6f37959270153cf2118923275cd54ad20db958", features = ["audio-ports", "audio-ports-config", "configurable-audio-ports", "clack-plugin", "note-ports", "params", "state"] } diff --git a/tests/clack-synth/src/lib.rs b/tests/clack-synth/src/lib.rs index 5783f76..f621575 100644 --- a/tests/clack-synth/src/lib.rs +++ b/tests/clack-synth/src/lib.rs @@ -1,8 +1,8 @@ use crate::params::{PolySynthParamModulations, PolySynthParams}; use crate::poly_oscillator::PolyOscillator; use clack_extensions::audio_ports_config::{ - AudioPortConfigWriter, AudioPortsConfiguration, MainPortInfo, PluginAudioPortsConfig, - PluginAudioPortsConfigImpl, + AudioPortConfigWriter, AudioPortsConfiguration, MainPortInfo, PluginAudioPortsConfig, PluginAudioPortsConfigImpl, + PluginAudioPortsConfigInfoImpl, }; use clack_extensions::configurable_audio_ports::{ AudioPortsRequestList, PluginConfigurableAudioPorts, PluginConfigurableAudioPortsImpl, @@ -25,10 +25,7 @@ impl Plugin for PolySynthPlugin { type Shared<'a> = PolySynthPluginShared; type MainThread<'a> = PolySynthPluginMainThread<'a>; - fn declare_extensions( - builder: &mut PluginExtensions, - _shared: Option<&PolySynthPluginShared>, - ) { + fn declare_extensions(builder: &mut PluginExtensions, _shared: Option<&PolySynthPluginShared>) { builder .register::() .register::() @@ -43,8 +40,11 @@ impl DefaultPluginFactory for PolySynthPlugin { fn get_descriptor() -> PluginDescriptor { use clack_plugin::plugin::features::*; - PluginDescriptor::new("org.rust-audio.clack.polysynth", "Clack PolySynth Example") - .with_features([SYNTHESIZER, MONO, INSTRUMENT]) + PluginDescriptor::new("org.rust-audio.clack.polysynth", "Clack PolySynth Example").with_features([ + SYNTHESIZER, + MONO, + INSTRUMENT, + ]) } fn new_shared(_host: HostSharedHandle) -> Result { @@ -59,7 +59,7 @@ impl DefaultPluginFactory for PolySynthPlugin { ) -> Result, PluginError> { Ok(PolySynthPluginMainThread { shared, - channels: 2, + config: ClapId::new(2), }) } } @@ -81,19 +81,14 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre audio_config: PluginAudioConfiguration, ) -> Result { Ok(Self { - channels: main_thread.channels, + channels: main_thread.config.get(), poly_osc: PolyOscillator::new(16, audio_config.sample_rate as f32), modulation_values: PolySynthParamModulations::new(), shared, }) } - fn process( - &mut self, - _process: Process, - mut audio: Audio, - events: Events, - ) -> Result { + fn process(&mut self, _process: Process, mut audio: Audio, events: Events) -> Result { let mut output_port = audio .output_port(0) .ok_or(PluginError::Message("No output port found"))?; @@ -180,16 +175,7 @@ impl PluginAudioPortsImpl for PolySynthPluginMainThread<'_> { } fn get(&mut self, index: u32, is_input: bool, writer: &mut AudioPortInfoWriter) { - if !is_input && index == 0 { - writer.set(&AudioPortInfo { - id: ClapId::new(1), - name: b"main", - channel_count: self.channels, - flags: AudioPortFlags::IS_MAIN, - port_type: AudioPortType::from_channel_count(self.channels), - in_place_pair: None, - }); - } + PluginAudioPortsConfigInfoImpl::get(self, self.config, index, is_input, writer); } } @@ -216,24 +202,23 @@ impl PluginAudioPortsConfigImpl for PolySynthPluginMainThread<'_> { } fn get(&mut self, index: u32, writer: &mut AudioPortConfigWriter) { + let channels = index + 1; writer.write(&AudioPortsConfiguration { - id: ClapId::new(index), - name: CString::new(format!("Config {}", index)) - .unwrap() - .as_bytes(), + id: ClapId::new(channels), + name: CString::new(format!("Config #{}", channels)).unwrap().as_bytes(), input_port_count: 0, output_port_count: 1, main_input: None, main_output: Some(MainPortInfo { - channel_count: index + 1, - port_type: AudioPortType::from_channel_count(self.channels), + channel_count: channels, + port_type: AudioPortType::from_channel_count(channels), }), }); } fn select(&mut self, config_id: ClapId) -> Result<(), PluginError> { - if config_id.get() < 8 { - self.channels = config_id.get() + 1; + if config_id.get() <= 8 { + self.config = config_id; Ok(()) } else { Err(PluginError::Message("Invalid configuration ID")) @@ -241,15 +226,41 @@ impl PluginAudioPortsConfigImpl for PolySynthPluginMainThread<'_> { } } +impl PluginAudioPortsConfigInfoImpl for PolySynthPluginMainThread<'_> { + fn current_config(&mut self) -> Option { + Some(self.config) + } + + fn get(&mut self, config_id: ClapId, index: u32, is_input: bool, writer: &mut AudioPortInfoWriter) { + let channels = config_id.get(); + + if !is_input && index == 0 { + writer.set(&AudioPortInfo { + id: ClapId::new(1), + name: b"main", + channel_count: channels, + flags: AudioPortFlags::IS_MAIN, + port_type: AudioPortType::from_channel_count(channels), + in_place_pair: None, + }); + } + } +} + impl PluginConfigurableAudioPortsImpl for PolySynthPluginMainThread<'_> { fn can_apply_configuration(&mut self, requests: AudioPortsRequestList) -> bool { - matches!(requests.get(0), Some(request) if !request.is_input && request.channel_count > 0) + matches!(requests.get(0), Some(request) if !request.is_input && request.port_index == 0 && request.channel_count > 0 && request.channel_count <= 8) } fn apply_configuration(&mut self, requests: AudioPortsRequestList) -> bool { match requests.get(0) { - Some(request) if !request.is_input && request.channel_count > 0 => { - self.channels = request.channel_count; + Some(request) + if !request.is_input + && request.port_index == 0 + && request.channel_count > 0 + && request.channel_count <= 8 => + { + self.config = ClapId::new(request.channel_count); true } _ => false, @@ -265,7 +276,7 @@ impl PluginShared<'_> for PolySynthPluginShared {} pub struct PolySynthPluginMainThread<'a> { shared: &'a PolySynthPluginShared, - channels: u32, + config: ClapId, } impl<'a> PluginMainThread<'a, PolySynthPluginShared> for PolySynthPluginMainThread<'a> {} diff --git a/tests/clack-synth/src/params.rs b/tests/clack-synth/src/params.rs index 44b3b23..b0914d7 100644 --- a/tests/clack-synth/src/params.rs +++ b/tests/clack-synth/src/params.rs @@ -123,12 +123,7 @@ impl PluginMainThreadParams for PolySynthPluginMainThread<'_> { } } - fn value_to_text( - &mut self, - param_id: ClapId, - value: f64, - writer: &mut ParamDisplayWriter, - ) -> std::fmt::Result { + fn value_to_text(&mut self, param_id: ClapId, value: f64, writer: &mut ParamDisplayWriter) -> std::fmt::Result { match param_id { PARAM_VOLUME_ID => write!(writer, "{0:.2} %", value * 100.0), _ => Err(std::fmt::Error), @@ -147,11 +142,7 @@ impl PluginMainThreadParams for PolySynthPluginMainThread<'_> { } } - fn flush( - &mut self, - input_parameter_changes: &InputEvents, - _output_parameter_changes: &mut OutputEvents, - ) { + fn flush(&mut self, input_parameter_changes: &InputEvents, _output_parameter_changes: &mut OutputEvents) { for event in input_parameter_changes { if let Some(CoreEventSpace::ParamValue(event)) = event.as_core_event() { self.shared.params.handle_event(event) @@ -161,11 +152,7 @@ impl PluginMainThreadParams for PolySynthPluginMainThread<'_> { } impl PluginAudioProcessorParams for PolySynthAudioProcessor<'_> { - fn flush( - &mut self, - input_parameter_changes: &InputEvents, - _output_parameter_changes: &mut OutputEvents, - ) { + fn flush(&mut self, input_parameter_changes: &InputEvents, _output_parameter_changes: &mut OutputEvents) { for event in input_parameter_changes { self.handle_event(event) } diff --git a/tests/clack-synth/src/poly_oscillator.rs b/tests/clack-synth/src/poly_oscillator.rs index dae432f..b5d3e3e 100644 --- a/tests/clack-synth/src/poly_oscillator.rs +++ b/tests/clack-synth/src/poly_oscillator.rs @@ -3,9 +3,7 @@ use crate::oscillator::SquareOscillator; use crate::params::PARAM_VOLUME_ID; use clack_plugin::events::Match; -use clack_plugin::events::event_types::{ - NoteOffEvent, NoteOnEvent, ParamModEvent, ParamValueEvent, -}; +use clack_plugin::events::event_types::{NoteOffEvent, NoteOnEvent, ParamModEvent, ParamValueEvent}; /// A voice in the polyphonic oscillator. /// @@ -117,8 +115,7 @@ impl PolyOscillator { .position(|v| v.matches(channel, note_key, note_id)) { // Swap the targeted voice with the last one. - self.voice_buffer - .swap(voice_index, self.active_voice_count - 1); + self.voice_buffer.swap(voice_index, self.active_voice_count - 1); // Remove the last voice from the active pool. self.active_voice_count -= 1; @@ -192,19 +189,12 @@ impl PolyOscillator { /// Each voice will play at the given volume. /// /// This method assumes the buffer is initialized with `0`s. - pub fn generate_next_samples( - &mut self, - output_buffer: &mut [f32], - global_volume: f32, - global_volume_mod: f32, - ) { + pub fn generate_next_samples(&mut self, output_buffer: &mut [f32], global_volume: f32, global_volume_mod: f32) { for voice in self.active_voice_buffer_mut() { let volume = voice.volume.unwrap_or(global_volume); let volume_mod = voice.volume_mod.unwrap_or(global_volume_mod); - voice - .oscillator - .synth_samples(output_buffer, volume + volume_mod); + voice.oscillator.synth_samples(output_buffer, volume + volume_mod); } } From 6a81b3aa5c20f991fc37497281c7f040b1f9206b Mon Sep 17 00:00:00 2001 From: Quant1um Date: Fri, 30 Jan 2026 17:49:22 +0400 Subject: [PATCH 047/114] - more host extension checks - custom panic hook - moved from chrono to time - tail check on process call - fix/simplify `send_main_thread` - refactor `InstanceShared` and `Plugin` --- Cargo.lock | 255 +-------------- Cargo.toml | 3 +- src/main.rs | 5 +- src/plugin/ext.rs | 1 + src/plugin/ext/latency.rs | 5 +- src/plugin/ext/tail.rs | 36 +++ src/plugin/instance/audio_thread.rs | 49 +-- src/plugin/instance/main_thread.rs | 64 ++-- src/plugin/instance/shared.rs | 293 ++++++++++++------ src/plugin/library.rs | 3 +- src/plugin/preset_discovery/indexer.rs | 4 +- .../preset_discovery/metadata_receiver.rs | 12 +- src/plugin/process.rs | 33 +- src/plugin/process/buffer.rs | 4 + src/tests/plugin.rs | 10 + src/tests/plugin/params.rs | 8 +- src/util.rs | 57 +++- src/validator.rs | 1 - tests/clack-synth/Cargo.toml | 4 +- tests/clack-synth/src/lib.rs | 36 ++- 20 files changed, 417 insertions(+), 466 deletions(-) create mode 100644 src/plugin/ext/tail.rs diff --git a/Cargo.lock b/Cargo.lock index c700d64..966c79c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.0.2" @@ -26,21 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.3.2" @@ -96,27 +66,6 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -129,47 +78,16 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "cc" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" -dependencies = [ - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - [[package]] name = "clack-common" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" +source = "git+https://github.com/Quant1um/clack?rev=8f39ba01643103bac2acaabad1b22ea9ee452400#8f39ba01643103bac2acaabad1b22ea9ee452400" dependencies = [ "bitflags 2.10.0", "clap-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -178,7 +96,7 @@ dependencies = [ [[package]] name = "clack-extensions" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" +source = "git+https://github.com/Quant1um/clack?rev=8f39ba01643103bac2acaabad1b22ea9ee452400#8f39ba01643103bac2acaabad1b22ea9ee452400" dependencies = [ "bitflags 2.10.0", "clack-common", @@ -189,7 +107,7 @@ dependencies = [ [[package]] name = "clack-plugin" version = "0.1.0" -source = "git+https://github.com/Quant1um/clack?rev=bd6f37959270153cf2118923275cd54ad20db958#bd6f37959270153cf2118923275cd54ad20db958" +source = "git+https://github.com/Quant1um/clack?rev=8f39ba01643103bac2acaabad1b22ea9ee452400#8f39ba01643103bac2acaabad1b22ea9ee452400" dependencies = [ "clack-common", "clap-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -230,7 +148,6 @@ name = "clap-validator" version = "0.3.2" dependencies = [ "anyhow", - "chrono", "clap", "clap-sys 0.5.0 (git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156)", "colored", @@ -239,7 +156,6 @@ dependencies = [ "either", "libloading", "log", - "log-panics", "midi-consts", "rand", "rand_pcg", @@ -252,6 +168,7 @@ dependencies = [ "strum_macros", "tempfile", "textwrap", + "time", "walkdir", ] @@ -380,6 +297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -416,12 +334,6 @@ dependencies = [ "wasip2", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "heck" version = "0.4.1" @@ -440,29 +352,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -491,15 +380,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "libc" version = "0.2.178" @@ -540,16 +420,6 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" -[[package]] -name = "log-panics" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" -dependencies = [ - "backtrace", - "log", -] - [[package]] name = "memchr" version = "2.5.0" @@ -562,30 +432,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-traits" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" -dependencies = [ - "autocfg", -] - [[package]] name = "num_threads" version = "0.1.6" @@ -595,15 +447,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -742,12 +585,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustix" version = "0.37.23" @@ -854,7 +691,7 @@ checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", "termcolor", - "time 0.3.36", + "time", ] [[package]] @@ -952,17 +789,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi", - "winapi", -] - [[package]] name = "time" version = "0.3.36" @@ -1030,12 +856,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1045,60 +865,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - [[package]] name = "winapi" version = "0.3.9" @@ -1130,15 +896,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.1", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 7dd2a20..1574639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ repository = "https://github.com/free-audio/clap-validator" [dependencies] anyhow = "1.0.58" either = "1.9.0" -chrono = { version = "0.4.23", features = ["serde"] } # All the claps! clap = { version = "4.1.8", features = ["derive", "wrap_help"] } # For CLAP 1.2.2 support @@ -27,7 +26,6 @@ colored = "3.0.0" crossbeam = "0.8.4" libloading = "0.9.0" log = "0.4" -log-panics = { version = "2.0", features = ["with-backtrace"] } midi-consts = "0.1.0" rand = "0.9.2" rand_pcg = "0.9.0" @@ -40,6 +38,7 @@ strum = "0.27.2" strum_macros = "0.27.2" tempfile = "3.3" textwrap = { version = "0.16.2", features = ["terminal_size"] } +time = { version = "0.3", features = ["serde"]} walkdir = "2.3" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src/main.rs b/src/main.rs index 11b4df7..186d76d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,9 +73,8 @@ fn main() -> ExitCode { ) .expect("Could not initialize logger"); - log_panics::Config::new() - .backtrace_mode(log_panics::BacktraceMode::Resolved) - .install_panic_hook(); + // Install the panic hook to log panics instead of printing them to stderr. + util::install_panic_hook(); // Mark the main thread as such for plugin instance creation checks. unsafe { diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index 01eaa56..6d5f7ec 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -16,6 +16,7 @@ pub mod params; pub mod preset_load; pub mod state; pub mod surround; +pub mod tail; /// An abstraction for a CLAP plugin extension. `P` here is the plugin type. In practice, this is /// either `Plugin` or `PluginAudioThread`. Abstractions for main thread functions will implement diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 7643f02..55606ae 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -1,5 +1,5 @@ use crate::plugin::ext::Extension; -use crate::plugin::instance::Plugin; +use crate::plugin::instance::{Plugin, PluginStatus}; use crate::util::clap_call; use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_plugin_latency}; use std::ffi::CStr; @@ -27,6 +27,9 @@ impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { impl<'a> Latency<'a> { #[allow(unused)] pub fn get(&self) -> u32 { + self.plugin.status().assert_is_not(PluginStatus::Uninitialized); + self.plugin.status().assert_is_not(PluginStatus::Deactivated); + let latency = self.latency.as_ptr(); let plugin = self.plugin.as_ptr(); unsafe { diff --git a/src/plugin/ext/tail.rs b/src/plugin/ext/tail.rs new file mode 100644 index 0000000..33e0450 --- /dev/null +++ b/src/plugin/ext/tail.rs @@ -0,0 +1,36 @@ +use crate::plugin::ext::Extension; +use crate::plugin::instance::PluginAudioThread; +use crate::util::clap_call; +use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_plugin_tail}; +use std::ffi::CStr; +use std::ptr::NonNull; + +#[allow(unused)] +pub struct Tail<'a> { + plugin: &'a PluginAudioThread<'a>, + tail: NonNull, +} + +impl<'a> Extension<&'a PluginAudioThread<'a>> for Tail<'a> { + const IDS: &'static [&'static CStr] = &[CLAP_EXT_TAIL]; + + type Struct = clap_plugin_tail; + + unsafe fn new(plugin: &'a PluginAudioThread<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + tail: extension_struct, + } + } +} + +impl<'a> Tail<'a> { + #[allow(unused)] + pub fn get(&self) -> u32 { + let tail = self.tail.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { tail=>get(plugin) } + } + } +} diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index ccffc08..00e8650 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -2,7 +2,7 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; -use crate::plugin::instance::{InstanceShared, MainThreadTask}; +use crate::plugin::instance::{MainThreadTask, PluginShared}; use crate::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; @@ -13,14 +13,14 @@ use clap_sys::process::{ use std::marker::PhantomData; use std::pin::Pin; use std::ptr::NonNull; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::Arc; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. pub struct PluginAudioThread<'a> { /// Information about this plugin instance stored on the host. This keeps track of things like /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - shared: Pin>, + shared: Pin>, _plugin_marker: PhantomData<&'a Plugin<'a>>, @@ -31,7 +31,7 @@ pub struct PluginAudioThread<'a> { /// The equivalent of `clap_process_status`, minus the `CLAP_PROCESS_ERROR` value as this is already /// treated as an error by `PluginAudioThread::process()`. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ProcessStatus { Continue, ContinueIfNotQuiet, @@ -47,7 +47,7 @@ impl Drop for PluginAudioThread<'_> { } impl<'a> PluginAudioThread<'a> { - pub(super) fn new(shared: Pin>) -> PluginAudioThread<'a> { + pub(super) fn new(shared: Pin>) -> PluginAudioThread<'a> { shared.audio_thread_id.store(Some(std::thread::current().id())); PluginAudioThread { shared, @@ -67,14 +67,13 @@ impl<'a> PluginAudioThread<'a> { } /// Get a reference to the plugin's shared state. - pub fn shared(&self) -> &Pin> { + pub fn shared(&self) -> &Pin> { &self.shared } /// Get the _audio thread_ extension abstraction for the extension `T`, if the plugin supports /// this extension. Returns `None` if it does not. The plugin needs to be initialized using /// [`init()`][Self::init()] before this may be called. - #[allow(unused)] pub fn get_extension>(&'a self) -> Option { self.status().assert_is_not(PluginStatus::Uninitialized); @@ -94,38 +93,20 @@ impl<'a> PluginAudioThread<'a> { /// Dispatch a task to be executed on the main thread. This is a blocking call that will wait /// for the task to complete and return its result. - pub fn send_main_thread T + Send, T: Send>(&self, callback: F) -> T { - struct Scope<'a, F, T> { - callback: F, - mutex: &'a Mutex>, - condvar: &'a Condvar, - } - - let mutex = Mutex::new(None); - let condvar = Condvar::new(); - let scope = Scope { - callback, - mutex: &mutex, - condvar: &condvar, - }; + /// + /// TODO: this could be optimized and the 'static requirement dropped. + pub fn dispatch_main T + Send + 'static, T: Send + 'static>(&self, callback: F) -> T { + let (send, recv) = std::sync::mpsc::sync_channel(0); self.shared .task_sender - .send(MainThreadTask::Dispatch { - data: &scope as *const _ as _, - func: |plugin, data| { - let scope = unsafe { data.cast::>().read() }; - scope.mutex.lock().unwrap().replace((scope.callback)(plugin)); - scope.condvar.notify_one(); - }, - }) + .send(MainThreadTask::Dispatch(Box::new(move |plugin| { + let result = callback(plugin); + send.send(result).unwrap(); + }))) .unwrap(); - condvar - .wait_while(mutex.lock().unwrap(), |x| x.is_none()) - .unwrap() - .take() - .unwrap() + recv.recv().unwrap() } /// Prepare for audio processing. Returns an error if the plugin returned `false`. See diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index bee9d35..6ac48b1 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -1,14 +1,26 @@ use crate::{ plugin::{ ext::Extension, - instance::{InstanceMainThread, InstanceShared, MainThreadTask, PluginAudioThread, PluginStatus}, + instance::{CallbackEvent, PluginAudioThread, PluginShared, PluginStatus}, library::PluginMetadata, }, util::clap_call, }; use anyhow::Result; -use clap_sys::{factory::plugin_factory::clap_plugin_factory, plugin::clap_plugin}; -use std::{ffi::CStr, marker::PhantomData, panic::resume_unwind, pin::Pin, ptr::NonNull, sync::Arc}; +use clap_sys::plugin::clap_plugin; +use std::{ + marker::PhantomData, + panic::resume_unwind, + pin::Pin, + ptr::NonNull, + sync::{Arc, mpsc::Receiver}, +}; + +pub enum MainThreadTask { + Dispatch(Box), + CallbackRequest, + StopAudioThread, +} /// A CLAP plugin instance. The plugin will be deinitialized when this object is dropped. All /// functions here are callable only from the main thread. Use the @@ -17,22 +29,23 @@ use std::{ffi::CStr, marker::PhantomData, panic::resume_unwind, pin::Pin, ptr::N /// All functions on `Plugin` and the objects created from it will panic if the plugin is not in the /// correct state. pub struct Plugin<'lib> { - main: InstanceMainThread, + pub(super) callback_receiver: Receiver, + pub(super) task_receiver: Receiver, /// Information about this plugin instance stored on the host. This keeps track of things like /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - shared: Pin>, + pub(super) shared: Pin>, /// The CLAP plugin library this plugin instance was created from. This field is not used /// directly, but keeping a reference to the library here prevents the plugin instance from /// outliving the library. - _library: PhantomData<&'lib ()>, + pub(super) _library: PhantomData<&'lib ()>, /// To honor CLAP's thread safety guidelines, the thread this object was created from is /// designated the 'main thread', and this object cannot be shared with other threads. The /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call /// the plugin's audio thread functions. - _thread: PhantomData<*const ()>, + pub(super) _thread: PhantomData<*const ()>, } impl Drop for Plugin<'_> { @@ -64,29 +77,6 @@ impl Drop for Plugin<'_> { } impl<'lib> Plugin<'lib> { - /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the - /// plugin could not be created. The plugin instance will be registered with the host, and - /// unregistered when this object is dropped again. - /// - /// # Panics - /// This MUST be called on the OS main thread (if applicable). - /// - /// # Safety - /// The `factory` object must be valid. - pub(crate) unsafe fn create_plugin(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result { - assert!(IS_OS_MAIN_THREAD.with(|cell| cell.get()), "not main thread"); - - let (shared, main) = unsafe { InstanceShared::new(factory, plugin_id)? }; - - Ok(Plugin { - shared, - main, - - _library: PhantomData, - _thread: PhantomData, - }) - } - /// Get the raw pointer to the `clap_plugin` instance. pub fn as_ptr(&self) -> *const clap_plugin { self.shared.clap_plugin_ptr() @@ -163,9 +153,9 @@ impl<'lib> Plugin<'lib> { .unwrap(); // Handle callbacks requests on the main thread while the audio thread is running - while let Ok(task) = self.main.task_receiver.recv() { + while let Ok(task) = self.task_receiver.recv() { match task { - MainThreadTask::Dispatch { func, data } => func(self, data), + MainThreadTask::Dispatch(func) => func(self), MainThreadTask::CallbackRequest => self.handle_callback_unchecked(), MainThreadTask::StopAudioThread => break, } @@ -252,13 +242,3 @@ impl<'lib> Plugin<'lib> { } } } - -thread_local! { - static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; -} - -pub unsafe fn mark_current_thread_as_os_main_thread() { - IS_OS_MAIN_THREAD.with(|cell| { - cell.set(true); - }); -} diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index dd0d3e1..62546ad 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -1,4 +1,12 @@ -use crate::plugin::instance::{CallbackEvent, Plugin, PluginStatus}; +use crate::plugin::ext::Extension; +use crate::plugin::ext::audio_ports::AudioPorts; +use crate::plugin::ext::latency::Latency; +use crate::plugin::ext::note_ports::NotePorts; +use crate::plugin::ext::params::Params; +use crate::plugin::ext::preset_load::PresetLoad; +use crate::plugin::ext::state::State; +use crate::plugin::ext::tail::Tail; +use crate::plugin::instance::{CallbackEvent, MainThreadTask, Plugin, PluginStatus}; use crate::plugin::preset_discovery::LocationValue; use crate::util::{self, check_null_ptr, clap_call, validator_version}; use anyhow::{Context, Result}; @@ -10,6 +18,7 @@ use clap_sys::ext::preset_load::{CLAP_EXT_PRESET_LOAD, clap_host_preset_load}; use clap_sys::ext::state::{CLAP_EXT_STATE, clap_host_state}; use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_host_tail}; use clap_sys::ext::thread_check::{CLAP_EXT_THREAD_CHECK, clap_host_thread_check}; +use clap_sys::ext::thread_pool::{CLAP_EXT_THREAD_POOL, clap_host_thread_pool}; use clap_sys::ext::voice_info::{CLAP_EXT_VOICE_INFO, clap_host_voice_info}; use clap_sys::factory::plugin_factory::clap_plugin_factory; use clap_sys::factory::preset_discovery::clap_preset_discovery_location_kind; @@ -18,15 +27,25 @@ use clap_sys::id::clap_id; use clap_sys::plugin::clap_plugin; use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::ffi::{CStr, c_char, c_void}; -use std::pin::Pin; -use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, Mutex}; use std::thread::ThreadId; +thread_local! { + static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +pub unsafe fn mark_current_thread_as_os_main_thread() { + IS_OS_MAIN_THREAD.with(|cell| { + cell.set(true); + }); +} + /// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. /// This struct contains the `clap_host` and its extensions, as well as fields for tracking the plugin's state. -pub struct InstanceShared { +pub struct PluginShared { pub task_sender: Sender, pub callback_sender: Sender, pub callback_error: Mutex>, @@ -50,36 +69,29 @@ pub struct InstanceShared { clap_plugin: *const clap_plugin, clap_host: clap_host, - clap_host_audio_ports: clap_host_audio_ports, - clap_host_note_ports: clap_host_note_ports, - clap_host_params: clap_host_params, - clap_host_preset_load: clap_host_preset_load, - clap_host_state: clap_host_state, - clap_host_thread_check: clap_host_thread_check, - clap_host_latency: clap_host_latency, - clap_host_tail: clap_host_tail, - clap_host_voice_info: clap_host_voice_info, } -/// Information about a plugin instance's main thread. -pub struct InstanceMainThread { - pub callback_receiver: Receiver, - pub task_receiver: Receiver, -} - -pub enum MainThreadTask { - Dispatch { func: fn(&Plugin, *mut ()), data: *mut () }, - CallbackRequest, - StopAudioThread, -} +unsafe impl Send for PluginShared {} +unsafe impl Sync for PluginShared {} + +impl PluginShared { + /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the + /// plugin could not be created. The plugin instance will be registered with the host, and + /// unregistered when this object is dropped again. + /// + /// # Panics + /// This MUST be called on the OS main thread (if applicable). + /// + /// # Safety + /// The `factory` object must be valid. + pub unsafe fn create_plugin<'a>(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result> { + assert!(IS_OS_MAIN_THREAD.with(|cell| cell.get()), "not on main thread"); -impl InstanceShared { - pub unsafe fn new(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result<(Pin>, InstanceMainThread)> { let main_thread = std::thread::current().id(); let (callback_sender, callback_receiver) = channel(); let (task_sender, task_receiver) = channel(); - let shared = Arc::pin(InstanceShared { + let shared = Arc::pin(PluginShared { task_sender, callback_sender, callback_error: Mutex::new(None), @@ -104,47 +116,8 @@ impl InstanceShared { request_process: Some(Self::request_process), request_callback: Some(Self::request_callback), }, - - clap_host_audio_ports: clap_host_audio_ports { - is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), - rescan: Some(Self::ext_audio_ports_rescan), - }, - clap_host_note_ports: clap_host_note_ports { - supported_dialects: Some(Self::ext_note_ports_supported_dialects), - rescan: Some(Self::ext_note_ports_rescan), - }, - clap_host_preset_load: clap_host_preset_load { - on_error: Some(Self::ext_preset_load_on_error), - loaded: Some(Self::ext_preset_load_loaded), - }, - clap_host_params: clap_host_params { - rescan: Some(Self::ext_params_rescan), - clear: Some(Self::ext_params_clear), - request_flush: Some(Self::ext_params_request_flush), - }, - clap_host_state: clap_host_state { - mark_dirty: Some(Self::ext_state_mark_dirty), - }, - clap_host_thread_check: clap_host_thread_check { - is_main_thread: Some(Self::ext_thread_check_is_main_thread), - is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), - }, - clap_host_latency: clap_host_latency { - changed: Some(Self::ext_latency_changed), - }, - clap_host_tail: clap_host_tail { - changed: Some(Self::ext_tail_changed), - }, - clap_host_voice_info: clap_host_voice_info { - changed: Some(Self::ext_voice_info_changed), - }, }); - let main = InstanceMainThread { - callback_receiver, - task_receiver, - }; - // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct // so it can be retrieved in host callbacks unsafe { @@ -167,7 +140,14 @@ impl InstanceShared { (&raw const shared.clap_plugin).cast_mut().write(clap_plugin); } - Ok((shared, main)) + Ok(Plugin { + shared, + callback_receiver, + task_receiver, + + _library: std::marker::PhantomData, + _thread: std::marker::PhantomData, + }) } pub fn clap_host_ptr(&self) -> *const clap_host { @@ -181,7 +161,7 @@ impl InstanceShared { #[track_caller] unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { unsafe { - let state = (*host).host_data as *const InstanceShared; + let state = (*host).host_data as *const PluginShared; &*state } } @@ -240,31 +220,106 @@ impl InstanceShared { } } + /// Checks whether the plugin has the required extension(s). If it does not, then an error + /// will be set. Subsequent errors will not overwrite earlier ones. + fn assert_has_extension(&self, function_name: &str, ids: &[&CStr]) { + if self.status.load() == PluginStatus::Uninitialized { + self.set_callback_error(format!("'{}' called while the plugin is uninitialized.", function_name)); + return; + } + + for id in ids { + let extension_ptr = unsafe { + clap_call! { self.clap_plugin_ptr()=>get_extension(self.clap_plugin_ptr(), id.as_ptr()) } + }; + + if !extension_ptr.is_null() { + return; // found it! + } + } + + self.set_callback_error(format!( + "'{}' called without the required extension: {}", + function_name, + ids[0].to_string_lossy() + )); + } +} + +// Extensions +impl PluginShared { + const EXT_AUDIO_PORTS: clap_host_audio_ports = clap_host_audio_ports { + is_rescan_flag_supported: Some(Self::ext_audio_ports_is_rescan_flag_supported), + rescan: Some(Self::ext_audio_ports_rescan), + }; + + const EXT_NOTE_PORTS: clap_host_note_ports = clap_host_note_ports { + supported_dialects: Some(Self::ext_note_ports_supported_dialects), + rescan: Some(Self::ext_note_ports_rescan), + }; + + const EXT_PRESET_LOAD: clap_host_preset_load = clap_host_preset_load { + on_error: Some(Self::ext_preset_load_on_error), + loaded: Some(Self::ext_preset_load_loaded), + }; + + const EXT_PARAMS: clap_host_params = clap_host_params { + rescan: Some(Self::ext_params_rescan), + clear: Some(Self::ext_params_clear), + request_flush: Some(Self::ext_params_request_flush), + }; + + const EXT_STATE: clap_host_state = clap_host_state { + mark_dirty: Some(Self::ext_state_mark_dirty), + }; + + const EXT_THREAD_CHECK: clap_host_thread_check = clap_host_thread_check { + is_audio_thread: Some(Self::ext_thread_check_is_audio_thread), + is_main_thread: Some(Self::ext_thread_check_is_main_thread), + }; + + const EXT_THREAD_POOL: clap_host_thread_pool = clap_host_thread_pool { + request_exec: Some(Self::ext_thread_pool_request_exec), + }; + + const EXT_LATENCY: clap_host_latency = clap_host_latency { + changed: Some(Self::ext_latency_changed), + }; + + const EXT_TAIL: clap_host_tail = clap_host_tail { + changed: Some(Self::ext_tail_changed), + }; + + const EXT_VOICE_INFO: clap_host_voice_info = clap_host_voice_info { + changed: Some(Self::ext_voice_info_changed), + }; + unsafe extern "C" fn get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { - //check_null_ptr!(host, (*host).host_data, extension_id); - let this = unsafe { InstanceShared::from_clap_host(host) }; + check_null_ptr!(host, (*host).host_data, extension_id); // Right now there's no way to have the host only expose certain extensions. We can always // add that when test cases need it. let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { - &this.clap_host_audio_ports as *const _ as *const c_void + &Self::EXT_AUDIO_PORTS as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { - &this.clap_host_note_ports as *const _ as *const c_void + &Self::EXT_NOTE_PORTS as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { - &this.clap_host_preset_load as *const _ as *const c_void + &Self::EXT_PRESET_LOAD as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_PARAMS { - &this.clap_host_params as *const _ as *const c_void + &Self::EXT_PARAMS as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_STATE { - &this.clap_host_state as *const _ as *const c_void + &Self::EXT_STATE as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { - &this.clap_host_thread_check as *const _ as *const c_void + &Self::EXT_THREAD_CHECK as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_THREAD_POOL { + &Self::EXT_THREAD_POOL as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_LATENCY { - &this.clap_host_latency as *const _ as *const c_void + &Self::EXT_LATENCY as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_TAIL { - &this.clap_host_tail as *const _ as *const c_void + &Self::EXT_TAIL as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { - &this.clap_host_voice_info as *const _ as *const c_void + &Self::EXT_VOICE_INFO as *const _ as *const c_void } else { std::ptr::null() } @@ -272,7 +327,7 @@ impl InstanceShared { unsafe extern "C" fn request_restart(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and // in the multi-iteration run function it will trigger a deactivate->reactivate cycle @@ -282,7 +337,7 @@ impl InstanceShared { unsafe extern "C" fn request_process(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; // Handling this within the context of the validator would be a bit messy. Do plugins use // this? @@ -292,7 +347,7 @@ impl InstanceShared { unsafe extern "C" fn request_callback(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; // This this is either handled by `handle_callbacks_blocking()` while the audio thread is // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the @@ -304,18 +359,22 @@ impl InstanceShared { unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); + this.assert_has_extension("clap_host_audio_ports::is_rescan_flag_supported()", AudioPorts::IDS); + log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); true } unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::rescan()"); + this.assert_has_extension("clap_host_audio_ports::rescan()", AudioPorts::IDS); + log::trace!("'clap_host_audio_ports::rescan()' was called"); if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { @@ -333,9 +392,11 @@ impl InstanceShared { unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::supported_dialects()"); + this.assert_has_extension("clap_host_note_ports::supported_dialects()", NotePorts::IDS); + log::trace!("'clap_host_note_ports::supported_dialects()' was called"); CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE @@ -343,9 +404,11 @@ impl InstanceShared { unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::rescan()"); + this.assert_has_extension("clap_host_note_ports::rescan()", NotePorts::IDS); + log::trace!("'clap_host_note_ports::rescan()' was called"); if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { @@ -373,9 +436,10 @@ impl InstanceShared { msg: *const c_char, ) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::on_error()"); + this.assert_has_extension("clap_host_preset_load::on_error()", PresetLoad::IDS); let location = unsafe { LocationValue::new(location_kind, location) } .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); @@ -409,9 +473,10 @@ impl InstanceShared { load_key: *const c_char, ) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::loaded()"); + this.assert_has_extension("clap_host_preset_load::loaded()", PresetLoad::IDS); let location = unsafe { LocationValue::new(location_kind, location) } .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); @@ -430,9 +495,11 @@ impl InstanceShared { unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::rescan()"); + this.assert_has_extension("clap_host_params::rescan()", Params::IDS); + log::trace!("'clap_host_params::rescan()' was called"); if flags & CLAP_PARAM_RESCAN_VALUES != 0 { @@ -460,45 +527,54 @@ impl InstanceShared { unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::clear()"); + this.assert_has_extension("clap_host_params::clear()", Params::IDS); + log::debug!("TODO: Handle 'clap_host_params::clear()'"); } unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_not_audio_thread("clap_host_params::request_flush()"); + this.assert_has_extension("clap_host_params::request_flush()", Params::IDS); + log::trace!("'clap_host_params::request_flush()' was called"); this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); } unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_state::mark_dirty()"); + this.assert_has_extension("clap_host_state::mark_dirty()", State::IDS); + log::trace!("'clap_host_state::mark_dirty()' was called"); this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); } unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.main_thread_id == std::thread::current().id() } unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.audio_thread_id.load() == Some(std::thread::current().id()) } unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; + + this.assert_main_thread("clap_host_latency::changed()"); + this.assert_has_extension("clap_host_latency::changed()", Latency::IDS); if this.status.load() != PluginStatus::Activating { this.set_callback_error( @@ -506,29 +582,44 @@ impl InstanceShared { ); } - this.assert_main_thread("clap_host_latency::changed()"); log::trace!("'clap_host_latency::changed()' was called"); this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); } unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_audio_thread("clap_host_tail::changed()"); + this.assert_has_extension("clap_host_tail::changed()", Tail::IDS); + log::trace!("'clap_host_tail::changed()' was called"); this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); } unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { check_null_ptr!(host, (*host).host_data); - let this = unsafe { InstanceShared::from_clap_host(host) }; + + let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_voice_info::changed()"); + //this.assert_has_extension("clap_host_voice_info::changed()", VoiceInfo::IDS); + log::trace!("'clap_host_voice_info::changed()' was called"); this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); } -} -unsafe impl Send for InstanceShared {} -unsafe impl Sync for InstanceShared {} + unsafe extern "C" fn ext_thread_pool_request_exec(host: *const clap_host, num_tasks: u32) -> bool { + check_null_ptr!(host, (*host).host_data); + let this = unsafe { PluginShared::from_clap_host(host) }; + + this.assert_audio_thread("clap_host_thread_pool::request_exec()"); + this.assert_has_extension("clap_host_thread_pool::request_exec()", &[CLAP_EXT_THREAD_POOL]); + + (0..num_tasks).into_par_iter().for_each(|index| { + log::trace!("Executing thread pool task {index} of {num_tasks}"); + }); + + true + } +} diff --git a/src/plugin/library.rs b/src/plugin/library.rs index c05cb08..a33387f 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -2,6 +2,7 @@ use super::instance::Plugin; use super::preset_discovery::PresetDiscoveryFactory; +use crate::plugin::instance::PluginShared; use crate::util::{self, clap_call}; use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; @@ -264,7 +265,7 @@ impl PluginLibrary { } let id_cstring = CString::new(id).context("Plugin ID contained null bytes")?; - unsafe { Plugin::create_plugin(&*plugin_factory, &id_cstring) } + unsafe { PluginShared::create_plugin(&*plugin_factory, &id_cstring) } } /// Returns the plugin's preset discovery factory, if it has one. diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 820829d..e3489cc 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -3,7 +3,6 @@ use crate::util::{self, check_null_ptr, validator_version}; use anyhow::{Context, Result}; -use chrono::{DateTime, Utc}; use clap_sys::factory::preset_discovery::{ CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, CLAP_PRESET_DISCOVERY_LOCATION_FILE, CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN, @@ -18,6 +17,7 @@ use std::fmt::Display; use std::path::Path; use std::pin::Pin; use std::thread::ThreadId; +use time::OffsetDateTime; #[derive(Debug)] pub struct Indexer { @@ -289,7 +289,7 @@ pub struct Soundpack { pub homepage_url: Option, pub vendor: Option, pub image_path: Option, - pub release_timestamp: Option>, + pub release_timestamp: Option, } impl Soundpack { diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index 548c9bb..36d03e3 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -5,7 +5,6 @@ use super::{Flags, LocationValue}; use crate::util::{self, check_null_ptr}; use anyhow::{Context, Result}; -use chrono::{DateTime, Utc}; use clap_sys::factory::preset_discovery::{ CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, clap_preset_discovery_metadata_receiver, @@ -19,6 +18,7 @@ use std::ffi::{c_char, c_void}; use std::fmt::Display; use std::pin::Pin; use std::thread::ThreadId; +use time::OffsetDateTime; /// An implementation of the preset discovery's metadata receiver. This borrows a /// `Result` because the important work is done when this object is dropped. When this @@ -94,8 +94,8 @@ struct PartialPreset { pub flags: Option, pub creators: Vec, pub description: Option, - pub creation_time: Option>, - pub modification_time: Option>, + pub creation_time: Option, + pub modification_time: Option, pub features: Vec, pub extra_info: BTreeMap, } @@ -200,8 +200,10 @@ pub struct Preset { pub flags: PresetFlags, pub creators: Vec, pub description: Option, - pub creation_time: Option>, - pub modification_time: Option>, + + pub creation_time: Option, + pub modification_time: Option, + pub features: Vec, pub extra_info: BTreeMap, } diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 2d8d670..6134e25 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -1,5 +1,8 @@ //! Data structures and functions surrounding audio processing. -use crate::plugin::instance::{PluginAudioThread, PluginStatus}; +use crate::plugin::{ + ext::tail::Tail, + instance::{PluginAudioThread, PluginStatus, ProcessStatus}, +}; use anyhow::Result; use clap_sys::process::*; use std::pin::Pin; @@ -14,6 +17,8 @@ pub use transport::*; pub struct ProcessScope<'a> { plugin: &'a PluginAudioThread<'a>, + plugin_tail: Option>, + buffer: &'a mut AudioBuffers, events_input: Pin>, @@ -37,8 +42,9 @@ impl<'a> ProcessScope<'a> { Ok(ProcessScope { plugin, - buffer, + plugin_tail: plugin.get_extension(), + buffer, events_input: EventQueue::new(), events_output: EventQueue::new(), transport: TransportState::dummy(), @@ -76,11 +82,11 @@ impl<'a> ProcessScope<'a> { } } - pub fn run(&mut self) -> Result<()> { + pub fn run(&mut self) -> Result { self.run_with_block_size(self.buffer.len()) } - pub fn run_with_block_size(&mut self, samples: u32) -> Result<()> { + pub fn run_with_block_size(&mut self, samples: u32) -> Result { assert!(samples > 0 && samples <= self.buffer.len()); // check for requested restart @@ -91,8 +97,11 @@ impl<'a> ProcessScope<'a> { // check state, activate if needed if self.plugin.status() == PluginStatus::Deactivated { self.plugin.shared().requested_restart.store(false); + + let sample_rate = self.sample_rate; + let buffer_size = self.buffer.len(); self.plugin - .send_main_thread(|plugin| plugin.activate(self.sample_rate, 1, self.buffer.len()))?; + .dispatch_main(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; } // start processing if needed @@ -128,7 +137,7 @@ impl<'a> ProcessScope<'a> { // run processing let transport = self.transport.as_clap_transport(0); let (inputs, outputs) = self.buffer.clap_buffers(); - self.plugin.process(&clap_process { + let status = self.plugin.process(&clap_process { steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), frames_count: samples, transport: if self.transport.is_freerun { @@ -149,7 +158,15 @@ impl<'a> ProcessScope<'a> { self.transport.advance(samples as i64, self.sample_rate()); // check output audio buffers for NaNs or infinities - check_process_call_consistency(self.buffer.buffers(), &original_buffers, self.output_queue(), samples) + check_process_call_consistency(self.buffer.buffers(), &original_buffers, self.output_queue(), samples)?; + + if status == ProcessStatus::Tail && self.plugin_tail.is_none() { + anyhow::bail!( + "Plugin returned `CLAP_PROCESS_TAIL` process status but does not implement the 'tail' extension." + ); + } + + Ok(status) } pub fn restart(&mut self) { @@ -158,7 +175,7 @@ impl<'a> ProcessScope<'a> { } if self.plugin.status() == PluginStatus::Activated { - self.plugin.send_main_thread(|plugin| { + self.plugin.dispatch_main(|plugin| { plugin.deactivate(); }); } diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs index 36f5341..38ab0fd 100644 --- a/src/plugin/process/buffer.rs +++ b/src/plugin/process/buffer.rs @@ -314,6 +314,10 @@ impl AudioBuffers { buffer.fill_white_noise(prng); } } + + for input in &mut self.clap_inputs { + input.constant_mask = 0; + } } /// Fill the input buffers with silence (zeros), and mark all input channels as constant. diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index f941b99..27bfadd 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -23,6 +23,8 @@ pub enum PluginTestCase { FeaturesCategories, #[strum(serialize = "features-duplicates")] FeaturesDuplicates, + #[strum(serialize = "layout-audio-ports-activation")] + LayoutAudioPortsActivation, #[strum(serialize = "layout-audio-ports-config")] LayoutAudioPortsConfig, #[strum(serialize = "layout-configurable-audio-ports")] @@ -119,6 +121,11 @@ impl<'a> TestCase<'a> for PluginTestCase { support it.", PluginTestCase::ProcessAudioBasicInPlace, ), + PluginTestCase::LayoutAudioPortsActivation => format!( + "Same as '{}', but this time it toggles the activation state of audio ports on and off via the \ + 'audio-ports-activation' extension.", + PluginTestCase::ProcessAudioBasicOutOfPlace, + ), PluginTestCase::LayoutConfigurableAudioPorts => format!( "Same as '{}', but this time it tries random configurations exposed via the \ 'configurable-audio-ports' extension.", @@ -254,6 +261,9 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::DescriptorConsistency => descriptor::test_consistency(library, plugin_id), PluginTestCase::FeaturesCategories => descriptor::test_features_categories(library, plugin_id), PluginTestCase::FeaturesDuplicates => descriptor::test_features_duplicates(library, plugin_id), + PluginTestCase::LayoutAudioPortsActivation => { + layout::test_layout_audio_ports_activation(library, plugin_id) + } PluginTestCase::LayoutAudioPortsConfig => layout::test_layout_audio_ports_config(library, plugin_id), PluginTestCase::LayoutConfigurableAudioPorts => { layout::test_layout_configurable_audio_ports(library, plugin_id) diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 3f85a3f..794d785 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -223,16 +223,12 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b for permutation_no in 1..=FUZZ_NUM_PERMUTATIONS { current_events = Some(param_fuzzer.randomize_params_at(&mut prng, 0).collect()); - let mut have_set_parameters = false; let run_result = plugin.on_audio_thread(|plugin| -> Result<()> { let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - for _ in 0..FUZZ_RUNS_PER_PERMUTATION { - if !have_set_parameters { - process.input_queue().add_events(current_events.clone().unwrap()); - have_set_parameters = true; - } + process.input_queue().add_events(current_events.clone().unwrap()); + for _ in 0..FUZZ_RUNS_PER_PERMUTATION { process.audio_buffers().randomize(&mut prng); process .input_queue() diff --git a/src/util.rs b/src/util.rs index 38fa89e..e9c0936 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,13 +1,13 @@ //! Miscellaneous functions for data conversions. use anyhow::{Context, Result}; -use chrono::{DateTime, TimeZone, Utc}; use clap_sys::timestamp::{CLAP_TIMESTAMP_UNKNOWN, clap_timestamp}; use rayon::iter::{ParallelBridge, ParallelIterator}; use std::ffi::CString; use std::os::raw::c_char; use std::path::PathBuf; use std::{ffi::CStr, sync::OnceLock}; +use time::OffsetDateTime; /// Assert that the specified pointers are non-null. Panics if this is not the case. macro_rules! check_null_ptr { @@ -129,17 +129,16 @@ pub fn c_char_slice_to_string(slice: &[c_char]) -> Result { .map(String::from) } -/// Convert a `clap_timestamp` to an `Option>`. A value of `CLAP_TIMESTAMP_UNKNOWN` +/// Convert a `clap_timestamp` to an `Option`. A value of `CLAP_TIMESTAMP_UNKNOWN` /// gets translated to `None`. -pub fn parse_timestamp(timestamp: clap_timestamp) -> Result>> { +pub fn parse_timestamp(timestamp: clap_timestamp) -> Result> { let parsed = if timestamp == CLAP_TIMESTAMP_UNKNOWN { None } else { - Some(match Utc.timestamp_millis_opt(timestamp as i64) { - chrono::LocalResult::Single(datetime) => datetime, - // This shouldn't happen - _ => anyhow::bail!("Could not parse the timestamp."), - }) + Some( + OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128 * 1_000_000) + .map_err(|_| anyhow::anyhow!("Could not parse the timestamp."))?, + ) }; Ok(parsed) @@ -169,6 +168,48 @@ pub fn validator_version() -> &'static CStr { .as_c_str() } +pub fn install_panic_hook() { + std::panic::set_hook(Box::new(move |info| { + let backtrace = std::backtrace::Backtrace::capture(); + let backtrace = if backtrace.status() == std::backtrace::BacktraceStatus::Disabled { + String::from(". Set RUST_BACKTRACE=1 for a backtrace.") + } else { + format!("\n{}", backtrace) + }; + + let thread = std::thread::current(); + let thread = thread.name().unwrap_or(""); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; + + match info.location() { + Some(location) => { + log::error!( + target: "panic", "thread '{}' panicked at '{}': {}:{}{}", + thread, + msg, + location.file(), + location.line(), + backtrace + ); + } + None => log::error!( + target: "panic", + "thread '{}' panicked at '{}'{:?}", + thread, + msg, + backtrace + ), + } + })); +} + impl IteratorExt for T where T: Iterator {} pub trait IteratorExt: Iterator { /// Map the iterator in parallel if `parallel` is `true`, or sequentially if it is `false`. diff --git a/src/validator.rs b/src/validator.rs index 3c89345..0529f17 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -379,7 +379,6 @@ impl ValidationResult { pub fn union(mut self, other: Self) -> Self { self.plugin_library_tests.extend(other.plugin_library_tests); self.plugin_tests.extend(other.plugin_tests); - self } } diff --git a/tests/clack-synth/Cargo.toml b/tests/clack-synth/Cargo.toml index 9ed6649..d836680 100644 --- a/tests/clack-synth/Cargo.toml +++ b/tests/clack-synth/Cargo.toml @@ -9,5 +9,5 @@ publish = false crate-type = ["cdylib"] [dependencies] -clack-plugin = { git = "https://github.com/Quant1um/clack", rev = "bd6f37959270153cf2118923275cd54ad20db958" } -clack-extensions = { git = "https://github.com/Quant1um/clack", rev = "bd6f37959270153cf2118923275cd54ad20db958", features = ["audio-ports", "audio-ports-config", "configurable-audio-ports", "clack-plugin", "note-ports", "params", "state"] } +clack-plugin = { git = "https://github.com/Quant1um/clack", rev = "8f39ba01643103bac2acaabad1b22ea9ee452400" } +clack-extensions = { git = "https://github.com/Quant1um/clack", rev = "8f39ba01643103bac2acaabad1b22ea9ee452400", features = ["audio-ports", "audio-ports-config", "audio-ports-activation", "configurable-audio-ports", "clack-plugin", "note-ports", "params", "state"] } diff --git a/tests/clack-synth/src/lib.rs b/tests/clack-synth/src/lib.rs index f621575..9e48187 100644 --- a/tests/clack-synth/src/lib.rs +++ b/tests/clack-synth/src/lib.rs @@ -1,8 +1,11 @@ use crate::params::{PolySynthParamModulations, PolySynthParams}; use crate::poly_oscillator::PolyOscillator; +use clack_extensions::audio_ports_activation::{ + PluginAudioPortsActivation, PluginAudioPortsActivationImpl, PluginAudioPortsActivationSetImpl, SampleSize, +}; use clack_extensions::audio_ports_config::{ AudioPortConfigWriter, AudioPortsConfiguration, MainPortInfo, PluginAudioPortsConfig, PluginAudioPortsConfigImpl, - PluginAudioPortsConfigInfoImpl, + PluginAudioPortsConfigInfo, PluginAudioPortsConfigInfoImpl, }; use clack_extensions::configurable_audio_ports::{ AudioPortsRequestList, PluginConfigurableAudioPorts, PluginConfigurableAudioPortsImpl, @@ -32,6 +35,8 @@ impl Plugin for PolySynthPlugin { .register::() .register::() .register::() + .register::() + .register::() .register::(); } } @@ -60,6 +65,7 @@ impl DefaultPluginFactory for PolySynthPlugin { Ok(PolySynthPluginMainThread { shared, config: ClapId::new(2), + active: true, }) } } @@ -268,6 +274,33 @@ impl PluginConfigurableAudioPortsImpl for PolySynthPluginMainThread<'_> { } } +impl PluginAudioPortsActivationImpl for PolySynthPluginMainThread<'_> { + fn can_activate_while_processing(&mut self) -> bool { + false + } +} + +impl PluginAudioPortsActivationSetImpl for PolySynthPluginMainThread<'_> { + fn set_active(&mut self, is_input: bool, port_index: u32, is_active: bool, sample_size: SampleSize) -> bool { + if is_input || port_index != 0 { + return false; + } + + if sample_size == SampleSize::Float64 { + return false; + } + + self.active = is_active; + true + } +} + +impl PluginAudioPortsActivationSetImpl for PolySynthAudioProcessor<'_> { + fn set_active(&mut self, _: bool, _: u32, _: bool, _: SampleSize) -> bool { + false + } +} + pub struct PolySynthPluginShared { params: PolySynthParams, } @@ -277,6 +310,7 @@ impl PluginShared<'_> for PolySynthPluginShared {} pub struct PolySynthPluginMainThread<'a> { shared: &'a PolySynthPluginShared, config: ClapId, + active: bool, } impl<'a> PluginMainThread<'a, PolySynthPluginShared> for PolySynthPluginMainThread<'a> {} From 0e8faf5b34e40ea9ab6049fd1880f6cd02b69342 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Fri, 30 Jan 2026 19:02:57 +0400 Subject: [PATCH 048/114] - poll_callback which handles various callback events (currently unused) - more surround/ambisonic consistency tests for audio-ports and audio-ports-config --- src/plugin/ext/ambisonic.rs | 31 ++- src/plugin/ext/audio_ports.rs | 271 ++++++++++--------- src/plugin/ext/audio_ports_config.rs | 74 ++--- src/plugin/ext/surround.rs | 45 ++- src/plugin/instance/audio_thread.rs | 2 +- src/plugin/instance/main_thread.rs | 24 +- src/plugin/instance/shared.rs | 13 +- src/plugin/process/buffer.rs | 1 - src/tests/plugin.rs | 2 +- src/tests/plugin/layout.rs | 8 +- src/tests/plugin/params.rs | 44 ++- src/tests/plugin/processing.rs | 36 ++- src/tests/plugin/state.rs | 68 +++-- src/tests/plugin/transport.rs | 12 +- src/tests/plugin_library/preset_discovery.rs | 8 +- 15 files changed, 402 insertions(+), 237 deletions(-) diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index 5f084e0..512bdb4 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -1,8 +1,10 @@ -use crate::plugin::{ext::Extension, instance::Plugin}; -use clap_sys::ext::ambisonic::{CLAP_EXT_AMBISONIC, CLAP_EXT_AMBISONIC_COMPAT, clap_plugin_ambisonic}; -use std::{ffi::CStr, ptr::NonNull}; +use crate::{ + plugin::{ext::Extension, instance::Plugin}, + util::clap_call, +}; +use clap_sys::ext::ambisonic::*; +use std::{ffi::CStr, mem::zeroed, ptr::NonNull}; -#[allow(unused)] pub struct Ambisonic<'a> { plugin: &'a Plugin<'a>, ambisonic: NonNull, @@ -20,3 +22,24 @@ impl<'a> Extension<&'a Plugin<'a>> for Ambisonic<'a> { } } } + +impl<'a> Ambisonic<'a> { + pub fn is_config_supported(&self, config: &clap_ambisonic_config) -> bool { + let ambisonic = self.ambisonic.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { ambisonic=>is_config_supported(plugin, config) } + } + } + + pub fn get_config(&self, is_input: bool, port_index: u32) -> Option { + let ambisonic = self.ambisonic.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut config = clap_ambisonic_config { ..zeroed() }; + let result = clap_call! { ambisonic=>get_config(plugin, is_input, port_index, &mut config) }; + if result { Some(config) } else { None } + } + } +} diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index ab1e3f3..e692723 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -69,17 +69,13 @@ impl AudioPorts<'_> { pub fn config(&self) -> Result { let mut config = AudioPortConfig::default(); - let has_ambisonic = self.plugin.get_extension::().is_some(); - let has_surround = self.plugin.get_extension::().is_some(); - - // TODO: Refactor this to reduce the duplication a little without hurting the human readable error messages let audio_ports = self.audio_ports.as_ptr(); let plugin = self.plugin.as_ptr(); - let num_inputs = unsafe { - clap_call! { audio_ports=>count(plugin, true) } - }; - let num_outputs = unsafe { - clap_call! { audio_ports=>count(plugin, false) } + let (num_inputs, num_outputs) = unsafe { + ( + clap_call! { audio_ports=>count(plugin, true) }, + clap_call! { audio_ports=>count(plugin, false) }, + ) }; // Audio ports have a stable ID attribute that can be used to connect input and output ports @@ -91,147 +87,73 @@ impl AudioPorts<'_> { let mut input_stable_index_pairs: HashMap = HashMap::new(); let mut output_stable_index_pairs: HashMap = HashMap::new(); - let mut has_single_precision_requires_common_port = false; - let mut has_double_precision_requires_common_port = false; - - for i in 0..num_inputs { + for index in 0..num_inputs { let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; let success = unsafe { - clap_call! { audio_ports=>get(plugin, i, true, &mut info) } + clap_call! { audio_ports=>get(plugin, index, true, &mut info) } }; if !success { anyhow::bail!( - "Plugin returned an error when querying input audio port {i} ({num_inputs} total input ports)." + "Plugin returned an error when querying input audio port {index} ({num_inputs} total input ports)." ); } - is_audio_port_type_consistent( - if info.port_type.is_null() { - None - } else { - Some(unsafe { CStr::from_ptr(info.port_type) }) - }, - info.channel_count, - has_ambisonic, - has_surround, - ) - .with_context(|| format!("Inconsistent channel count for output port {i}"))?; - // We'll convert these stable IDs to vector indices later - if input_stable_index_pairs.contains_key(&info.id) { - anyhow::bail!("The stable ID of input audio port {i} (id={}) is a duplicate.", info.id); - } - input_stable_index_pairs.insert(info.id, (i as usize, info.in_place_pair)); - - // Check is main - let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; - if is_main && i != 0 { - anyhow::bail!( - "Input audio port {i} (id={}) is marked as main, but it is not the first port in the list.", - info.id - ); - } - - let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; - let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; - let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; - - if prefers_double_sample_size && !supports_double_sample_size { + if input_stable_index_pairs + .insert(info.id, (index as usize, info.in_place_pair)) + .is_some() + { anyhow::bail!( - "Input audio port {i} (id={}) prefers 64-bit sample size, but does not support it.", + "The stable ID of input audio port {index} (id={}) is a duplicate.", info.id ); } - if requires_common_sample_size { - if supports_double_sample_size { - has_double_precision_requires_common_port = true; - } else { - has_single_precision_requires_common_port = true; - } - } - - config.inputs.push(AudioPort { - is_main, - num_channels: info.channel_count, - // These are reconstructed from `input_stable_index_pairs` and - // `output_stable_index_pairs` later - in_place_pair_idx: None, - - supports_double_sample_size, - requires_common_sample_size, - }); + config + .inputs + .push(check_audio_port_info_valid(self.plugin, true, index, &info)?); } - for i in 0..num_outputs { + for index in 0..num_outputs { let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; let success = unsafe { - clap_call! { audio_ports=>get(plugin, i, false, &mut info) } + clap_call! { audio_ports=>get(plugin, index, false, &mut info) } }; if !success { anyhow::bail!( - "Plugin returned an error when querying output audio port {i} ({num_outputs} total output ports)." - ); - } - - is_audio_port_type_consistent( - if info.port_type.is_null() { - None - } else { - Some(unsafe { CStr::from_ptr(info.port_type) }) - }, - info.channel_count, - has_ambisonic, - has_surround, - ) - .with_context(|| format!("Inconsistent channel count for output port {i}"))?; - - if output_stable_index_pairs.contains_key(&info.id) { - anyhow::bail!( - "The stable ID of output audio port {i} (id={}) is a duplicate.", - info.id + "Plugin returned an error when querying output audio port {index} ({num_outputs} total output \ + ports)." ); } - output_stable_index_pairs.insert(info.id, (i as usize, info.in_place_pair)); - let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; - if is_main && i != 0 { - anyhow::bail!( - "Output audio port {i} (id={}) is marked as main, but it is not the first port in the list.", - info.id - ); - } - - let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; - let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; - let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; - - if prefers_double_sample_size && !supports_double_sample_size { + if output_stable_index_pairs + .insert(info.id, (index as usize, info.in_place_pair)) + .is_some() + { anyhow::bail!( - "Output audio port {i} (id={}) prefers 64-bit sample size, but does not support it.", + "The stable ID of output audio port {index} (id={}) is a duplicate.", info.id ); } - if requires_common_sample_size { - if supports_double_sample_size { - has_double_precision_requires_common_port = true; - } else { - has_single_precision_requires_common_port = true; - } - } + config + .outputs + .push(check_audio_port_info_valid(self.plugin, false, index, &info)?); + } - config.outputs.push(AudioPort { - is_main, - num_channels: info.channel_count, - in_place_pair_idx: None, + let has_single_precision_requires_common_port = config + .inputs + .iter() + .chain(config.outputs.iter()) + .any(|port| port.requires_common_sample_size && !port.supports_double_sample_size); - supports_double_sample_size, - requires_common_sample_size, - }); - } + let has_double_precision_requires_common_port = config + .inputs + .iter() + .chain(config.outputs.iter()) + .any(|port| port.requires_common_sample_size && port.supports_double_sample_size); // this implies that the common sample size requirement is useless (i.e. every port can only support // 32bit sample size) and nullifies the 64 bit support of the other ports @@ -315,13 +237,76 @@ impl AudioPorts<'_> { } } -/// Check whether the number of channels matches an audio port's type string, if that is set. -/// Returns an error if the port type is not consistent -pub fn is_audio_port_type_consistent( +pub fn check_audio_port_info_valid( + plugin: &Plugin, + is_input: bool, + port_index: u32, + info: &clap_audio_port_info, +) -> Result { + let ext_ambisonic = plugin.get_extension::(); + let ext_surround = plugin.get_extension::(); + + let port_type = if info.port_type.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(info.port_type) }) + }; + + // check consistency between port type and channel count / extensions + check_audio_port_type_consistent( + is_input, + port_index, + port_type, + info.channel_count, + ext_ambisonic.as_ref(), + ext_surround.as_ref(), + ) + .with_context(|| { + format!( + "Inconsistent port info for {} port {port_index}", + if is_input { "input" } else { "output" } + ) + })?; + + // if the main port flag is set, the port index must be 0 + let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; + if is_main && port_index != 0 { + anyhow::bail!( + "{} audio port {port_index} is marked as main, but it is not the first port in the list.", + if is_input { "Input" } else { "Output" } + ); + } + + let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; + let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; + + if !supports_double_sample_size && prefers_double_sample_size { + anyhow::bail!( + "{} audio port {port_index} prefers 64-bit sample size, but does not support it.", + if is_input { "Input" } else { "Output" } + ); + } + + Ok(AudioPort { + is_main: (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0, + num_channels: info.channel_count, + in_place_pair_idx: None, + + supports_double_sample_size, + requires_common_sample_size, + }) +} + +/// Check if the returned port information consistent with the audio port type, ambisonic extension, surround extension, etc. +/// Returns an error if the port information is not consistent. +pub fn check_audio_port_type_consistent( + is_input: bool, + port_index: u32, port_type: Option<&CStr>, channel_count: u32, - has_ambisonic: bool, - has_surround: bool, + ext_ambisonic: Option<&Ambisonic>, + ext_surround: Option<&Surround>, ) -> Result<()> { if port_type.is_none() { return Ok(()); @@ -331,29 +316,50 @@ pub fn is_audio_port_type_consistent( if channel_count == 1 { Ok(()) } else { - anyhow::bail!("Expected 1 channel, but the audio port has {} channels.", channel_count); + anyhow::bail!( + "Audio port type is 'mono', but the audio port has {} channels.", + channel_count + ); } } else if port_type == Some(CLAP_PORT_STEREO) { if channel_count == 2 { Ok(()) } else { anyhow::bail!( - "Expected 2 channels, but the audio port has {} channel(s).", + "Audio port type is 'stereo', but the audio port has {} channel(s).", channel_count ); } } else if port_type == Some(CLAP_PORT_SURROUND) { - if !has_surround { + let Some(ext_surround) = ext_surround else { anyhow::bail!("Audio port type is 'surround', but the plugin does not implement the 'surround' extension."); + }; + + let channel_map = ext_surround.get_channel_map(is_input, port_index, channel_count); + if channel_map.len() as u32 != channel_count { + anyhow::bail!( + "The surround channel map returned by 'clap_plugin_surround::get_channel_map' has length {}, but the \ + audio port has {} channels.", + channel_map.len(), + channel_count + ); + } + + let mask = channel_map.iter().fold(0u64, |acc, &ch| acc | (1u64 << ch)); + if !ext_surround.is_channel_mask_supported(mask) { + anyhow::bail!( + "The surround channel mask {mask:#b} returned by 'clap_plugin_surround::get_channel_map' is not \ + supported by the plugin ('clap_plugin_surround::is_channel_mask_supported' returned false)." + ); } Ok(()) } else if port_type == Some(CLAP_PORT_AMBISONIC) { - if !has_ambisonic { + let Some(ext_ambisonic) = ext_ambisonic else { anyhow::bail!( "Audio port type is 'ambisonic', but the plugin does not implement the 'ambisonic' extension." ); - } + }; // ambisonic audio requires (N^2) channels where N is the ambisonics order if channel_count.isqrt().pow(2) != channel_count { @@ -364,6 +370,17 @@ pub fn is_audio_port_type_consistent( ); } + let config = ext_ambisonic + .get_config(is_input, port_index) + .context("Failed to get ambisonic configuration for the port.")?; + + if !ext_ambisonic.is_config_supported(&config) { + anyhow::bail!( + "The ambisonic configuration returned by 'clap_plugin_ambisonic::get_config' is not supported by the \ + plugin ('clap_plugin_ambisonic::is_config_supported' returned false).", + ); + } + Ok(()) } else { log::warn!("Unknown audio port type '{port_type:?}'"); diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index dec9680..9a62504 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::ext::ambisonic::Ambisonic; -use crate::plugin::ext::audio_ports::is_audio_port_type_consistent; +use crate::plugin::ext::audio_ports::check_audio_port_type_consistent; use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; use crate::util::{c_char_slice_to_string, clap_call}; @@ -65,8 +65,8 @@ impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfigInfo<'a> { impl AudioPortsConfig<'_> { pub fn enumerate(&self) -> Result> { - let has_ambisonic = self.plugin.get_extension::().is_some(); - let has_surround = self.plugin.get_extension::().is_some(); + let ext_ambisonic = self.plugin.get_extension::(); + let ext_surround = self.plugin.get_extension::(); let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -76,47 +76,55 @@ impl AudioPortsConfig<'_> { (0..count) .map(|i| unsafe { - let mut dst = clap_audio_ports_config { ..zeroed() }; - let result = clap_call! { audio_ports_config=>get(plugin, i, &mut dst) }; + let mut info = clap_audio_ports_config { ..zeroed() }; + let result = clap_call! { audio_ports_config=>get(plugin, i, &mut info) }; if !result { anyhow::bail!("audio_ports_config::get({}) returned false", i); } - if dst.has_main_input { - is_audio_port_type_consistent( - if dst.main_input_port_type.is_null() { - None - } else { - Some(CStr::from_ptr(dst.main_input_port_type)) - }, - dst.main_input_channel_count, - has_ambisonic, - has_surround, + if info.has_main_input { + let port_type = if info.main_input_port_type.is_null() { + None + } else { + Some(CStr::from_ptr(info.main_input_port_type)) + }; + + check_audio_port_type_consistent( + true, + 0, + port_type, + info.main_input_channel_count, + ext_ambisonic.as_ref(), + ext_surround.as_ref(), ) - .with_context(|| format!("Inconsistent channel count for main input port for config {i}"))?; + .with_context(|| format!("Inconsistent main input port info for config {i}"))?; } - if dst.has_main_output { - is_audio_port_type_consistent( - if dst.main_output_port_type.is_null() { - None - } else { - Some(CStr::from_ptr(dst.main_output_port_type)) - }, - dst.main_output_channel_count, - has_ambisonic, - has_surround, + if info.has_main_output { + let port_type = if info.main_output_port_type.is_null() { + None + } else { + Some(CStr::from_ptr(info.main_output_port_type)) + }; + + check_audio_port_type_consistent( + false, + 0, + port_type, + info.main_output_channel_count, + ext_ambisonic.as_ref(), + ext_surround.as_ref(), ) - .with_context(|| format!("Inconsistent channel count for main output port for config {i}"))?; + .with_context(|| format!("Inconsistent main output port info for config {i}"))?; } Ok(AudioPortsConfigConfig { - id: dst.id, - name: c_char_slice_to_string(&dst.name)?, - input_port_count: dst.input_port_count, - output_port_count: dst.output_port_count, - main_input_channel_count: dst.has_main_input.then_some(dst.main_input_channel_count), - main_output_channel_count: dst.has_main_output.then_some(dst.main_output_channel_count), + id: info.id, + name: c_char_slice_to_string(&info.name)?, + input_port_count: info.input_port_count, + output_port_count: info.output_port_count, + main_input_channel_count: info.has_main_input.then_some(info.main_input_channel_count), + main_output_channel_count: info.has_main_output.then_some(info.main_output_channel_count), }) }) .collect() diff --git a/src/plugin/ext/surround.rs b/src/plugin/ext/surround.rs index 20dd74d..a61b552 100644 --- a/src/plugin/ext/surround.rs +++ b/src/plugin/ext/surround.rs @@ -1,8 +1,10 @@ -use crate::plugin::{ext::Extension, instance::Plugin}; -use clap_sys::ext::surround::{CLAP_EXT_SURROUND, CLAP_EXT_SURROUND_COMPAT, clap_plugin_surround}; +use crate::{ + plugin::{ext::Extension, instance::Plugin}, + util::clap_call, +}; +use clap_sys::ext::surround::*; use std::{ffi::CStr, ptr::NonNull}; -#[allow(unused)] pub struct Surround<'a> { plugin: &'a Plugin<'a>, surround: NonNull, @@ -20,3 +22,40 @@ impl<'a> Extension<&'a Plugin<'a>> for Surround<'a> { } } } + +impl<'a> Surround<'a> { + pub fn is_channel_mask_supported(&self, channel_mask: u64) -> bool { + let surround = self.surround.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + clap_call! { + surround=>is_channel_mask_supported( + plugin, + channel_mask + ) + } + } + } + + pub fn get_channel_map(&self, is_input: bool, port_index: u32, channel_count: u32) -> Vec { + let surround = self.surround.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut channel_map = vec![0u8; channel_count as usize]; + let channels_real = clap_call! { + surround=>get_channel_map( + plugin, + is_input, + port_index, + channel_map.as_mut_ptr(), + channel_count + ) + }; + + channel_map.truncate(channels_real as usize); + channel_map + } + } +} diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 00e8650..9363240 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -67,7 +67,7 @@ impl<'a> PluginAudioThread<'a> { } /// Get a reference to the plugin's shared state. - pub fn shared(&self) -> &Pin> { + pub fn shared(&self) -> &PluginShared { &self.shared } diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index 6ac48b1..3b219f3 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -60,15 +60,12 @@ impl Drop for Plugin<'_> { // Make sure the plugin is in the correct state before it gets destroyed match self.status() { PluginStatus::Uninitialized | PluginStatus::Deactivated => (), - PluginStatus::Activated => self.deactivate(), - status => log::warn!( + status => panic!( "The plugin was in an invalid state '{status:?}' when the instance got dropped, this is a \ clap-validator bug" ), } - self.handle_callback_unchecked(); - let plugin = self.as_ptr(); unsafe { clap_call! { plugin=>destroy(plugin) } @@ -99,19 +96,18 @@ impl<'lib> Plugin<'lib> { self.shared.status.load() } - /// Handle any pending main-thread callbacks for this plugin. + /// Handle any pending main-thread callbacks for this plugin and pending callback events. /// Returns an error if there is a callback error pending. - pub fn handle_callback(&self) -> Result<()> { - self.handle_callback_unchecked(); + pub fn poll_callback(&self, mut f: impl FnMut(CallbackEvent)) -> Result<()> { + self.poll_callback_unchecked(); if let Some(error) = self.shared.callback_error.lock().unwrap().take() { anyhow::bail!(error); } - // TODO: - // while let Ok(event) = self.shared.callback_receiver.lock().unwrap().recv() { - // println!("{:?}", event); - // } + while let Ok(event) = self.callback_receiver.try_recv() { + f(event); + } Ok(()) } @@ -156,7 +152,7 @@ impl<'lib> Plugin<'lib> { while let Ok(task) = self.task_receiver.recv() { match task { MainThreadTask::Dispatch(func) => func(self), - MainThreadTask::CallbackRequest => self.handle_callback_unchecked(), + MainThreadTask::CallbackRequest => self.poll_callback_unchecked(), MainThreadTask::StopAudioThread => break, } } @@ -165,7 +161,7 @@ impl<'lib> Plugin<'lib> { thread.join() }); - self.handle_callback_unchecked(); + self.poll_callback_unchecked(); result.flatten().unwrap_or_else(|e| resume_unwind(e)) } @@ -233,7 +229,7 @@ impl<'lib> Plugin<'lib> { self.shared.status.store(PluginStatus::Deactivated); } - fn handle_callback_unchecked(&self) { + fn poll_callback_unchecked(&self) { if self.shared.requested_callback.swap(false) { let plugin = self.as_ptr(); unsafe { diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index 62546ad..b9e7543 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -33,14 +33,10 @@ use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, Mutex}; use std::thread::ThreadId; -thread_local! { - static IS_OS_MAIN_THREAD: std::cell::Cell = const { std::cell::Cell::new(false) }; -} +static OS_MAIN_THREAD: AtomicCell> = AtomicCell::new(None); pub unsafe fn mark_current_thread_as_os_main_thread() { - IS_OS_MAIN_THREAD.with(|cell| { - cell.set(true); - }); + OS_MAIN_THREAD.store(Some(std::thread::current().id())); } /// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. @@ -85,7 +81,10 @@ impl PluginShared { /// # Safety /// The `factory` object must be valid. pub unsafe fn create_plugin<'a>(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result> { - assert!(IS_OS_MAIN_THREAD.with(|cell| cell.get()), "not on main thread"); + assert!( + OS_MAIN_THREAD.load() == Some(std::thread::current().id()), + "not on main thread" + ); let main_thread = std::thread::current().id(); let (callback_sender, callback_receiver) = channel(); diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs index 38ab0fd..14b7869 100644 --- a/src/plugin/process/buffer.rs +++ b/src/plugin/process/buffer.rs @@ -307,7 +307,6 @@ impl AudioBuffers { } /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). - /// Output buffers are filled with random NaN values to detect if they have been written to. pub fn randomize(&mut self, prng: &mut Pcg32) { for buffer in self.buffers_mut() { if buffer.is_input() { diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 27bfadd..1bd98a1 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -81,7 +81,7 @@ pub enum PluginTestCase { TransportNull, #[strum(serialize = "transport-fuzz")] TransportFuzz, - #[strum(serialize = "transport-sample-accurate")] + #[strum(serialize = "transport-fuzz-sample-accurate")] TransportFuzzSampleAccurate, } diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 1f20e74..b385bcc 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -189,7 +189,9 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) })?; } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -327,7 +329,9 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: })?; } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if checks_passed == 0 { return Ok(TestStatus::Warning { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 794d785..43ae607 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -69,7 +69,9 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let param_infos = params .info() @@ -141,7 +143,9 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { return Ok(TestStatus::Skipped { @@ -191,7 +195,9 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -275,7 +281,9 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b std::mem::swap(&mut previous_events, &mut current_events); } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -302,7 +310,9 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -354,7 +364,9 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) })?; } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -409,7 +421,9 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -437,7 +451,9 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let param_infos = params .info() @@ -477,7 +493,9 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if actual_param_values == initial_param_values { Ok(TestStatus::Success { details: None }) @@ -508,7 +526,9 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let param_infos = params .info() @@ -530,7 +550,9 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 6a6a700..65408a0 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -50,7 +50,9 @@ pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_pla Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -84,7 +86,9 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let has_double_support = audio_ports_config .inputs @@ -119,7 +123,9 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -191,7 +197,9 @@ pub fn test_process_note_out_of_place( Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -245,7 +253,9 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st .with_context(|| format!("Error while processing with {:.2}hz sample rate", sample_rate))?; } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -296,7 +306,9 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str .with_context(|| format!("Error while processing with buffer size of {}", buffer_size))?; } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -350,7 +362,9 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) } @@ -441,7 +455,9 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str Ok(()) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if !has_received_constant_flag && has_received_constant_output { return Ok(TestStatus::Warning { @@ -523,7 +539,9 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: Ok(TestStatus::Success { details: None }) })?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; Ok(result) } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 344adfc..9c6cc67 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -43,7 +43,9 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res let result = state.load(&[]); - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; match result { Ok(_) => Ok(TestStatus::Warning { @@ -75,7 +77,9 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let mut random_data = vec![0u8; 1024 * 1024]; let mut succeeded = false; @@ -85,7 +89,9 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re succeeded |= state.load(&random_data).is_ok(); } - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; match succeeded { false => Ok(TestStatus::Success { details: None }), @@ -143,7 +149,9 @@ pub fn test_state_reproducibility_basic( } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let param_infos = params .info() @@ -188,7 +196,9 @@ pub fn test_state_reproducibility_basic( let expected_state = state.save()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -227,11 +237,15 @@ pub fn test_state_reproducibility_basic( } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; state.load(&expected_state)?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -258,7 +272,9 @@ pub fn test_state_reproducibility_basic( // Now for the moment of truth let actual_state = state.save()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) @@ -313,7 +329,9 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let param_infos = params .info() @@ -337,7 +355,9 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; // We'll compare against these values in that second pass let expected_param_values: BTreeMap = param_infos @@ -346,7 +366,9 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str .collect::>>()?; let expected_state = state.save()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; // Plugins with no parameters at all should of course not trigger this error if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { @@ -400,7 +422,9 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; // NOTE: We can reuse random parameter set events, except that the cookie pointers may be // different if the plugin uses those. So we need to update these cookies first. @@ -456,7 +480,9 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let actual_state = state.save()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) @@ -539,7 +565,9 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> // treating this as the ground truth. let expected_state = state.save()?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; (expected_state, expected_param_values) }; @@ -577,12 +605,16 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> } }; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -607,7 +639,9 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; - plugin.handle_callback().context("An error occured during a callback")?; + plugin + .poll_callback(|_| {}) + .context("An error occured during a callback")?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index 8321515..3908d29 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -56,7 +56,9 @@ pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result Result Result // successful, but it doesn't matter if the plugin doesn't have any audio ports let audio_ports = plugin.get_extension::(); plugin - .handle_callback() + .poll_callback(|_| {}) .context("An error occured during a host callback")?; let audio_ports_config = audio_ports @@ -150,7 +150,7 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // In case the plugin uses `clap_host_preset_load::on_error()` to report an error, // we will check that first before making sure the preset loaded correctly. This // might otherwise mask the error message. - plugin.handle_callback().with_context(|| { + plugin.poll_callback(|_| {}).with_context(|| { format!( "An error occurred while loading the preset '{}' for plugin '{}'", preset.name, plugin_id @@ -171,12 +171,12 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result })?; plugin - .handle_callback() + .poll_callback(|_| {}) .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } plugin - .handle_callback() + .poll_callback(|_| {}) .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } } From 533b4d90b088da3616f063e89adc36e802faf688 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sat, 31 Jan 2026 19:43:41 +0400 Subject: [PATCH 049/114] refactor audio buffers abstraction; move in-place port checks to in-place tests (lazy check); proper threadpool support; audio-ports-config info consistency checks; minor refactor here and there --- src/main.rs | 2 +- src/plugin/ext.rs | 11 +- src/plugin/ext/audio_ports.rs | 241 +++---- src/plugin/ext/audio_ports_activation.rs | 1 + src/plugin/ext/audio_ports_config.rs | 22 + src/plugin/ext/configurable_audio_ports.rs | 21 + src/plugin/ext/note_ports.rs | 113 ++-- src/plugin/ext/params.rs | 35 +- src/plugin/ext/thread_pool.rs | 37 + src/plugin/ext/voice_info.rs | 40 ++ src/plugin/instance/audio_thread.rs | 6 +- src/plugin/instance/main_thread.rs | 6 +- src/plugin/instance/shared.rs | 123 ++-- src/plugin/library.rs | 14 +- src/plugin/process.rs | 51 +- src/plugin/process/buffer.rs | 671 ++++++++++--------- src/plugin/process/transport.rs | 3 + src/tests.rs | 2 +- src/tests/plugin/layout.rs | 110 ++- src/tests/plugin/params.rs | 30 +- src/tests/plugin/processing.rs | 90 ++- src/tests/plugin/state.rs | 41 +- src/tests/plugin/transport.rs | 12 +- src/tests/plugin_library/preset_discovery.rs | 26 +- 24 files changed, 973 insertions(+), 735 deletions(-) create mode 100644 src/plugin/ext/thread_pool.rs create mode 100644 src/plugin/ext/voice_info.rs diff --git a/src/main.rs b/src/main.rs index 186d76d..f40f3f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,7 +78,7 @@ fn main() -> ExitCode { // Mark the main thread as such for plugin instance creation checks. unsafe { - plugin::instance::mark_current_thread_as_os_main_thread(); + plugin::library::mark_current_thread_as_os_main_thread(); } let result = match cli.command { diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index 6d5f7ec..8062870 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -17,13 +17,16 @@ pub mod preset_load; pub mod state; pub mod surround; pub mod tail; +pub mod thread_pool; +pub mod voice_info; /// An abstraction for a CLAP plugin extension. `P` here is the plugin type. In practice, this is -/// either `Plugin` or `PluginAudioThread`. Abstractions for main thread functions will implement -/// this trait for `Plugin`, and abstractions for audio thread functions will implement this trait -/// for `PluginAudioThread`. +/// either `Plugin`, `PluginShared` or `PluginAudioThread`. Abstractions for main thread functions will implement +/// this trait for `Plugin`, abstractions for audio thread functions will implement this trait +/// for `PluginAudioThread` and abstractions for thread-safe functions will implement this trait for +/// `PluginShared`. pub trait Extension

{ - /// The C-string IDs for the extension. + /// The list of C-string IDs for the extension. const IDS: &'static [&'static CStr]; /// The type of the C-struct for the extension. diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index e692723..1537747 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -9,9 +9,10 @@ use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::*; use clap_sys::ext::surround::CLAP_PORT_SURROUND; -use clap_sys::id::CLAP_INVALID_ID; -use std::collections::HashMap; +use clap_sys::id::{CLAP_INVALID_ID, clap_id}; +use std::collections::HashSet; use std::ffi::CStr; +use std::mem::zeroed; use std::ptr::NonNull; /// Abstraction for the `audio-ports` extension covering the main thread functionality. @@ -30,17 +31,19 @@ pub struct AudioPortConfig { } /// The configuration for a single audio port. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AudioPort { + /// Stable ID of the audio port. + pub id: clap_id, + /// Whether this is the main audio port. pub is_main: bool, /// The number of channels for an audio port. - pub num_channels: u32, + pub channel_count: u32, - /// The index if the output/input port this input/output port should be connected to. This is - /// the index in the other **port list**, not a stable ID (which have already been translated). - pub in_place_pair_idx: Option, + /// The stable ID of the output/input port this input/output port should be connected to. + pub in_place_pair: Option, /// Supports 64 bit processing pub supports_double_sample_size: bool, @@ -78,69 +81,38 @@ impl AudioPorts<'_> { ) }; - // Audio ports have a stable ID attribute that can be used to connect input and output ports - // so the host can do in-place processing. This uses stable IDs rather than the indices in - // the list. To make it easier for us, we'll translate those stable IDs to vector indices. - // These two hashmaps are keyed by the port's stable ID, and the value is a pair containing - // the port's index in the input/output port vector, and the stable ID of its in-place pair - // port. - let mut input_stable_index_pairs: HashMap = HashMap::new(); - let mut output_stable_index_pairs: HashMap = HashMap::new(); - for index in 0..num_inputs { - let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe { - clap_call! { audio_ports=>get(plugin, index, true, &mut info) } + let info = match self.get_raw_port_info(false, index) { + Some(info) => info, + None => { + anyhow::bail!( + "Plugin returned false when querying audio port info for input port {index} (out of \ + {num_inputs} total)." + ); + } }; - if !success { - anyhow::bail!( - "Plugin returned an error when querying input audio port {index} ({num_inputs} total input ports)." - ); - } - - // We'll convert these stable IDs to vector indices later - if input_stable_index_pairs - .insert(info.id, (index as usize, info.in_place_pair)) - .is_some() - { - anyhow::bail!( - "The stable ID of input audio port {index} (id={}) is a duplicate.", - info.id - ); - } - - config - .inputs - .push(check_audio_port_info_valid(self.plugin, true, index, &info)?); + config.inputs.push( + check_audio_port_info_valid(self.plugin, true, index, &info) + .with_context(|| format!("Inconsistent port info for input audio port {index}"))?, + ); } for index in 0..num_outputs { - let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe { - clap_call! { audio_ports=>get(plugin, index, false, &mut info) } + let info = match self.get_raw_port_info(false, index) { + Some(info) => info, + None => { + anyhow::bail!( + "Plugin returned false when querying audio port info for output port {index} (out of \ + {num_outputs} total)." + ); + } }; - if !success { - anyhow::bail!( - "Plugin returned an error when querying output audio port {index} ({num_outputs} total output \ - ports)." - ); - } - - if output_stable_index_pairs - .insert(info.id, (index as usize, info.in_place_pair)) - .is_some() - { - anyhow::bail!( - "The stable ID of output audio port {index} (id={}) is a duplicate.", - info.id - ); - } - - config - .outputs - .push(check_audio_port_info_valid(self.plugin, false, index, &info)?); + config.outputs.push( + check_audio_port_info_valid(self.plugin, false, index, &info) + .with_context(|| format!("Inconsistent port info for output audio port {index}"))?, + ); } let has_single_precision_requires_common_port = config @@ -164,77 +136,41 @@ impl AudioPorts<'_> { ); } - // Now we need to convert the stable in-place pair indices to vector indices - for (input_stable_id, (input_port_idx, pair_stable_id)) in input_stable_index_pairs - .iter() - .filter(|(_, (_, pair_stable_id))| *pair_stable_id != CLAP_INVALID_ID) - { - match output_stable_index_pairs - .iter() - .find(|(output_stable_id, (_, _))| *output_stable_id == pair_stable_id) - { - // This relation should be symmetrical - Some((_, (pair_output_port_idx, output_pair_stable_id))) - if output_pair_stable_id == input_stable_id => - { - config.inputs[*input_port_idx].in_place_pair_idx = Some(*pair_output_port_idx); - config.outputs[*pair_output_port_idx].in_place_pair_idx = Some(*input_port_idx); - } - Some((output_stable_id, (pair_output_port_idx, output_pair_stable_id))) => { - anyhow::bail!( - "Input port {input_port_idx} with stable ID {input_stable_id} is connected to output port \ - {pair_output_port_idx} with stable ID {output_stable_id} through an in-place pair, but the \ - relation is not symmetrical. The output port reports to have an in-place pair with stable ID \ - {output_pair_stable_id}." - ) - } - None => anyhow::bail!( - "Input port {input_port_idx} with stable ID {input_stable_id} claims to be connected to an output \ - port with stable ID {pair_stable_id} through an in-place pair, but this port does not exist." - ), - } - } + // check for duplicate stable IDs + for is_input in [true, false] { + let mut ids = HashSet::new(); + let ports = if is_input { &config.inputs } else { &config.outputs }; - // This needs to be repeated for output ports that are connected to input ports in case an - // output port has a stable ID pair but the corresponding input port does not - for (output_stable_id, (output_port_idx, pair_stable_id)) in output_stable_index_pairs - .iter() - .filter(|(_, (_, pair_stable_id))| *pair_stable_id != CLAP_INVALID_ID) - { - match input_stable_index_pairs - .iter() - .find(|(input_stable_id, (_, _))| *input_stable_id == pair_stable_id) - { - Some((_, (pair_input_port_idx, input_pair_stable_id))) if input_pair_stable_id == output_stable_id => { - // We should have already done this. If this is not the case, then this is an - // error in the validator - assert_eq!( - config.inputs[*output_port_idx].in_place_pair_idx, - Some(*pair_input_port_idx) - ); - assert_eq!( - config.inputs[*pair_input_port_idx].in_place_pair_idx, - Some(*output_port_idx) - ); - } - Some((input_stable_id, (pair_input_port_idx, input_pair_stable_id))) => { + for (index, port) in ports.iter().enumerate() { + if !ids.insert(port.id) { anyhow::bail!( - "Output port {output_port_idx} with stable ID {output_stable_id} is connected to input port \ - {pair_input_port_idx} with stable ID {input_stable_id} through an in-place pair, but the \ - relation is not symmetrical. The input port reports to have an in-place pair with stable ID \ - {input_pair_stable_id}." - ) + "Found {} audio port ({}) with a duplicate ID ({}).", + if is_input { "input" } else { "output" }, + index, + port.id + ); } - None => anyhow::bail!( - "Output port {output_port_idx} with stable ID {output_stable_id} claims to be connected to an \ - input port with stable ID {pair_stable_id} through an in-place pair, but this port does not \ - exist." - ), } } Ok(config) } + + /// Get the raw audio port information for the given port index. This does not perform any + /// consistency checks. + pub fn get_raw_port_info(&self, is_input: bool, port_index: u32) -> Option { + let audio_ports = self.audio_ports.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut info = clap_audio_port_info { ..zeroed() }; + if !clap_call! { audio_ports=>get(plugin, port_index, is_input, &mut info) } { + return None; + } + + Some(info) + } + } } pub fn check_audio_port_info_valid( @@ -246,6 +182,24 @@ pub fn check_audio_port_info_valid( let ext_ambisonic = plugin.get_extension::(); let ext_surround = plugin.get_extension::(); + if info.id == CLAP_INVALID_ID { + anyhow::bail!("The stable ID is `CLAP_INVALID_ID`."); + } + + // if the main port flag is set, the port index must be 0 + let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; + if is_main && port_index != 0 { + anyhow::bail!("Port is marked as main, but it is not the first port in the list."); + } + + let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; + let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; + let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; + + if !supports_double_sample_size && prefers_double_sample_size { + anyhow::bail!("Port prefers 64-bit sample size, but does not support it."); + } + let port_type = if info.port_type.is_null() { None } else { @@ -260,38 +214,17 @@ pub fn check_audio_port_info_valid( info.channel_count, ext_ambisonic.as_ref(), ext_surround.as_ref(), - ) - .with_context(|| { - format!( - "Inconsistent port info for {} port {port_index}", - if is_input { "input" } else { "output" } - ) - })?; - - // if the main port flag is set, the port index must be 0 - let is_main = (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0; - if is_main && port_index != 0 { - anyhow::bail!( - "{} audio port {port_index} is marked as main, but it is not the first port in the list.", - if is_input { "Input" } else { "Output" } - ); - } - - let supports_double_sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0; - let requires_common_sample_size = (info.flags & CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE) != 0; - let prefers_double_sample_size = (info.flags & CLAP_AUDIO_PORT_PREFERS_64BITS) != 0; - - if !supports_double_sample_size && prefers_double_sample_size { - anyhow::bail!( - "{} audio port {port_index} prefers 64-bit sample size, but does not support it.", - if is_input { "Input" } else { "Output" } - ); - } + )?; Ok(AudioPort { + id: info.id, is_main: (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0, - num_channels: info.channel_count, - in_place_pair_idx: None, + channel_count: info.channel_count, + in_place_pair: if info.in_place_pair == CLAP_INVALID_ID { + None + } else { + Some(info.in_place_pair) + }, supports_double_sample_size, requires_common_sample_size, diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index a8e9b05..e4ec90c 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -26,6 +26,7 @@ impl<'a> Extension<&'a Plugin<'a>> for AudioPortsActivation<'a> { impl<'a> AudioPortsActivation<'a> { /// TODO: extra test where we do this while processing + #[allow(unused)] pub fn can_activate_while_processing(&self) -> bool { let audio_ports_activation = self.audio_ports_activation.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 9a62504..492c90f 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -5,6 +5,7 @@ use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; use crate::util::{c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; +use clap_sys::ext::audio_ports::clap_audio_port_info; use clap_sys::ext::audio_ports_config::*; use clap_sys::id::clap_id; use std::ffi::CStr; @@ -154,4 +155,25 @@ impl AudioPortsConfigInfo<'_> { clap_call! { audio_ports_config_info=>current_config(plugin) } } } + + /// Get the raw audio port information for the given port index. This does not perform any + /// consistency checks. + pub fn get_raw_port_info( + &self, + config_id: clap_id, + is_input: bool, + port_index: u32, + ) -> Option { + let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut info = clap_audio_port_info { ..zeroed() }; + if !clap_call! { audio_ports_config_info=>get(plugin, config_id, port_index, is_input, &mut info) } { + return None; + } + + Some(info) + } + } } diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index b48ddc6..d7963bd 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -1,6 +1,7 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; use crate::util::clap_call; +use clap_sys::ext::ambisonic::clap_ambisonic_config; use clap_sys::ext::audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}; use clap_sys::ext::configurable_audio_ports::{ CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, clap_audio_port_configuration_request, @@ -17,6 +18,26 @@ pub struct AudioPortsRequest { pub channel_count: u32, } +/// Different types of port details that can be requested. +#[derive(Debug, Clone)] +pub enum AudioPortsRequestInfo { + Mono, + Stereo, + + Untyped { + channel_count: u32, + }, + + Ambisonic { + channel_count: u32, + config: clap_ambisonic_config, + }, + + Surround { + channel_map: Vec, + }, +} + pub struct ConfigurableAudioPorts<'a> { plugin: &'a Plugin<'a>, configurable_audio_ports: NonNull, diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 376f101..82f974e 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -3,8 +3,9 @@ use super::Extension; use crate::plugin::instance::Plugin; use crate::util::clap_call; -use anyhow::Result; +use anyhow::{Context, Result}; use clap_sys::ext::note_ports::*; +use clap_sys::id::CLAP_INVALID_ID; use std::collections::HashSet; use std::ffi::CStr; use std::mem; @@ -28,9 +29,6 @@ pub struct NotePortConfig { /// The configuration for a single note port. #[derive(Debug, Clone)] pub struct NotePort { - #[allow(unused)] - /// The preferred dialect for this note port. This should only ever contain a single value. - pub prefered_dialect: clap_note_dialect, /// All supported note dialects for this port. All of these note dialect values will only ever /// contain a single value. pub supported_dialects: Vec, @@ -57,91 +55,62 @@ impl NotePorts<'_> { let note_ports = self.note_ports.as_ptr(); let plugin = self.plugin.as_ptr(); - let num_inputs = unsafe { - clap_call! { note_ports=>count(plugin, true) } - }; - let num_outputs = unsafe { - clap_call! { note_ports=>count(plugin, false) } + let (num_inputs, num_outputs) = unsafe { + ( + clap_call! { note_ports=>count(plugin, true) }, + clap_call! { note_ports=>count(plugin, false) }, + ) }; // We don't need the port's stable IDs, but we'll still verify that they're unique let mut input_stable_indices: HashSet = HashSet::new(); let mut output_stable_indices: HashSet = HashSet::new(); - for i in 0..num_inputs { + for index in 0..num_inputs { let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; let success = unsafe { - clap_call! { note_ports=>get(plugin, i, true, &mut info) } + clap_call! { note_ports=>get(plugin, index, true, &mut info) } }; - if !success { - anyhow::bail!( - "Plugin returned an error when querying input note port {i} ({num_inputs} total input ports)." - ); - } - - let num_preferred_dialects = info.preferred_dialect.count_ones(); - if num_preferred_dialects != 1 { - anyhow::bail!("Plugin prefers {num_preferred_dialects} dialects for input note port {i}."); - } - if (info.supported_dialects & info.preferred_dialect) == 0 { + if !success { anyhow::bail!( - "Plugin prefers note dialect {:#b} for input note port {i} which is not contained within the \ - supported note dialects field ({:#b}).", - info.preferred_dialect, - info.supported_dialects + "Plugin returned false when querying input note port {index} ({num_inputs} total input ports)." ); } if !input_stable_indices.insert(info.id) { - anyhow::bail!("The stable ID of input note port {i} ({}) is a duplicate.", info.id); + anyhow::bail!("The stable ID of input note port {index} ({}) is a duplicate.", info.id); } - config.inputs.push(NotePort { - prefered_dialect: info.preferred_dialect, - supported_dialects: (0..(mem::size_of::() * 8) - 1) - .map(|bit| 1 << bit) - .filter(|flag| (info.supported_dialects & flag) != 0) - .collect(), - }); + config.inputs.push( + check_note_port_valid(&info) + .with_context(|| format!("Inconsistent port info for input note port {index}"))?, + ); } - for i in 0..num_outputs { + for index in 0..num_outputs { let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; let success = unsafe { - clap_call! { note_ports=>get(plugin, i, false, &mut info) } + clap_call! { note_ports=>get(plugin, index, false, &mut info) } }; + if !success { anyhow::bail!( - "Plugin returned an error when querying output note port {i} ({num_outputs} total output ports)." + "Plugin returned false when querying output note port {index} ({num_outputs} total output ports)." ); } - let num_preferred_dialects = info.preferred_dialect.count_ones(); - if num_preferred_dialects != 1 { - anyhow::bail!("Plugin prefers {num_preferred_dialects} dialects for output note port {i}."); - } - - if (info.supported_dialects & info.preferred_dialect) == 0 { + if !output_stable_indices.insert(info.id) { anyhow::bail!( - "Plugin prefers note dialect {:#b} for output note port {i} which is not contained within the \ - supported note dialects field ({:#b}).", - info.preferred_dialect, - info.supported_dialects + "The stable ID of output note port {index} ({}) is a duplicate.", + info.id ); } - if !output_stable_indices.insert(info.id) { - anyhow::bail!("The stable ID of output note port {i} ({}) is a duplicate.", info.id); - } - - config.outputs.push(NotePort { - prefered_dialect: info.preferred_dialect, - supported_dialects: (0..(mem::size_of::() * 8) - 1) - .map(|bit| 1 << bit) - .filter(|flag| (info.supported_dialects & flag) != 0) - .collect(), - }); + config.outputs.push( + check_note_port_valid(&info) + .with_context(|| format!("Inconsistent port info for output note port {index}"))?, + ); } Ok(config) @@ -158,3 +127,31 @@ impl NotePort { || self.supported_dialects.contains(&CLAP_NOTE_DIALECT_MIDI_MPE) } } + +fn check_note_port_valid(info: &clap_note_port_info) -> Result { + if info.id == CLAP_INVALID_ID { + anyhow::bail!("The stable ID is `CLAP_INVALID_ID`."); + } + + let num_preferred_dialects = info.preferred_dialect.count_ones(); + if num_preferred_dialects != 1 { + anyhow::bail!( + "`preferred_dialect` contains multiple ({num_preferred_dialects}) dialect values, must be exactly one." + ); + } + + if (info.supported_dialects & info.preferred_dialect) == 0 { + anyhow::bail!( + "Port prefers note dialect {:#b} which is not contained within the supported note dialects field ({:#b}).", + info.preferred_dialect, + info.supported_dialects + ); + } + + Ok(NotePort { + supported_dialects: (0..(mem::size_of::() * 8) - 1) + .map(|bit| 1 << bit) + .filter(|flag| (info.supported_dialects & flag) != 0) + .collect(), + }) +} diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index a54cb79..39d169b 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -6,7 +6,7 @@ use crate::plugin::process::EventQueue; use crate::util::{self, c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; use clap_sys::ext::params::*; -use clap_sys::id::clap_id; +use clap_sys::id::{CLAP_INVALID_ID, clap_id}; use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::collections::BTreeMap; use std::ffi::{CStr, CString, c_void}; @@ -149,7 +149,11 @@ impl Params<'_> { }; if !success { - anyhow::bail!("Plugin returned an error when querying parameter {i} ({num_params} total parameters)."); + anyhow::bail!("Plugin returned false when querying parameter {i} ({num_params} total parameters)."); + } + + if info.id == CLAP_INVALID_ID { + anyhow::bail!("The stable ID for parameter {i} is `CLAP_INVALID_ID`."); } let name = util::c_char_slice_to_string(&info.name) @@ -164,6 +168,7 @@ impl Params<'_> { &name, info.id ) })?; + if module.starts_with('/') { anyhow::bail!( "The module name for parameter '{}' (stable ID {}) starts with a leading slash: '{}'.", @@ -171,14 +176,18 @@ impl Params<'_> { info.id, module ) - } else if module.ends_with('/') { + } + + if module.ends_with('/') { anyhow::bail!( "The module name for parameter '{}' (stable ID {}) ends with a trailing slash: '{}'.", &name, info.id, module ) - } else if module.contains("//") { + } + + if module.contains("//") { anyhow::bail!( "The module name for parameter '{}' (stable ID {}) contains multiple subsequent slashes: '{}'.", &name, @@ -187,7 +196,6 @@ impl Params<'_> { ) } - let range = info.min_value..=info.max_value; if info.min_value > info.max_value { anyhow::bail!( "Parameter '{}' (stable ID {}) has a minimum value ({:?}) that's higher than it's maximum value \ @@ -198,16 +206,18 @@ impl Params<'_> { info.max_value ) } - if !range.contains(&info.default_value) { + + if !(info.min_value..=info.max_value).contains(&info.default_value) { anyhow::bail!( "Parameter '{}' (stable ID {}) has a default value ({:?}) that falls outside of its value range \ ({:?}).", &name, info.id, info.default_value, - &range + info.min_value..=info.max_value ) } + if (info.flags & CLAP_PARAM_IS_STEPPED) != 0 { if info.min_value != info.min_value.trunc() { anyhow::bail!( @@ -228,6 +238,7 @@ impl Params<'_> { ) } } + if (info.flags & CLAP_PARAM_IS_BYPASS) != 0 { match bypass_parameter_id { Some(bypass_parameter_id) => anyhow::bail!( @@ -265,6 +276,7 @@ impl Params<'_> { info.id ) } + if (info.flags & CLAP_PARAM_IS_MODULATABLE) == 0 && (info.flags & (CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID @@ -280,12 +292,13 @@ impl Params<'_> { info.id ) } + if ((info.flags & CLAP_PARAM_IS_READONLY) != 0) && ((info.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0 || (info.flags & CLAP_PARAM_IS_MODULATABLE) != 0) { anyhow::bail!( - "Parameter '{}' (stable ID {}) has the CLAP_PARAM_IS_READONLY flag set, but it is also marked as \ - automatable or modulatable. This is likely a bug.", + "Parameter '{}' (stable ID {}) has the 'CLAP_PARAM_IS_READONLY' flag set, but it is also marked \ + as automatable or modulatable. This is likely a bug.", &name, info.id ) @@ -294,10 +307,11 @@ impl Params<'_> { let processed_info = Param { name, cookie: info.cookie, - range, + range: info.min_value..=info.max_value, default: info.default_value, flags: info.flags, }; + if result.insert(info.id, processed_info).is_some() { anyhow::bail!("The plugin contains multiple parameters with stable ID {}.", info.id); } @@ -315,7 +329,6 @@ impl Params<'_> { // This may only be called on the audio thread when the plugin is active. This object is the // main thread interface for the parameters extension. self.status().assert_inactive(); - assert!(input_events.is_sorted(), "Input event queue must be sorted."); let params = self.params.as_ptr(); diff --git a/src/plugin/ext/thread_pool.rs b/src/plugin/ext/thread_pool.rs new file mode 100644 index 0000000..3b52b6f --- /dev/null +++ b/src/plugin/ext/thread_pool.rs @@ -0,0 +1,37 @@ +use crate::{ + plugin::{ext::Extension, instance::PluginShared}, + util::clap_call, +}; +use clap_sys::ext::thread_pool::{CLAP_EXT_THREAD_POOL, clap_plugin_thread_pool}; +use std::{ffi::CStr, ptr::NonNull}; + +pub struct ThreadPool<'a> { + plugin: &'a PluginShared, + tail: NonNull, +} + +unsafe impl Send for ThreadPool<'_> {} +unsafe impl Sync for ThreadPool<'_> {} + +impl<'a> Extension<&'a PluginShared> for ThreadPool<'a> { + const IDS: &'static [&'static CStr] = &[CLAP_EXT_THREAD_POOL]; + + type Struct = clap_plugin_thread_pool; + + unsafe fn new(plugin: &'a PluginShared, extension_struct: NonNull) -> Self { + Self { + plugin, + tail: extension_struct, + } + } +} + +impl<'a> ThreadPool<'a> { + pub fn exec(&self, task: u32) { + let thread_pool = self.tail.as_ptr(); + let plugin = self.plugin.clap_plugin_ptr(); + unsafe { + clap_call! { thread_pool=>exec(plugin, task) } + } + } +} diff --git a/src/plugin/ext/voice_info.rs b/src/plugin/ext/voice_info.rs new file mode 100644 index 0000000..69edacb --- /dev/null +++ b/src/plugin/ext/voice_info.rs @@ -0,0 +1,40 @@ +use crate::plugin::ext::Extension; +use crate::plugin::instance::Plugin; +use crate::util::clap_call; +use clap_sys::ext::voice_info::*; +use std::ffi::CStr; +use std::mem::zeroed; +use std::ptr::NonNull; + +#[allow(unused)] +pub struct VoiceInfo<'a> { + plugin: &'a Plugin<'a>, + voice_info: NonNull, +} + +impl<'a> Extension<&'a Plugin<'a>> for VoiceInfo<'a> { + const IDS: &'static [&'static CStr] = &[CLAP_EXT_VOICE_INFO]; + + type Struct = clap_plugin_voice_info; + + unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { + Self { + plugin, + voice_info: extension_struct, + } + } +} + +impl<'a> VoiceInfo<'a> { + #[allow(unused)] + pub fn get(&self) -> Option { + let voice_info = self.voice_info.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut result = clap_voice_info { ..zeroed() }; + let success = clap_call! { voice_info=>get(plugin, &mut result) }; + if success { Some(result) } else { None } + } + } +} diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 9363240..d207a34 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -63,7 +63,7 @@ impl<'a> PluginAudioThread<'a> { /// Get the plugin's current initialization status. pub fn status(&self) -> PluginStatus { - self.shared.status.load() + self.shared.status() } /// Get a reference to the plugin's shared state. @@ -135,11 +135,15 @@ impl<'a> PluginAudioThread<'a> { pub fn process(&self, process_data: &clap_process) -> Result { self.status().assert_is(PluginStatus::Processing); + self.shared.is_currently_in_process_call.store(true); + let plugin = self.as_ptr(); let result = unsafe { clap_call! { plugin=>process(plugin, process_data) } }; + self.shared.is_currently_in_process_call.store(false); + match result { CLAP_PROCESS_ERROR => { anyhow::bail!("The plugin returned 'CLAP_PROCESS_ERROR' from 'clap_plugin::process()'.") diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index 3b219f3..d96bde5 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -93,12 +93,12 @@ impl<'lib> Plugin<'lib> { /// The plugin's current initialization status. pub fn status(&self) -> PluginStatus { - self.shared.status.load() + self.shared.status() } /// Handle any pending main-thread callbacks for this plugin and pending callback events. /// Returns an error if there is a callback error pending. - pub fn poll_callback(&self, mut f: impl FnMut(CallbackEvent)) -> Result<()> { + pub fn poll_callback(&self, mut f: impl FnMut(CallbackEvent) -> Result<()>) -> Result<()> { self.poll_callback_unchecked(); if let Some(error) = self.shared.callback_error.lock().unwrap().take() { @@ -106,7 +106,7 @@ impl<'lib> Plugin<'lib> { } while let Ok(event) = self.callback_receiver.try_recv() { - f(event); + f(event)?; } Ok(()) diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index b9e7543..c806b07 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -6,6 +6,8 @@ use crate::plugin::ext::params::Params; use crate::plugin::ext::preset_load::PresetLoad; use crate::plugin::ext::state::State; use crate::plugin::ext::tail::Tail; +use crate::plugin::ext::thread_pool::ThreadPool; +use crate::plugin::ext::voice_info::VoiceInfo; use crate::plugin::instance::{CallbackEvent, MainThreadTask, Plugin, PluginStatus}; use crate::plugin::preset_discovery::LocationValue; use crate::util::{self, check_null_ptr, clap_call, validator_version}; @@ -29,18 +31,13 @@ use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::ffi::{CStr, c_char, c_void}; +use std::ptr::NonNull; use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, Mutex}; use std::thread::ThreadId; -static OS_MAIN_THREAD: AtomicCell> = AtomicCell::new(None); - -pub unsafe fn mark_current_thread_as_os_main_thread() { - OS_MAIN_THREAD.store(Some(std::thread::current().id())); -} - /// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. -/// This struct contains the `clap_host` and its extensions, as well as fields for tracking the plugin's state. +/// This struct also acts as the `clap_host` implementation for the plugin instance. pub struct PluginShared { pub task_sender: Sender, pub callback_sender: Sender, @@ -63,6 +60,10 @@ pub struct PluginShared { /// deactivated and subsequently reactivated. pub requested_restart: AtomicCell, + /// Whether the plugin is currently being called from within a process call. This is used to + /// check that certain functions (like thread_pool::request_exec()) are called from the process function. + pub is_currently_in_process_call: AtomicCell, + clap_plugin: *const clap_plugin, clap_host: clap_host, } @@ -75,18 +76,10 @@ impl PluginShared { /// plugin could not be created. The plugin instance will be registered with the host, and /// unregistered when this object is dropped again. /// - /// # Panics - /// This MUST be called on the OS main thread (if applicable). - /// /// # Safety /// The `factory` object must be valid. - pub unsafe fn create_plugin<'a>(factory: &clap_plugin_factory, plugin_id: &CStr) -> Result> { - assert!( - OS_MAIN_THREAD.load() == Some(std::thread::current().id()), - "not on main thread" - ); - - let main_thread = std::thread::current().id(); + /// The caller must ensure that this is called from the OS main thread. + pub unsafe fn create_plugin<'a>(factory: *const clap_plugin_factory, plugin_id: &CStr) -> Result> { let (callback_sender, callback_receiver) = channel(); let (task_sender, task_receiver) = channel(); @@ -96,10 +89,11 @@ impl PluginShared { callback_error: Mutex::new(None), status: AtomicCell::new(PluginStatus::Uninitialized), - main_thread_id: main_thread, + main_thread_id: std::thread::current().id(), audio_thread_id: AtomicCell::new(None), requested_callback: AtomicCell::new(false), requested_restart: AtomicCell::new(false), + is_currently_in_process_call: AtomicCell::new(false), clap_plugin: std::ptr::null(), clap_host: clap_host { @@ -110,10 +104,10 @@ impl PluginShared { vendor: c"Robbert van der Helm".as_ptr(), url: c"https://github.com/free-audio/clap-validator".as_ptr(), version: validator_version().as_ptr(), - get_extension: Some(Self::get_extension), - request_restart: Some(Self::request_restart), - request_process: Some(Self::request_process), - request_callback: Some(Self::request_callback), + get_extension: Some(Self::clap_get_extension), + request_restart: Some(Self::clap_request_restart), + request_process: Some(Self::clap_request_process), + request_callback: Some(Self::clap_request_callback), }, }); @@ -149,14 +143,36 @@ impl PluginShared { }) } + /// Get a pointer to the `clap_host` struct for this plugin instance. pub fn clap_host_ptr(&self) -> *const clap_host { &self.clap_host as *const clap_host } + /// Get a pointer to the plugin-provided `clap_plugin` struct for this plugin instance. pub fn clap_plugin_ptr(&self) -> *const clap_plugin { self.clap_plugin } + /// Get a shared extension abstraction for the extension `T`, if the plugin supports this extension. + pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { + for id in T::IDS { + let extension_ptr = unsafe { + clap_call! { self.clap_plugin_ptr()=>get_extension(self.clap_plugin_ptr(), id.as_ptr()) } + }; + + if !extension_ptr.is_null() { + return unsafe { Some(T::new(self, NonNull::new_unchecked(extension_ptr as *mut _))) }; + } + } + + None + } + + /// The plugin's current initialization status. + pub fn status(&self) -> PluginStatus { + self.status.load() + } + #[track_caller] unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { unsafe { @@ -222,7 +238,7 @@ impl PluginShared { /// Checks whether the plugin has the required extension(s). If it does not, then an error /// will be set. Subsequent errors will not overwrite earlier ones. fn assert_has_extension(&self, function_name: &str, ids: &[&CStr]) { - if self.status.load() == PluginStatus::Uninitialized { + if self.status() == PluginStatus::Uninitialized { self.set_callback_error(format!("'{}' called while the plugin is uninitialized.", function_name)); return; } @@ -293,7 +309,7 @@ impl PluginShared { changed: Some(Self::ext_voice_info_changed), }; - unsafe extern "C" fn get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { + unsafe extern "C" fn clap_get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { check_null_ptr!(host, (*host).host_data, extension_id); // Right now there's no way to have the host only expose certain extensions. We can always @@ -324,8 +340,7 @@ impl PluginShared { } } - unsafe extern "C" fn request_restart(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); + unsafe extern "C" fn clap_request_restart(host: *const clap_host) { let this = unsafe { PluginShared::from_clap_host(host) }; // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and @@ -334,8 +349,7 @@ impl PluginShared { this.requested_restart.store(true); } - unsafe extern "C" fn request_process(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); + unsafe extern "C" fn clap_request_process(host: *const clap_host) { let this = unsafe { PluginShared::from_clap_host(host) }; // Handling this within the context of the validator would be a bit messy. Do plugins use @@ -344,8 +358,7 @@ impl PluginShared { this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); } - unsafe extern "C" fn request_callback(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); + unsafe extern "C" fn clap_request_callback(host: *const clap_host) { let this = unsafe { PluginShared::from_clap_host(host) }; // This this is either handled by `handle_callbacks_blocking()` while the audio thread is @@ -357,7 +370,6 @@ impl PluginShared { } unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); @@ -368,7 +380,6 @@ impl PluginShared { } unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_audio_ports::rescan()"); @@ -381,7 +392,7 @@ impl PluginShared { } if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - if this.status.load() > PluginStatus::Activated { + if this.status() > PluginStatus::Activated { this.set_callback_error("'clap_host_audio_ports::rescan()' was called while the plugin was activated"); } @@ -390,7 +401,6 @@ impl PluginShared { } unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::supported_dialects()"); @@ -402,7 +412,6 @@ impl PluginShared { } unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_note_ports::rescan()"); @@ -415,7 +424,7 @@ impl PluginShared { } if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { - if this.status.load() > PluginStatus::Activated { + if this.status() > PluginStatus::Activated { this.set_callback_error( "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while the plugin was \ activated", @@ -434,7 +443,6 @@ impl PluginShared { os_error: i32, msg: *const c_char, ) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::on_error()"); @@ -471,7 +479,6 @@ impl PluginShared { location: *const c_char, load_key: *const c_char, ) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_preset_load::loaded()"); @@ -493,7 +500,6 @@ impl PluginShared { } unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::rescan()"); @@ -514,9 +520,9 @@ impl PluginShared { } if flags & CLAP_PARAM_RESCAN_ALL != 0 { - if this.status.load() > PluginStatus::Activated { + if this.status() > PluginStatus::Activated { this.set_callback_error( - "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is activated", + "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is active", ); } @@ -525,7 +531,6 @@ impl PluginShared { } unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_params::clear()"); @@ -535,7 +540,6 @@ impl PluginShared { } unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_not_audio_thread("clap_host_params::request_flush()"); @@ -546,7 +550,6 @@ impl PluginShared { } unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_state::mark_dirty()"); @@ -557,25 +560,22 @@ impl PluginShared { } unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.main_thread_id == std::thread::current().id() } unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.audio_thread_id.load() == Some(std::thread::current().id()) } unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_latency::changed()"); this.assert_has_extension("clap_host_latency::changed()", Latency::IDS); - if this.status.load() != PluginStatus::Activating { + if this.status() != PluginStatus::Activating { this.set_callback_error( "'clap_host_latency::changed()' must only be called within 'clap_plugin::activate()'", ); @@ -586,7 +586,6 @@ impl PluginShared { } unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_audio_thread("clap_host_tail::changed()"); @@ -597,26 +596,40 @@ impl PluginShared { } unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { - check_null_ptr!(host, (*host).host_data); - let this = unsafe { PluginShared::from_clap_host(host) }; this.assert_main_thread("clap_host_voice_info::changed()"); - //this.assert_has_extension("clap_host_voice_info::changed()", VoiceInfo::IDS); + this.assert_has_extension("clap_host_voice_info::changed()", VoiceInfo::IDS); log::trace!("'clap_host_voice_info::changed()' was called"); this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); } unsafe extern "C" fn ext_thread_pool_request_exec(host: *const clap_host, num_tasks: u32) -> bool { - check_null_ptr!(host, (*host).host_data); let this = unsafe { PluginShared::from_clap_host(host) }; + log::trace!("'clap_host_thread_pool::request_exec()' was called"); + this.assert_audio_thread("clap_host_thread_pool::request_exec()"); - this.assert_has_extension("clap_host_thread_pool::request_exec()", &[CLAP_EXT_THREAD_POOL]); + this.assert_has_extension("clap_host_thread_pool::request_exec()", ThreadPool::IDS); + + // Ensure this is called from within the process() function + // We already checked that we're on the audio thread, so this is sufficient + if !this.is_currently_in_process_call.load() { + this.set_callback_error( + "'clap_host_thread_pool::request_exec()' may only be called from within the audio thread's \ + 'clap_plugin::process()' function.", + ); + + return false; + } + + let Some(extension) = this.get_extension::() else { + return false; + }; (0..num_tasks).into_par_iter().for_each(|index| { - log::trace!("Executing thread pool task {index} of {num_tasks}"); + extension.exec(index); }); true diff --git a/src/plugin/library.rs b/src/plugin/library.rs index a33387f..728d4f4 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -10,12 +10,14 @@ use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_fact use clap_sys::factory::preset_discovery::{CLAP_PRESET_DISCOVERY_FACTORY_ID, clap_preset_discovery_factory}; use clap_sys::plugin::clap_plugin_descriptor; use clap_sys::version::clap_version; +use crossbeam::atomic::AtomicCell; use serde::Serialize; use std::collections::HashSet; use std::ffi::CString; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::ptr::NonNull; +use std::thread::ThreadId; /// A CLAP plugin library built from a CLAP plugin's entry point. This can be used to iterate over /// all plugins exposed by the library and to initialize plugins. @@ -250,6 +252,10 @@ impl PluginLibrary { /// [`metadata()`][Self::metadata()]. The returned plugin has not yet been initialized, and /// `destroy()` will be called automatically when the object is dropped. pub fn create_plugin(&self, id: &str) -> Result> { + if OS_MAIN_THREAD.load() != Some(std::thread::current().id()) { + anyhow::bail!("Plugins must be created from the OS main thread."); + } + let entry_point = get_clap_entry_point(&self.library).expect("A Plugin was constructed for a plugin with no entry point"); @@ -265,7 +271,7 @@ impl PluginLibrary { } let id_cstring = CString::new(id).context("Plugin ID contained null bytes")?; - unsafe { PluginShared::create_plugin(&*plugin_factory, &id_cstring) } + unsafe { PluginShared::create_plugin(plugin_factory, &id_cstring) } } /// Returns the plugin's preset discovery factory, if it has one. @@ -311,3 +317,9 @@ fn get_clap_entry_point(library: &libloading::Library) -> Result<&clap_plugin_en Ok(unsafe { &**entry_point }) } + +static OS_MAIN_THREAD: AtomicCell> = AtomicCell::new(None); + +pub unsafe fn mark_current_thread_as_os_main_thread() { + OS_MAIN_THREAD.store(Some(std::thread::current().id())); +} diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 6134e25..aafd964 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -57,7 +57,7 @@ impl<'a> ProcessScope<'a> { } pub fn max_block_size(&self) -> u32 { - self.buffer.len() + self.buffer.samples() } pub fn input_queue(&self) -> &EventQueue { @@ -83,11 +83,11 @@ impl<'a> ProcessScope<'a> { } pub fn run(&mut self) -> Result { - self.run_with_block_size(self.buffer.len()) + self.run_with_block_size(self.max_block_size()) } pub fn run_with_block_size(&mut self, samples: u32) -> Result { - assert!(samples > 0 && samples <= self.buffer.len()); + assert!(samples > 0 && samples <= self.buffer.samples()); // check for requested restart if self.plugin.shared().requested_restart.load() { @@ -99,7 +99,7 @@ impl<'a> ProcessScope<'a> { self.plugin.shared().requested_restart.store(false); let sample_rate = self.sample_rate; - let buffer_size = self.buffer.len(); + let buffer_size = self.buffer.samples(); self.plugin .dispatch_main(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; } @@ -125,32 +125,33 @@ impl<'a> ProcessScope<'a> { // prepare output audio buffers for processing // this is used to detect uninitialized output buffers - for buffer in self.buffer.buffers_mut() { - if buffer.is_output_only() { + for buffer in self.buffer.iter_mut() { + if buffer.port().input().is_none() { buffer.fill(CHECK_NAN_F32, CHECK_NAN_F64); } } // save original buffers for consistency check - let original_buffers = self.buffer.buffers().to_owned(); + let original_buffers = self.buffer[..].to_owned(); // run processing - let transport = self.transport.as_clap_transport(0); - let (inputs, outputs) = self.buffer.clap_buffers(); - let status = self.plugin.process(&clap_process { - steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), - frames_count: samples, - transport: if self.transport.is_freerun { - std::ptr::null() - } else { - &transport as *const _ - }, - audio_inputs: inputs.as_ptr(), - audio_outputs: outputs.as_mut_ptr(), - audio_inputs_count: inputs.len() as u32, - audio_outputs_count: outputs.len() as u32, - in_events: self.events_input.vtable_input(), - out_events: self.events_output.vtable_output(), + let status = self.buffer.process(|inputs, outputs| { + let transport = self.transport.as_clap_transport(0); + self.plugin.process(&clap_process { + steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), + frames_count: samples, + transport: if self.transport.is_freerun { + std::ptr::null() + } else { + &transport as *const _ + }, + audio_inputs: inputs.as_ptr(), + audio_outputs: outputs.as_mut_ptr(), + audio_inputs_count: inputs.len() as u32, + audio_outputs_count: outputs.len() as u32, + in_events: self.events_input.vtable_input(), + out_events: self.events_output.vtable_output(), + }) })?; // clear input event queue and advance transport @@ -158,7 +159,7 @@ impl<'a> ProcessScope<'a> { self.transport.advance(samples as i64, self.sample_rate()); // check output audio buffers for NaNs or infinities - check_process_call_consistency(self.buffer.buffers(), &original_buffers, self.output_queue(), samples)?; + check_process_call_consistency(&self.buffer[..], &original_buffers, self.output_queue(), samples)?; if status == ProcessStatus::Tail && self.plugin_tail.is_none() { anyhow::bail!( @@ -234,8 +235,8 @@ fn check_process_call_consistency( if let Some((sample, channel_idx, sample_idx)) = maybe_non_finite { let is_subnormal = sample.either(|x| x.is_subnormal(), |x| x.is_subnormal()); let is_unwritten = sample.either( - |x| x.to_bits() == CHECK_NAN_F64.to_bits(), |x| x.to_bits() == CHECK_NAN_F32.to_bits(), + |x| x.to_bits() == CHECK_NAN_F64.to_bits(), ); if is_subnormal { diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs index 14b7869..ad720de 100644 --- a/src/plugin/process/buffer.rs +++ b/src/plugin/process/buffer.rs @@ -1,9 +1,15 @@ use crate::plugin::{ext::audio_ports::AudioPortConfig, process::ConstantMask}; +use anyhow::Result; use clap_sys::audio_buffer::*; use either::Either; use rand::Rng; use rand_pcg::Pcg32; -use std::ptr::null_mut; +use std::{ + collections::HashMap, + fmt::Debug, + ops::{Deref, DerefMut}, + ptr::null_mut, +}; /// Audio buffers for audio processing. These contain both input and output buffers, that can be either in-place /// or out-of-place, single or double precision. @@ -14,32 +20,35 @@ pub struct AudioBuffers { /// reinitializing the pointers. buffers: Box<[AudioBuffer]>, - /// These point to `inputs` and `outputs` because `clap_audio_buffer` needs to contain a - /// `*const *const f32` - _pointers: Box<[Box<[*const ()]>]>, - - /// The CLAP audio buffer representations for inputs and outputs. + /// The CLAP audio buffer representations for inputs clap_inputs: Box<[clap_audio_buffer]>, + /// The CLAP audio buffer representations for outputs clap_outputs: Box<[clap_audio_buffer]>, /// The number of samples for this buffer. This is consistent across all inner vectors. - num_samples: u32, + samples: u32, } -/// A single audio buffer, either input, output or in-place. This can be either single or double precision. -#[derive(Clone)] -pub enum AudioBuffer { - Float32 { - port: AudioBufferPort, - data: Box<[Box<[f32]>]>, - }, - - Float64 { - port: AudioBufferPort, - data: Box<[Box<[f64]>]>, - }, +#[derive(Debug, Clone)] +pub struct AudioBuffer { + port: AudioBufferPort, + data: AudioBufferData, + + input_constant_mask: ConstantMask, + output_constant_mask: ConstantMask, + + input_latency: u32, + output_latency: u32, +} + +pub struct AudioBufferData { + #[allow(clippy::type_complexity)] + data: Either]>, Box<[Box<[f64]>]>>, + pointers: Box<[*const ()]>, + samples: u32, } +/// A port to which an audio buffer belongs. #[derive(Clone, Copy, Debug)] pub enum AudioBufferPort { Input(usize), @@ -47,95 +56,49 @@ pub enum AudioBufferPort { Inplace(usize, usize), } -// SAFETY: Sharing these pointers with other threads is safe as they refer to the borrowed input and -// output slices. The pointers thus cannot be invalidated. -unsafe impl Send for AudioBuffers {} -unsafe impl Sync for AudioBuffers {} - impl AudioBuffers { /// Construct the audio buffers from the given buffer configurations. The number of samples must /// be greater than zero and all channel vectors must have the same length. - pub fn new(buffers: Vec, num_samples: u32) -> Self { - assert!(num_samples > 0, "Number of samples must be greater than zero."); - - let mut pointers = vec![]; + pub fn new(buffers: Vec, samples: u32) -> Self { let mut clap_inputs = vec![]; let mut clap_outputs = vec![]; for buffer in buffers.iter() { - let pointer_list = match buffer { - AudioBuffer::Float32 { data, .. } => { - assert!( - data.iter().all(|x| x.len() as u32 == num_samples), - "Channel buffer length does not match" - ); - - data.iter().map(|x| x.as_ptr() as *const ()).collect::>() - } - AudioBuffer::Float64 { data, .. } => { - assert!( - data.iter().all(|x| x.len() as u32 == num_samples), - "Channel buffer length does not match" - ); + assert!( + buffer.samples() == samples, + "All audio buffers must have the same number of samples." + ); - data.iter().map(|x| x.as_ptr() as *const ()).collect::>() - } - }; - - if let Some(input) = buffer.port().as_input() { + if let Some(input) = buffer.port().input() { if clap_inputs.len() <= input { clap_inputs.resize(input + 1, None); } clap_inputs[input] = Some(clap_audio_buffer { - data32: if buffer.is_64bit() { - null_mut() - } else { - pointer_list.as_ptr() as *mut *mut f32 - }, - - data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *mut *mut f64 - } else { - null_mut() - }, - - channel_count: pointer_list.len() as u32, - latency: 0, //TODO: do some interesting tests with these 2 fields + data32: buffer.as_ptr().either(|x| x, |_| null_mut()), + data64: buffer.as_ptr().either(|_| null_mut(), |x| x), + channel_count: buffer.channels(), + latency: 0, constant_mask: 0, }); } - if let Some(output) = buffer.port().as_output() { + if let Some(output) = buffer.port().output() { if clap_outputs.len() <= output { clap_outputs.resize(output + 1, None); } clap_outputs[output] = Some(clap_audio_buffer { - data32: if buffer.is_64bit() { - null_mut() - } else { - pointer_list.as_ptr() as *mut *mut f32 - }, - - data64: if buffer.is_64bit() { - pointer_list.as_ptr() as *mut *mut f64 - } else { - null_mut() - }, - - channel_count: pointer_list.len() as u32, - latency: 0, //TODO: do some interesting tests with these 2 fields + data32: buffer.as_ptr().either(|x| x, |_| null_mut()), + data64: buffer.as_ptr().either(|_| null_mut(), |x| x), + channel_count: buffer.channels(), + latency: 0, constant_mask: 0, }); } - - pointers.push(pointer_list.into_boxed_slice()); } Self { - buffers: buffers.into_boxed_slice(), - _pointers: pointers.into_boxed_slice(), clap_inputs: clap_inputs .into_iter() .collect::>>() @@ -146,200 +109,141 @@ impl AudioBuffers { .collect::>>() .expect("Missing an output bus") .into_boxed_slice(), - num_samples, + buffers: buffers.into_boxed_slice(), + samples, } } - pub fn new_out_of_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { + pub fn new_out_of_place_f32(config: &AudioPortConfig, samples: u32) -> Self { Self::new( - config - .inputs - .iter() - .enumerate() - .map(|(index, port)| { - AudioBuffer::new(AudioBufferPort::Input(index), false, port.num_channels, num_samples) - }) - .chain(config.outputs.iter().enumerate().map(|(index, port)| { - AudioBuffer::new(AudioBufferPort::Output(index), false, port.num_channels, num_samples) - })) + (0..config.inputs.len()) + .map(AudioBufferPort::Input) + .chain((0..config.outputs.len()).map(AudioBufferPort::Output)) + .map(|port| port.create_buffer(config, samples, false)) .collect(), - num_samples, + samples, ) } - pub fn new_in_place_f32(config: &AudioPortConfig, num_samples: u32) -> Self { - let mut buffers = vec![]; - - for (index, port) in config.inputs.iter().enumerate() { - let in_place = port - .in_place_pair_idx - .filter(|output| config.outputs[*output].num_channels == port.num_channels); - - if in_place.is_none() { - buffers.push(AudioBuffer::new( - AudioBufferPort::Input(index), - false, - port.num_channels, - num_samples, - )); - } - } - - for (index, port) in config.outputs.iter().enumerate() { - let in_place = port - .in_place_pair_idx - .filter(|input| config.inputs[*input].num_channels == port.num_channels); - - buffers.push(AudioBuffer::new( - match in_place { - Some(input) => AudioBufferPort::Inplace(input, index), - None => AudioBufferPort::Output(index), - }, - false, - port.num_channels, - num_samples, - )); - } - - Self::new(buffers, num_samples) - } - - pub fn new_out_of_place_f64(config: &AudioPortConfig, num_samples: u32) -> Self { + pub fn new_out_of_place_f64(config: &AudioPortConfig, samples: u32) -> Self { Self::new( - config - .inputs - .iter() - .enumerate() - .map(|(index, port)| { - AudioBuffer::new( - AudioBufferPort::Input(index), - port.supports_double_sample_size, - port.num_channels, - num_samples, - ) - }) - .chain(config.outputs.iter().enumerate().map(|(index, port)| { - AudioBuffer::new( - AudioBufferPort::Output(index), - port.supports_double_sample_size, - port.num_channels, - num_samples, - ) - })) + (0..config.inputs.len()) + .map(AudioBufferPort::Input) + .chain((0..config.outputs.len()).map(AudioBufferPort::Output)) + .map(|port| port.create_buffer(config, samples, true)) .collect(), - num_samples, + samples, ) } - pub fn new_in_place_f64(config: &AudioPortConfig, num_samples: u32) -> Self { - let mut buffers = vec![]; + pub fn new_in_place_f32(config: &AudioPortConfig, samples: u32) -> Result { + Ok(Self::new( + resolve_in_place_pairs(config)? + .iter() + .map(|port| port.create_buffer(config, samples, false)) + .collect(), + samples, + )) + } - for (index, port) in config.inputs.iter().enumerate() { - let in_place = port - .in_place_pair_idx - .filter(|output| config.outputs[*output].num_channels == port.num_channels); + pub fn new_in_place_f64(config: &AudioPortConfig, samples: u32) -> Result { + Ok(Self::new( + resolve_in_place_pairs(config)? + .iter() + .map(|port| port.create_buffer(config, samples, true)) + .collect(), + samples, + )) + } - if in_place.is_none() { - buffers.push(AudioBuffer::new( - AudioBufferPort::Input(index), - port.supports_double_sample_size, - port.num_channels, - num_samples, - )); + pub fn process(&mut self, f: impl FnOnce(&[clap_audio_buffer], &mut [clap_audio_buffer]) -> R) -> R { + for buffer in self.buffers.iter() { + if let Some(input) = buffer.port().input() { + self.clap_inputs[input].constant_mask = buffer.input_constant_mask.0; + self.clap_inputs[input].latency = buffer.input_latency; } } - for (index, port) in config.outputs.iter().enumerate() { - let in_place = port.in_place_pair_idx.filter(|input| { - let input = &config.inputs[*input]; - port.num_channels == input.num_channels - && port.supports_double_sample_size == input.supports_double_sample_size - }); - - buffers.push(AudioBuffer::new( - match in_place { - Some(input) => AudioBufferPort::Inplace(input, index), - None => AudioBufferPort::Output(index), - }, - port.supports_double_sample_size, - port.num_channels, - num_samples, - )); - } + let result = f(&self.clap_inputs, &mut self.clap_outputs); - Self::new(buffers, num_samples) - } + for buffer in self.buffers.iter_mut() { + if let Some(output) = buffer.port().output() { + buffer.output_constant_mask = ConstantMask(self.clap_outputs[output].constant_mask); + buffer.output_latency = self.clap_outputs[output].latency; + } + } - /// The number of samples in the buffer. - pub fn len(&self) -> u32 { - self.num_samples + result } - /// Pointers for the inputs and the outputs. These can be used to construct the `clap_process` - /// data. - pub fn clap_buffers(&mut self) -> (&[clap_audio_buffer], &mut [clap_audio_buffer]) { - (&self.clap_inputs, &mut self.clap_outputs) + pub fn samples(&self) -> u32 { + self.samples } - /// Pointers to the internal audio buffers - pub fn buffers(&self) -> &[AudioBuffer] { - &self.buffers + pub fn fill_white_noise(&mut self, prng: &mut Pcg32) { + for buffer in self.buffers.iter_mut() { + if buffer.port().input().is_some() { + buffer.fill_white_noise(prng); + } + } } - /// Pointers to the internal audio buffers - pub fn buffers_mut(&mut self) -> &mut [AudioBuffer] { - &mut self.buffers + pub fn fill_silence(&mut self) { + for buffer in self.buffers.iter_mut() { + if buffer.port().input().is_some() { + buffer.fill_silence(); + } + } } +} - /// Check whether the audio buffers are identical to another set of audio buffers. - pub fn is_same(&self, other: &Self) -> bool { - if self.buffers.len() != other.buffers.len() { - return false; +impl AudioBuffer { + pub fn new(port: AudioBufferPort, data: AudioBufferData) -> Self { + Self { + port, + data, + input_constant_mask: ConstantMask::DYNAMIC, + output_constant_mask: ConstantMask::DYNAMIC, + input_latency: 0, + output_latency: 0, } + } - for (this, other) in self.buffers.iter().zip(other.buffers.iter()) { - if !this.is_same(other) { - return false; - } - } + pub fn port(&self) -> AudioBufferPort { + self.port + } - true + pub fn set_input_constant_mask(&mut self, mask: ConstantMask) { + self.input_constant_mask = mask; } - /// Fill the input buffers with white noise ([-1, 1], denormals are snapped to zero). - pub fn randomize(&mut self, prng: &mut Pcg32) { - for buffer in self.buffers_mut() { - if buffer.is_input() { - buffer.fill_white_noise(prng); - } - } + pub fn get_output_constant_mask(&self) -> ConstantMask { + self.output_constant_mask + } - for input in &mut self.clap_inputs { - input.constant_mask = 0; - } + #[allow(unused)] + pub fn set_input_latency(&mut self, latency: u32) { + self.input_latency = latency; } - /// Fill the input buffers with silence (zeros), and mark all input channels as constant. - pub fn silence_inputs(&mut self) { - for buffer in self.buffers_mut() { - if buffer.is_input() { - buffer.fill(0.0, 0.0); - } - } + #[allow(unused)] + pub fn get_output_latency(&self) -> u32 { + self.output_latency + } - for input in &mut self.clap_inputs { - input.constant_mask = u64::MAX; - } + pub fn fill_white_noise(&mut self, prng: &mut Pcg32) { + self.data.fill_white_noise(prng); + self.set_input_constant_mask(ConstantMask::DYNAMIC); } - /// Get the constant mask for the given output bus. - pub fn get_output_constant_mask(&self, bus: usize) -> ConstantMask { - ConstantMask(self.clap_outputs[bus].constant_mask) + pub fn fill_silence(&mut self) { + self.data.fill(0.0, 0.0); + self.set_input_constant_mask(ConstantMask::CONSTANT); } } impl AudioBufferPort { - pub fn as_input(&self) -> Option { + pub fn input(&self) -> Option { match self { AudioBufferPort::Input(index) => Some(*index), AudioBufferPort::Inplace(index, _) => Some(*index), @@ -347,147 +251,264 @@ impl AudioBufferPort { } } - pub fn as_output(&self) -> Option { + pub fn output(&self) -> Option { match self { AudioBufferPort::Output(index) => Some(*index), AudioBufferPort::Inplace(_, index) => Some(*index), AudioBufferPort::Input(_) => None, } } + + pub fn create_buffer(self, config: &AudioPortConfig, samples: u32, is_double: bool) -> AudioBuffer { + match self { + AudioBufferPort::Input(index) => AudioBuffer::new( + self, + AudioBufferData::new( + config.inputs[index].channel_count, + samples, + is_double && config.inputs[index].supports_double_sample_size, + ), + ), + AudioBufferPort::Output(index) => AudioBuffer::new( + self, + AudioBufferData::new( + config.outputs[index].channel_count, + samples, + is_double && config.outputs[index].supports_double_sample_size, + ), + ), + AudioBufferPort::Inplace(input_index, output_index) => AudioBuffer::new( + self, + AudioBufferData::new( + config.inputs[input_index].channel_count, + samples, + is_double + && config.inputs[input_index].supports_double_sample_size + && config.outputs[output_index].supports_double_sample_size, + ), + ), + } + } } -impl AudioBuffer { - pub fn new(port: AudioBufferPort, is_double_precision: bool, num_channels: u32, num_samples: u32) -> Self { - if is_double_precision { - AudioBuffer::Float64 { - port, - data: vec![vec![0.0f64; num_samples as usize].into_boxed_slice(); num_channels as usize] - .into_boxed_slice(), - } +impl AudioBufferData { + pub fn new(channels: u32, samples: u32, is_double: bool) -> Self { + let data = if is_double { + Either::Right(vec![vec![0.0f64; samples as usize].into_boxed_slice(); channels as usize].into_boxed_slice()) } else { - AudioBuffer::Float32 { - port, - data: vec![vec![0.0f32; num_samples as usize].into_boxed_slice(); num_channels as usize] - .into_boxed_slice(), - } + Either::Left(vec![vec![0.0f32; samples as usize].into_boxed_slice(); channels as usize].into_boxed_slice()) + }; + + let pointers = match &data { + Either::Left(data) => data.iter().map(|channel| channel.as_ptr() as *const ()).collect(), + Either::Right(data) => data.iter().map(|channel| channel.as_ptr() as *const ()).collect(), + }; + + Self { + samples, + pointers, + data, } } - pub fn port(&self) -> AudioBufferPort { - match self { - AudioBuffer::Float32 { port, .. } => *port, - AudioBuffer::Float64 { port, .. } => *port, + pub fn is_64bit(&self) -> bool { + self.data.is_right() + } + + pub fn samples(&self) -> u32 { + self.samples + } + + pub fn channels(&self) -> u32 { + match &self.data { + Either::Left(data) => data.len() as u32, + Either::Right(data) => data.len() as u32, } } - /// Check whether this is a double precision buffer. - pub fn is_64bit(&self) -> bool { - match self { - AudioBuffer::Float32 { .. } => false, - AudioBuffer::Float64 { .. } => true, + pub fn channel(&self, channel: u32) -> Either<&[f32], &[f64]> { + match &self.data { + Either::Left(data) => Either::Left(&data[channel as usize]), + Either::Right(data) => Either::Right(&data[channel as usize]), } } - pub fn is_input(&self) -> bool { - self.port().as_input().is_some() + pub fn channel_mut(&mut self, channel: u32) -> Either<&mut [f32], &mut [f64]> { + match &mut self.data { + Either::Left(data) => Either::Left(&mut data[channel as usize]), + Either::Right(data) => Either::Right(&mut data[channel as usize]), + } } - pub fn is_output_only(&self) -> bool { - self.port().as_output().is_some() && self.port().as_input().is_none() + pub fn get(&self, channel: u32, sample: u32) -> Either { + match &self.data { + Either::Left(data) => Either::Left(data[channel as usize][sample as usize]), + Either::Right(data) => Either::Right(data[channel as usize][sample as usize]), + } } - /// Check whether this audio buffer's contents are identical to another audio buffer. pub fn is_same(&self, other: &Self) -> bool { - match (self, other) { - (AudioBuffer::Float32 { data: this, .. }, AudioBuffer::Float32 { data: other, .. }) => { - for (this, other) in this.iter().zip(other.iter()) { - for (this, other) in this.iter().zip(other.iter()) { - if this.to_bits() != other.to_bits() { - return false; - } - } - } + if self.channels() != other.channels() { + return false; + } - true - } + for channel in 0..self.channels() { + let left = self.channel(channel); + let right = other.channel(channel); - (AudioBuffer::Float64 { data: this, .. }, AudioBuffer::Float64 { data: other, .. }) => { - for (this, other) in this.iter().zip(other.iter()) { - for (this, other) in this.iter().zip(other.iter()) { - if this.to_bits() != other.to_bits() { - return false; - } + match (left, right) { + (Either::Left(left), Either::Left(right)) => { + if left != right { + return false; } } + (Either::Right(left), Either::Right(right)) => { + if left != right { + return false; + } + } + _ => return false, + } + } - true + true + } + + /// Fill the buffer with silence (zeros). + pub fn fill(&mut self, value_f32: f32, value_f64: f64) { + for channel in 0..self.channels() { + match self.channel_mut(channel) { + Either::Left(data) => data.fill(value_f32), + Either::Right(data) => data.fill(value_f64), } + } + } - _ => false, + /// Fill the buffer with white noise (random values in the range [-1, 1]). + pub fn fill_white_noise(&mut self, prng: &mut Pcg32) { + for channel in 0..self.channels() { + match self.channel_mut(channel) { + Either::Left(data) => data.fill_with(|| prng.random_range(-1.0..1.0)), + Either::Right(data) => data.fill_with(|| prng.random_range(-1.0..1.0)), + } } } - /// The number of samples in this buffer. - pub fn len(&self) -> u32 { - match self { - AudioBuffer::Float32 { data, .. } => data.first().map_or(0, |x| x.len() as u32), - AudioBuffer::Float64 { data, .. } => data.first().map_or(0, |x| x.len() as u32), + pub fn as_ptr(&self) -> Either<*mut *mut f32, *mut *mut f64> { + match &self.data { + Either::Left(_) => Either::Left(self.pointers.as_ptr() as *mut *mut f32), + Either::Right(_) => Either::Right(self.pointers.as_ptr() as *mut *mut f64), } } +} - /// The number of channels in this buffer. - pub fn channels(&self) -> u32 { - match self { - AudioBuffer::Float32 { data, .. } => data.len() as u32, - AudioBuffer::Float64 { data, .. } => data.len() as u32, +unsafe impl Send for AudioBufferData {} +unsafe impl Sync for AudioBufferData {} + +impl Clone for AudioBufferData { + fn clone(&self) -> Self { + let data = self.data.clone(); + + Self { + samples: self.samples, + pointers: data.as_ref().either( + |x| x.iter().map(|channel| channel.as_ptr() as *const ()).collect(), + |x| x.iter().map(|channel| channel.as_ptr() as *const ()).collect(), + ), + data, } } +} - /// Get a sample from the buffer. - pub fn get(&self, channel: u32, sample: u32) -> Either { - match self { - AudioBuffer::Float32 { data, .. } => Either::Right(data[channel as usize][sample as usize]), - AudioBuffer::Float64 { data, .. } => Either::Left(data[channel as usize][sample as usize]), +impl Debug for AudioBufferData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AudioBufferData") + .field("channels", &self.channels()) + .field("type", if self.is_64bit() { &"f64" } else { &"f32" }) + .finish() + } +} + +impl Deref for AudioBuffer { + type Target = AudioBufferData; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for AudioBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl Deref for AudioBuffers { + type Target = [AudioBuffer]; + fn deref(&self) -> &Self::Target { + &self.buffers + } +} + +impl DerefMut for AudioBuffers { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.buffers + } +} + +/// Resolve the in-place pairs from the given audio port configuration. +/// +/// Returns an error if there are any inconsistencies, such as an input or output port +/// referencing a non-existent in-place pair. +fn resolve_in_place_pairs(config: &AudioPortConfig) -> Result> { + let mut ports = vec![]; + let mut in_place: HashMap<(u32, u32), (Option, Option)> = HashMap::new(); + + for (index, port) in config.inputs.iter().enumerate() { + if let Some(inplace_id) = port.in_place_pair { + in_place.entry((port.id, inplace_id)).or_default().0 = Some(index); + } else { + ports.push(AudioBufferPort::Input(index)); } } - /// Fill the buffer with silence (zeros). - pub fn fill(&mut self, value_f32: f32, value_f64: f64) { - match self { - AudioBuffer::Float32 { data, .. } => { - for channel in data { - for sample in channel { - *sample = value_f32; - } - } - } - AudioBuffer::Float64 { data, .. } => { - for channel in data { - for sample in channel { - *sample = value_f64; - } - } - } + for (index, port) in config.outputs.iter().enumerate() { + if let Some(inplace_id) = port.in_place_pair { + in_place.entry((inplace_id, port.id)).or_default().1 = Some(index); + } else { + ports.push(AudioBufferPort::Output(index)); } } - /// Fill the buffer with white noise (random values in the range [-1, 1]). - pub fn fill_white_noise(&mut self, prng: &mut Pcg32) { - match self { - AudioBuffer::Float32 { data, .. } => { - for channel in data { - for sample in channel { - *sample = prng.random_range(-1.0..=1.0f32); - } - } - } - AudioBuffer::Float64 { data, .. } => { - for channel in data { - for sample in channel { - *sample = prng.random_range(-1.0..=1.0f64); - } + for ((input_id, output_id), (input, output)) in in_place { + match (input, output) { + (None, Some(output)) => anyhow::bail!( + "Output port {output} has an in-place pair ({input_id}), but the corresponding input port does not \ + exist." + ), + (Some(input), None) => anyhow::bail!( + "Input port {input} has an in-place pair ({output_id}), but the corresponding output port does not \ + exist." + ), + (Some(input), Some(output)) => { + if config.inputs[input].channel_count != config.outputs[output].channel_count { + // TODO: is this allowed? + // anyhow::bail!( + // "Input port {input} and output port {output} are configured as an in-place pair, but they \ + // have different channel counts ({} vs {}).", + // config.inputs[input].channel_count, + // config.outputs[output].channel_count + // ); + + ports.push(AudioBufferPort::Input(input)); + ports.push(AudioBufferPort::Output(output)); + continue; } + + ports.push(AudioBufferPort::Inplace(input, output)); } + _ => {} } } + + Ok(ports) } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index 8671a35..9922bd8 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -119,6 +119,9 @@ impl TransportState { pub struct ConstantMask(pub u64); impl ConstantMask { + pub const DYNAMIC: Self = ConstantMask(0); + pub const CONSTANT: Self = ConstantMask(u64::MAX); + /// Check if the specified channel marked as constant. pub fn is_channel_constant(&self, channel: u32) -> bool { self.0 & 1u64.unbounded_shl(channel) != 0 diff --git a/src/tests.rs b/src/tests.rs index 313b830..a253207 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -23,7 +23,7 @@ use strum::IntoEnumIterator; mod plugin; mod plugin_library; -pub mod rng; +mod rng; pub use plugin::PluginTestCase; pub use plugin_library::PluginLibraryTestCase; diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index b385bcc..be70211 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -7,7 +7,9 @@ use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng}; +use crate::util::{cstr_ptr_to_mandatory_string, cstr_ptr_to_string}; use anyhow::{Context, Result}; +use clap_sys::ext::audio_ports::clap_audio_port_info; use rand::Rng; use rand::seq::SliceRandom; use rand_pcg::Pcg32; @@ -76,13 +78,13 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) .inputs .first() .filter(|x| x.is_main) - .map(|x| x.num_channels); + .map(|x| x.channel_count); let main_output_channels = config_audio_ports .outputs .first() .filter(|x| x.is_main) - .map(|x| x.num_channels); + .map(|x| x.channel_count); anyhow::ensure!( config_audio_ports.inputs.len() as u32 == config_audio_ports_config.input_port_count, @@ -162,17 +164,59 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) config_audio_ports_config.id, ); - // TODO: check info + for is_input in [true, false] { + let count = if is_input { + config_audio_ports_config.input_port_count + } else { + config_audio_ports_config.output_port_count + }; + + for index in 0..count { + let info_apci = audio_ports_config_info + .get_raw_port_info(config_audio_ports_config.id, is_input, index) + .with_context(|| { + format!( + "Could not get info for {} port {} of configuration '{}' ({}) from \ + 'audio-ports-config-info'", + if is_input { "input" } else { "output" }, + index, + config_audio_ports_config.name, + config_audio_ports_config.id, + ) + })?; + + let info_ap = audio_ports.get_raw_port_info(is_input, index).with_context(|| { + format!( + "Could not get info for {} port {} of configuration '{}' ({}) from 'audio-ports'", + if is_input { "input" } else { "output" }, + index, + config_audio_ports_config.name, + config_audio_ports_config.id, + ) + })?; + + check_mismatch_audio_port_info(&info_apci, &info_ap).with_context(|| { + format!( + "Mismatch between info queried via 'audio-ports-config-info' and 'audio-ports' for {} \ + port {} of configuration '{}' ({})", + if is_input { "input" } else { "output" }, + index, + config_audio_ports_config.name, + config_audio_ports_config.id, + ) + })?; + } + } } plugin .on_audio_thread(|plugin| -> Result<()> { - let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE)?; let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -190,7 +234,7 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -307,12 +351,12 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: plugin .on_audio_thread(|plugin| -> Result<()> { - let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE); + let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE)?; let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -330,13 +374,13 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if checks_passed == 0 { return Ok(TestStatus::Warning { details: Some(String::from( - "Tried 200 random audio port layouts, but none was accepted.", + "Tried 200 random audio port layouts, but none were accepted.", )), }); } @@ -386,3 +430,49 @@ pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &s Ok(TestStatus::Success { details: None }) } + +fn check_mismatch_audio_port_info(info_left: &clap_audio_port_info, info_right: &clap_audio_port_info) -> Result<()> { + if info_left.id != info_right.id { + anyhow::bail!("ID mismatch: {} vs {}", info_left.id, info_right.id); + } + + if info_left.channel_count != info_right.channel_count { + anyhow::bail!( + "Channel count mismatch: {} vs {}", + info_left.channel_count, + info_right.channel_count + ); + } + + if info_left.flags != info_right.flags { + anyhow::bail!("Flags mismatch"); + } + + let (name_left, name_right) = unsafe { + ( + cstr_ptr_to_mandatory_string(info_left.name.as_ptr())?, + cstr_ptr_to_mandatory_string(info_right.name.as_ptr())?, + ) + }; + + if name_left != name_right { + anyhow::bail!("Name mismatch: {:?} vs {:?}", name_left, name_right); + } + + let (port_type_left, port_type_right) = unsafe { + ( + cstr_ptr_to_string(info_left.port_type)?, + cstr_ptr_to_string(info_right.port_type)?, + ) + }; + + if port_type_left != port_type_right { + anyhow::bail!( + "Port type mismatch: {:?} vs {:?}", + port_type_left.as_deref().unwrap_or(""), + port_type_right.as_deref().unwrap_or("") + ); + } + + Ok(()) +} diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 43ae607..3fba588 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -70,7 +70,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let param_infos = params @@ -144,7 +144,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { @@ -196,7 +196,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let audio_ports_config = audio_ports @@ -235,7 +235,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b process.input_queue().add_events(current_events.clone().unwrap()); for _ in 0..FUZZ_RUNS_PER_PERMUTATION { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -282,7 +282,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -311,7 +311,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let audio_ports_config = audio_ports @@ -353,7 +353,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) // Audio and MIDI/note events are randomized in accordance to what the plugin // supports - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -365,7 +365,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -411,7 +411,7 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R plugin.on_audio_thread(|plugin| -> Result<()> { let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(param_fuzzer.generate_events(&mut prng, process.max_block_size())); @@ -422,7 +422,7 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -452,7 +452,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let param_infos = params @@ -480,7 +480,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.input_queue().add_events(random_param_set_events); process.run() })?; @@ -494,7 +494,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) .collect::>>()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if actual_param_values == initial_param_values { @@ -527,7 +527,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let param_infos = params @@ -551,7 +551,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 65408a0..d1a90a8 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -34,7 +34,7 @@ pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_pla }; let mut audio_buffers = if in_place { - AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE) + AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE)? } else { AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE) }; @@ -43,7 +43,7 @@ pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_pla let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.run()?; } @@ -51,7 +51,7 @@ pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_pla })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -87,7 +87,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl .unwrap_or_default(); plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let has_double_support = audio_ports_config @@ -104,7 +104,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut audio_buffers = if in_place { - AudioBuffers::new_in_place_f64(&audio_ports_config, BUFFER_SIZE) + AudioBuffers::new_in_place_f64(&audio_ports_config, BUFFER_SIZE)? } else { AudioBuffers::new_out_of_place_f64(&audio_ports_config, BUFFER_SIZE) }; @@ -113,7 +113,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -124,7 +124,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -187,7 +187,7 @@ pub fn test_process_note_out_of_place( let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -198,7 +198,7 @@ pub fn test_process_note_out_of_place( })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -241,7 +241,7 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st let mut process = ProcessScope::with_sample_rate(&plugin, &mut audio_buffers, sample_rate)?; for _ in 0..5 { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); @@ -254,7 +254,7 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -294,7 +294,7 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str let num_iters = (32768 / buffer_size).min(5); for _ in 0..num_iters { - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, buffer_size)); @@ -307,7 +307,7 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -350,7 +350,7 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) 1 }; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, buffer_size)); @@ -363,7 +363,7 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(TestStatus::Success { details: None }) @@ -402,15 +402,15 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str let mut has_received_constant_flag = false; let mut check_buffers = |buffers: &AudioBuffers| -> Result<()> { - for buffer in buffers.buffers() { - let Some(output) = buffer.port().as_output() else { + for buffer in buffers.iter() { + let Some(output) = buffer.port().output() else { continue; }; for channel in 0..buffer.channels() { - let is_constant = (0..buffer.len()).all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); // TODO: relax, allow small variations? - - let marked_constant = buffers.get_output_constant_mask(output).is_channel_constant(channel); + let is_constant = + (0..buffer.samples()).all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); // TODO: relax, allow small variations? + let marked_constant = buffer.get_output_constant_mask().is_channel_constant(channel); if marked_constant && !is_constant { anyhow::bail!( @@ -440,13 +440,13 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str check_buffers(process.audio_buffers())?; // block 2: randomize inputs, see if the plugin tracks constant channels - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.run()?; check_buffers(process.audio_buffers())?; // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay - process.audio_buffers().silence_inputs(); + process.audio_buffers().fill_silence(); for _ in 3..=40 { process.run()?; check_buffers(process.audio_buffers())?; @@ -456,7 +456,7 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if !has_received_constant_flag && has_received_constant_output { @@ -499,32 +499,54 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; // first run, "control" run - process.audio_buffers().randomize(&mut new_prng()); + process.audio_buffers().fill_white_noise(&mut new_prng()); process .input_queue() .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; - let output_control = process.audio_buffers().clone(); + + let output_control = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); // second run, deactivate and reactivate the plugin, see if the output changes process.restart(); - process.audio_buffers().randomize(&mut new_prng()); + process.audio_buffers().fill_white_noise(&mut new_prng()); process .input_queue() .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; - let output_reactivated = process.audio_buffers().clone(); + + let output_reactivated = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); // third run, reset the plugin, see if the output matches the control run process.reset(); - process.audio_buffers().randomize(&mut new_prng()); + process.audio_buffers().fill_white_noise(&mut new_prng()); process .input_queue() .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; - let output_reset = process.audio_buffers().clone(); - if !output_control.is_same(&output_reactivated) { + let output_reset = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); + + if output_control + .iter() + .zip(output_reactivated.iter()) + .any(|(a, b)| !a.is_same(b)) + { return Ok(TestStatus::Warning { details: Some(String::from( "Plugin output does not seem to be deterministic after reactivation", @@ -532,7 +554,11 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: }); } - if !output_reactivated.is_same(&output_reset) { + if output_control + .iter() + .zip(output_reset.iter()) + .any(|(a, b)| !a.is_same(b)) + { anyhow::bail!("Plugin output differs after reset"); } @@ -540,7 +566,7 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; Ok(result) diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index 9c6cc67..ea7a85c 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -44,7 +44,7 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res let result = state.load(&[]); plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; match result { @@ -78,7 +78,7 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let mut random_data = vec![0u8; 1024 * 1024]; @@ -90,7 +90,7 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; match succeeded { @@ -150,7 +150,7 @@ pub fn test_state_reproducibility_basic( }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let param_infos = params @@ -181,7 +181,7 @@ pub fn test_state_reproducibility_basic( let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.input_queue().add_events(random_param_set_events); process.run() })?; @@ -197,7 +197,7 @@ pub fn test_state_reproducibility_basic( let expected_state = state.save()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; (expected_state, expected_param_values) @@ -238,13 +238,13 @@ pub fn test_state_reproducibility_basic( }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; state.load(&expected_state)?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values @@ -273,7 +273,7 @@ pub fn test_state_reproducibility_basic( let actual_state = state.save()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if actual_state == expected_state { @@ -330,7 +330,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let param_infos = params @@ -356,7 +356,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str params.flush(&input_events, &output_events); plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; // We'll compare against these values in that second pass @@ -367,7 +367,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let expected_state = state.save()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; // Plugins with no parameters at all should of course not trigger this error @@ -423,7 +423,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; // NOTE: We can reuse random parameter set events, except that the cookie pointers may be @@ -431,6 +431,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let param_infos = params .info() .context("Failure while fetching the plugin's parameters")?; + let mut new_random_param_set_events = old_random_param_set_events; for event in new_random_param_set_events.iter_mut() { match event { @@ -454,7 +455,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.input_queue().add_events(new_random_param_set_events); process.run() })?; @@ -481,7 +482,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let actual_state = state.save()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if actual_state == expected_state { @@ -550,7 +551,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; - process.audio_buffers().randomize(&mut prng); + process.audio_buffers().fill_white_noise(&mut prng); process.input_queue().add_events(random_param_set_events); process.run() })?; @@ -566,7 +567,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let expected_state = state.save()?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; (expected_state, expected_param_values) @@ -606,14 +607,14 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> }; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; let actual_param_values: BTreeMap = expected_param_values @@ -640,7 +641,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; if actual_state == expected_state { diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index 3908d29..c118bcc 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -49,7 +49,7 @@ pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result Result Result Result Result // successful, but it doesn't matter if the plugin doesn't have any audio ports let audio_ports = plugin.get_extension::(); plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .context("An error occured during a host callback")?; let audio_ports_config = audio_ports @@ -134,26 +134,26 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, 512); - for LoadablePreset { - location, - load_key, - preset, - } in presets - { + for preset in presets { // TODO: We now always deactivate the plugin before loading presets, but presets can // be loaded at any point, even when the plugin is processing audio. Test // this. let load_result = preset_load - .from_location(&location, load_key.as_deref()) - .with_context(|| format!("Could not load the preset '{}' for plugin '{}'", preset.name, plugin_id)); + .from_location(&preset.location, preset.load_key.as_deref()) + .with_context(|| { + format!( + "Could not load the preset '{}' for plugin '{}'", + preset.preset.name, plugin_id + ) + }); // In case the plugin uses `clap_host_preset_load::on_error()` to report an error, // we will check that first before making sure the preset loaded correctly. This // might otherwise mask the error message. - plugin.poll_callback(|_| {}).with_context(|| { + plugin.poll_callback(|_| Ok(())).with_context(|| { format!( "An error occurred while loading the preset '{}' for plugin '{}'", - preset.name, plugin_id + preset.preset.name, plugin_id ) })?; // See above @@ -171,12 +171,12 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result })?; plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } plugin - .poll_callback(|_| {}) + .poll_callback(|_| Ok(())) .with_context(|| format!("An error occured during a host callback made by '{plugin_id}'"))?; } } From 72cc7e3a2854f225db5a59ec5c897b2ec8644c69 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Sun, 1 Feb 2026 15:31:20 +0400 Subject: [PATCH 050/114] ambisonic/surround support for the configurable-audio-ports test; initial prototype for sleep-status/tail test; refactor `PluginShared` - nicer `wrap`; fix a bug in audio-ports extension config query impl --- src/plugin/ext/audio_ports.rs | 2 +- src/plugin/ext/configurable_audio_ports.rs | 115 ++++-- src/plugin/ext/latency.rs | 1 - src/plugin/instance/audio_thread.rs | 38 +- src/plugin/instance/main_thread.rs | 13 +- src/plugin/instance/shared.rs | 451 ++++++++++----------- src/plugin/process.rs | 19 +- src/plugin/process/transport.rs | 5 + src/tests/plugin.rs | 16 +- src/tests/plugin/layout.rs | 58 +-- src/tests/plugin/processing.rs | 307 +++++++++----- src/tests/rng.rs | 96 ++++- src/util.rs | 7 +- 13 files changed, 659 insertions(+), 469 deletions(-) diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 1537747..757874e 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -82,7 +82,7 @@ impl AudioPorts<'_> { }; for index in 0..num_inputs { - let info = match self.get_raw_port_info(false, index) { + let info = match self.get_raw_port_info(true, index) { Some(info) => info, None => { anyhow::bail!( diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index d7963bd..4366588 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -1,40 +1,40 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; use crate::util::clap_call; -use clap_sys::ext::ambisonic::clap_ambisonic_config; +use clap_sys::ext::ambisonic::{CLAP_PORT_AMBISONIC, clap_ambisonic_config}; use clap_sys::ext::audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}; use clap_sys::ext::configurable_audio_ports::{ CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, clap_audio_port_configuration_request, clap_plugin_configurable_audio_ports, }; +use clap_sys::ext::surround::CLAP_PORT_SURROUND; use std::ffi::CStr; +use std::fmt::{Debug, Display}; use std::ptr::{NonNull, null}; -/// TODO: surround/ambisonic extensions? #[derive(Debug, Clone, Copy)] -pub struct AudioPortsRequest { +pub struct AudioPortsRequest<'a> { pub is_input: bool, pub port_index: u32, - pub channel_count: u32, + pub request_info: AudioPortsRequestInfo<'a>, } /// Different types of port details that can be requested. -#[derive(Debug, Clone)] -pub enum AudioPortsRequestInfo { +#[derive(Debug, Clone, Copy)] +pub enum AudioPortsRequestInfo<'a> { Mono, Stereo, - Untyped { channel_count: u32, }, Ambisonic { channel_count: u32, - config: clap_ambisonic_config, + config: &'a clap_ambisonic_config, }, Surround { - channel_map: Vec, + channel_map: &'a [u8], }, } @@ -60,24 +60,10 @@ impl<'a> Extension<&'a Plugin<'a>> for ConfigurableAudioPorts<'a> { } impl<'a> ConfigurableAudioPorts<'a> { - pub fn can_apply_configuration(&self, requests: impl IntoIterator) -> bool { + pub fn can_apply_configuration<'b>(&self, requests: impl IntoIterator>) -> bool { self.plugin.status().assert_inactive(); - let requests = requests - .into_iter() - .map(|r| clap_audio_port_configuration_request { - is_input: r.is_input, - port_index: r.port_index, - channel_count: r.channel_count, - port_details: null(), - port_type: match r.channel_count { - 1 => CLAP_PORT_MONO.as_ptr(), - 2 => CLAP_PORT_STEREO.as_ptr(), - _ => null(), - }, - }) - .collect::>(); - + let requests = convert_requests(requests); let plugin = self.plugin.as_ptr(); let ext = self.configurable_audio_ports.as_ptr(); @@ -90,24 +76,10 @@ impl<'a> ConfigurableAudioPorts<'a> { } } - pub fn apply_configuration(&self, requests: impl IntoIterator) -> bool { + pub fn apply_configuration<'b>(&self, requests: impl IntoIterator>) -> bool { self.plugin.status().assert_inactive(); - let requests = requests - .into_iter() - .map(|r| clap_audio_port_configuration_request { - is_input: r.is_input, - port_index: r.port_index, - channel_count: r.channel_count, - port_details: null(), - port_type: match r.channel_count { - 1 => CLAP_PORT_MONO.as_ptr(), - 2 => CLAP_PORT_STEREO.as_ptr(), - _ => null(), - }, - }) - .collect::>(); - + let requests = convert_requests(requests); let plugin = self.plugin.as_ptr(); let ext = self.configurable_audio_ports.as_ptr(); @@ -120,3 +92,64 @@ impl<'a> ConfigurableAudioPorts<'a> { } } } + +fn convert_requests<'a>( + requests: impl IntoIterator>, +) -> Vec { + requests + .into_iter() + .map(|r| clap_audio_port_configuration_request { + is_input: r.is_input, + port_index: r.port_index, + channel_count: match r.request_info { + AudioPortsRequestInfo::Mono => 1, + AudioPortsRequestInfo::Stereo => 2, + AudioPortsRequestInfo::Untyped { channel_count } => channel_count, + AudioPortsRequestInfo::Ambisonic { channel_count, .. } => channel_count, + AudioPortsRequestInfo::Surround { channel_map } => channel_map.len() as u32, + }, + port_type: match r.request_info { + AudioPortsRequestInfo::Mono => CLAP_PORT_MONO.as_ptr(), + AudioPortsRequestInfo::Stereo => CLAP_PORT_STEREO.as_ptr(), + AudioPortsRequestInfo::Ambisonic { .. } => CLAP_PORT_AMBISONIC.as_ptr(), + AudioPortsRequestInfo::Surround { .. } => CLAP_PORT_SURROUND.as_ptr(), + AudioPortsRequestInfo::Untyped { .. } => null(), + }, + port_details: match r.request_info { + AudioPortsRequestInfo::Surround { channel_map } => channel_map.as_ptr() as *const _, + AudioPortsRequestInfo::Ambisonic { config, .. } => config as *const clap_ambisonic_config as *const _, + _ => null(), + }, + }) + .collect::>() +} + +impl Display for AudioPortsRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} #{}: {}", + if self.is_input { "Input" } else { "Output" }, + self.port_index, + self.request_info + ) + } +} + +impl Display for AudioPortsRequestInfo<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AudioPortsRequestInfo::Mono => write!(f, "Mono"), + AudioPortsRequestInfo::Stereo => write!(f, "Stereo"), + AudioPortsRequestInfo::Untyped { channel_count } => { + write!(f, "Untyped ({}ch)", channel_count) + } + AudioPortsRequestInfo::Ambisonic { channel_count, .. } => { + write!(f, "Ambisonic ({}ch)", channel_count) + } + AudioPortsRequestInfo::Surround { channel_map } => { + write!(f, "Surround ({}ch)", channel_map.len()) + } + } + } +} diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 55606ae..75f6f47 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -27,7 +27,6 @@ impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { impl<'a> Latency<'a> { #[allow(unused)] pub fn get(&self) -> u32 { - self.plugin.status().assert_is_not(PluginStatus::Uninitialized); self.plugin.status().assert_is_not(PluginStatus::Deactivated); let latency = self.latency.as_ptr(); diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index d207a34..aca78f6 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -2,7 +2,7 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; -use crate::plugin::instance::{MainThreadTask, PluginShared}; +use crate::plugin::instance::{CallbackEvent, MainThreadTask, PluginShared}; use crate::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; @@ -10,10 +10,14 @@ use clap_sys::process::{ CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR, CLAP_PROCESS_SLEEP, CLAP_PROCESS_TAIL, clap_process, }; +use std::any::Any; use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::pin::Pin; use std::ptr::NonNull; use std::sync::Arc; +use std::sync::mpsc::SyncSender; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. @@ -95,18 +99,36 @@ impl<'a> PluginAudioThread<'a> { /// for the task to complete and return its result. /// /// TODO: this could be optimized and the 'static requirement dropped. - pub fn dispatch_main T + Send + 'static, T: Send + 'static>(&self, callback: F) -> T { - let (send, recv) = std::sync::mpsc::sync_channel(0); + pub fn on_main_thread T + Send, T: Send + 'static>(&self, callback: F) -> T { + struct Context { + sender: SyncSender>>, + callback: F, + } + + let (sender, recv) = std::sync::mpsc::sync_channel(0); + let context = MaybeUninit::new(Context { sender, callback }); self.shared .task_sender - .send(MainThreadTask::Dispatch(Box::new(move |plugin| { - let result = callback(plugin); - send.send(result).unwrap(); - }))) + .send(MainThreadTask::Dispatch { + data: context.as_ptr() as *mut (), + call: |plugin, data| { + // Safety: we are the only ones with access to this pointer right now. + let context = unsafe { (data as *mut Context).read() }; + let result = catch_unwind(AssertUnwindSafe(|| (context.callback)(plugin))); + context.sender.send(result).unwrap(); + }, + }) .unwrap(); - recv.recv().unwrap() + match recv.recv().unwrap() { + Ok(value) => value, + Err(panic) => std::panic::resume_unwind(panic), + } + } + + pub fn poll_callback(&self, mut f: impl FnMut(&Plugin, CallbackEvent) -> Result<()> + Send) -> Result<()> { + self.on_main_thread(|plugin| plugin.poll_callback(|event| f(plugin, event))) } /// Prepare for audio processing. Returns an error if the plugin returned `false`. See diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index d96bde5..9317975 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -17,7 +17,11 @@ use std::{ }; pub enum MainThreadTask { - Dispatch(Box), + Dispatch { + call: fn(&Plugin<'_>, *mut ()), + data: *mut (), + }, + CallbackRequest, StopAudioThread, } @@ -140,8 +144,11 @@ impl<'lib> Plugin<'lib> { /// then those will be handled concurrently. pub fn on_audio_thread T + Send>(&self, f: F) -> T { let result = crossbeam::scope(|s| { - let shared = self.shared.clone(); + if self.shared.audio_thread_id.load().is_some() { + panic!("An audio thread is already running for this plugin instance."); + } + let shared = self.shared.clone(); let thread = s .builder() .name("audio_thread".into()) @@ -151,7 +158,7 @@ impl<'lib> Plugin<'lib> { // Handle callbacks requests on the main thread while the audio thread is running while let Ok(task) = self.task_receiver.recv() { match task { - MainThreadTask::Dispatch(func) => func(self), + MainThreadTask::Dispatch { call, data } => call(self, data), MainThreadTask::CallbackRequest => self.poll_callback_unchecked(), MainThreadTask::StopAudioThread => break, } diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index c806b07..9536a8e 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -174,73 +174,79 @@ impl PluginShared { } #[track_caller] - unsafe fn from_clap_host<'a>(host: *const clap_host) -> &'a Self { - unsafe { - let state = (*host).host_data as *const PluginShared; - &*state - } - } + fn wrap(host: *const clap_host, function_name: &str, f: impl FnOnce(&Self) -> Result) -> Option { + log::trace!("'{}' was called by the plugin", function_name); - /// Set the callback error field if it does not already contain a value. Earlier errors are not - /// overwritten. - fn set_callback_error(&self, error: impl Into) { - let mut guard = self.callback_error.lock().unwrap(); - if guard.is_none() { - *guard = Some(error.into()); + check_null_ptr!(host, (*host).host_data); + let state = unsafe { &*((*host).host_data as *const PluginShared) }; + + match f(state) { + Ok(result) => Some(result), + Err(error) => { + let mut guard = state.callback_error.lock().unwrap(); + if guard.is_none() { + *guard = Some(format!("{}: {}", function_name, error)); + } + + None + } } } /// Checks whether this is the main thread. If it is not, then an error indicating this can be /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread /// safety errors will not overwrite earlier ones. - fn assert_main_thread(&self, function_name: &str) { + fn assert_main_thread(&self) -> Result<()> { let current_thread_id = std::thread::current().id(); - if current_thread_id != self.main_thread_id { - self.set_callback_error(format!( - "'{}' may only be called from the main thread (thread {:?}), but it was called from thread {:?}.", - function_name, self.main_thread_id, current_thread_id - )); - } + + anyhow::ensure!( + current_thread_id == self.main_thread_id, + "The function may only be called from the main thread (thread {:?}), but it was called from thread {:?}.", + self.main_thread_id, + current_thread_id + ); + + Ok(()) } /// Checks whether this is the audio thread. If it is not, then an error indicating this can be /// retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread /// safety errors will not overwrite earlier ones. - fn assert_audio_thread(&self, function_name: &str) { + fn assert_audio_thread(&self) -> Result<()> { let current_thread_id = std::thread::current().id(); if self.audio_thread_id.load() != Some(current_thread_id) { if current_thread_id == self.main_thread_id { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called from the main \ - thread." - )); + anyhow::bail!( + "This function may only be called from an audio thread, but it was called from the main thread." + ); } else { - self.set_callback_error(format!( - "'{function_name}' may only be called from an audio thread, but it was called from an unknown \ - thread." - )); + anyhow::bail!( + "This function may only be called from an audio thread, but it was called from an unknown thread \ + ({:?}).", + current_thread_id + ); } } + + Ok(()) } /// Checks whether this is **not** the audio thread. If it is, then an error indicating this can /// be retrieved using [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread /// safety errors will not overwrite earlier ones. - fn assert_not_audio_thread(&self, function_name: &str) { + fn assert_not_audio_thread(&self) -> Result<()> { let current_thread_id = std::thread::current().id(); if self.audio_thread_id.load() == Some(current_thread_id) { - self.set_callback_error(format!( - "'{function_name}' was called from an audio thread, this is not allowed.", - )); + anyhow::bail!("This function was called from the audio thread, this is not allowed."); } + Ok(()) } /// Checks whether the plugin has the required extension(s). If it does not, then an error /// will be set. Subsequent errors will not overwrite earlier ones. - fn assert_has_extension(&self, function_name: &str, ids: &[&CStr]) { + fn assert_has_extension(&self, ids: &[&CStr]) -> Result<()> { if self.status() == PluginStatus::Uninitialized { - self.set_callback_error(format!("'{}' called while the plugin is uninitialized.", function_name)); - return; + anyhow::bail!("Called while the plugin is uninitialized."); } for id in ids { @@ -249,15 +255,11 @@ impl PluginShared { }; if !extension_ptr.is_null() { - return; // found it! + return Ok(()); // found it! } } - self.set_callback_error(format!( - "'{}' called without the required extension: {}", - function_name, - ids[0].to_string_lossy() - )); + anyhow::bail!("Plugin does not implement extension {}", ids[0].to_string_lossy()); } } @@ -341,98 +343,85 @@ impl PluginShared { } unsafe extern "C" fn clap_request_restart(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - // This flag will be reset at the start of one of the `ProcessingTest::run*` functions, and - // in the multi-iteration run function it will trigger a deactivate->reactivate cycle - log::trace!("'clap_host::request_restart()' was called by the plugin, setting the flag"); - this.requested_restart.store(true); + Self::wrap(host, "clap_host::request_restart", |this| { + this.requested_restart.store(true); + Ok(()) + }); } unsafe extern "C" fn clap_request_process(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - // Handling this within the context of the validator would be a bit messy. Do plugins use - // this? - log::trace!("'clap_host::request_process()' was called by the plugin"); - this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); + Self::wrap(host, "clap_host::request_process", |this| { + this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); + Ok(()) + }); } unsafe extern "C" fn clap_request_callback(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - // This this is either handled by `handle_callbacks_blocking()` while the audio thread is - // active, or by an explicit call to `handle_callbacks_once()`. We print a warning if the - // callback is not handled before the plugin is destroyed. - log::trace!("'clap_host::request_callback()' was called by the plugin, setting the flag"); - this.requested_callback.store(true); - this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); + Self::wrap(host, "clap_host::request_callback", |this| { + this.requested_callback.store(true); + this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); + Ok(()) + }); } unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_audio_ports::is_rescan_flag_supported()"); - this.assert_has_extension("clap_host_audio_ports::is_rescan_flag_supported()", AudioPorts::IDS); - - log::trace!("'clap_host_audio_ports::is_rescan_flag_supported()' was called"); - true + Self::wrap(host, "clap_host_audio_ports::is_rescan_flag_supported", |this| { + this.assert_main_thread()?; + this.assert_has_extension(AudioPorts::IDS)?; + Ok(true) + }) + .unwrap_or(false) } unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { - let this = unsafe { PluginShared::from_clap_host(host) }; + Self::wrap(host, "clap_host_audio_ports::rescan", |this| { + this.assert_main_thread()?; + this.assert_has_extension(AudioPorts::IDS)?; - this.assert_main_thread("clap_host_audio_ports::rescan()"); - this.assert_has_extension("clap_host_audio_ports::rescan()", AudioPorts::IDS); - - log::trace!("'clap_host_audio_ports::rescan()' was called"); + if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); + } - if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); - } + if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { + if this.status() > PluginStatus::Activated { + anyhow::bail!("Called while the plugin is activate"); + } - if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - if this.status() > PluginStatus::Activated { - this.set_callback_error("'clap_host_audio_ports::rescan()' was called while the plugin was activated"); + this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); } - this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); - } + Ok(()) + }); } unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_note_ports::supported_dialects()"); - this.assert_has_extension("clap_host_note_ports::supported_dialects()", NotePorts::IDS); - - log::trace!("'clap_host_note_ports::supported_dialects()' was called"); - - CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE + Self::wrap(host, "clap_host_note_ports::supported_dialects", |this| { + this.assert_main_thread()?; + this.assert_has_extension(NotePorts::IDS)?; + Ok(CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE) + }) + .unwrap_or(0) } unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { - let this = unsafe { PluginShared::from_clap_host(host) }; + Self::wrap(host, "clap_host_note_ports::rescan", |this| { + this.assert_main_thread()?; + this.assert_has_extension(NotePorts::IDS)?; - this.assert_main_thread("clap_host_note_ports::rescan()"); - this.assert_has_extension("clap_host_note_ports::rescan()", NotePorts::IDS); + if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { + this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); + } - log::trace!("'clap_host_note_ports::rescan()' was called"); + if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { + if this.status() > PluginStatus::Activated { + anyhow::bail!("Called while the plugin is activate"); + } - if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { - this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); - } - - if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { - if this.status() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_note_ports::rescan(CLAP_NOTE_PORTS_RESCAN_ALL)' was called while the plugin was \ - activated", - ); + this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); } - this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); - } + Ok(()) + }); } unsafe extern "C" fn ext_preset_load_on_error( @@ -443,34 +432,29 @@ impl PluginShared { os_error: i32, msg: *const c_char, ) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_preset_load::on_error()"); - this.assert_has_extension("clap_host_preset_load::on_error()", PresetLoad::IDS); - - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::on_error()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } - .context("'clap_host_preset_load::on_error()' called with an invalid load_key parameter"); - let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } - .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter"); - match (location, load_key, msg) { - (Ok(location), Ok(Some(load_key)), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with load key {load_key}, OS error \ - code {os_error}, and the following error message: {msg}" - )); - } - (Ok(location), Ok(None), Ok(msg)) => { - this.set_callback_error(format!( - "'clap_host_preset_load::on_error()' called for {location} with no load key, OS error code \ - {os_error}, and the following error message: {msg}" - )); - } - (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => { - this.set_callback_error(format!("{err:#}")); + Self::wrap(host, "clap_host_preset_load::on_error", |this| -> Result<()> { + this.assert_main_thread()?; + this.assert_has_extension(PresetLoad::IDS)?; + + let location = unsafe { LocationValue::new(location_kind, location) } + .context("'clap_host_preset_load::on_error()' called with invalid location parameters")?; + let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'clap_host_preset_load::on_error()' called with an invalid load_key parameter")?; + let msg = unsafe { util::cstr_ptr_to_mandatory_string(msg) } + .context("'clap_host_preset_load::on_error()' called with an invalid msg parameter")?; + + if let Some(load_key) = &load_key { + anyhow::bail!( + "Called for {location} with load key {load_key}, OS error code {os_error}, and the following \ + error message: {msg}" + ); + } else { + anyhow::bail!( + "Called for {location} with no load key, OS error code {os_error}, and the following error \ + message: {msg}" + ); } - } + }); } unsafe extern "C" fn ext_preset_load_loaded( @@ -479,159 +463,144 @@ impl PluginShared { location: *const c_char, load_key: *const c_char, ) { - let this = unsafe { PluginShared::from_clap_host(host) }; + Self::wrap(host, "clap_host_preset_load::loaded", |this| { + this.assert_main_thread()?; + this.assert_has_extension(PresetLoad::IDS)?; - this.assert_main_thread("clap_host_preset_load::loaded()"); - this.assert_has_extension("clap_host_preset_load::loaded()", PresetLoad::IDS); + let _location = unsafe { LocationValue::new(location_kind, location) } + .context("'Called with invalid location parameters")?; + let _load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } + .context("'Called with an invalid load_key parameter")?; - let location = unsafe { LocationValue::new(location_kind, location) } - .context("'clap_host_preset_load::loaded()' called with invalid location parameters"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } - .context("'clap_host_preset_load::loaded()' called with an invalid load_key parameter"); - - match (location, load_key) { - (Ok(_location), Ok(_load_key)) => { - log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); - } - (Err(err), _) | (_, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - } - } + log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); + Ok(()) + }); } unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { - let this = unsafe { PluginShared::from_clap_host(host) }; + Self::wrap(host, "clap_host_params::rescan", |this| { + this.assert_main_thread()?; + this.assert_has_extension(Params::IDS)?; - this.assert_main_thread("clap_host_params::rescan()"); - this.assert_has_extension("clap_host_params::rescan()", Params::IDS); - - log::trace!("'clap_host_params::rescan()' was called"); - - if flags & CLAP_PARAM_RESCAN_VALUES != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); - } + if flags & CLAP_PARAM_RESCAN_VALUES != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); + } - if flags & CLAP_PARAM_RESCAN_TEXT != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanText).unwrap(); - } + if flags & CLAP_PARAM_RESCAN_TEXT != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanText).unwrap(); + } - if flags & CLAP_PARAM_RESCAN_INFO != 0 { - this.callback_sender.send(CallbackEvent::ParamsRescanInfo).unwrap(); - } + if flags & CLAP_PARAM_RESCAN_INFO != 0 { + this.callback_sender.send(CallbackEvent::ParamsRescanInfo).unwrap(); + } - if flags & CLAP_PARAM_RESCAN_ALL != 0 { - if this.status() > PluginStatus::Activated { - this.set_callback_error( - "'clap_host_params::rescan(CLAP_PARAM_RESCAN_ALL)' was called while the plugin is active", + if flags & CLAP_PARAM_RESCAN_ALL != 0 { + anyhow::ensure!( + this.status() <= PluginStatus::Activated, + "Called while the plugin is active" ); + + this.callback_sender.send(CallbackEvent::ParamsRescanAll).unwrap(); } - this.callback_sender.send(CallbackEvent::ParamsRescanAll).unwrap(); - } + Ok(()) + }); } unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_params::clear()"); - this.assert_has_extension("clap_host_params::clear()", Params::IDS); - - log::debug!("TODO: Handle 'clap_host_params::clear()'"); + Self::wrap(host, "clap_host_params::clear", |this| { + this.assert_main_thread()?; + this.assert_has_extension(Params::IDS)?; + log::debug!("TODO: Handle 'clap_host_params::clear()'"); + Ok(()) + }); } unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_not_audio_thread("clap_host_params::request_flush()"); - this.assert_has_extension("clap_host_params::request_flush()", Params::IDS); - - log::trace!("'clap_host_params::request_flush()' was called"); - this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); + Self::wrap(host, "clap_host_params::request_flush", |this| { + this.assert_not_audio_thread()?; + this.assert_has_extension(Params::IDS)?; + this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); + Ok(()) + }); } unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_state::mark_dirty()"); - this.assert_has_extension("clap_host_state::mark_dirty()", State::IDS); - - log::trace!("'clap_host_state::mark_dirty()' was called"); - this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); + Self::wrap(host, "clap_host_state::mark_dirty", |this| { + this.assert_main_thread()?; + this.assert_has_extension(State::IDS)?; + this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); + Ok(()) + }); } unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { - let this = unsafe { PluginShared::from_clap_host(host) }; - this.main_thread_id == std::thread::current().id() + Self::wrap(host, "clap_host_thread_check::is_main_thread", |this| { + Ok(this.main_thread_id == std::thread::current().id()) + }) + .unwrap_or(false) } unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { - let this = unsafe { PluginShared::from_clap_host(host) }; - this.audio_thread_id.load() == Some(std::thread::current().id()) + Self::wrap(host, "clap_host_thread_check::is_audio_thread", |this| { + Ok(this.audio_thread_id.load() == Some(std::thread::current().id())) + }) + .unwrap_or(false) } unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; + Self::wrap(host, "clap_host_latency::changed", |this| { + this.assert_main_thread()?; + this.assert_has_extension(Latency::IDS)?; - this.assert_main_thread("clap_host_latency::changed()"); - this.assert_has_extension("clap_host_latency::changed()", Latency::IDS); - - if this.status() != PluginStatus::Activating { - this.set_callback_error( - "'clap_host_latency::changed()' must only be called within 'clap_plugin::activate()'", + anyhow::ensure!( + this.status() == PluginStatus::Activating, + "Must only be called within 'clap_plugin::activate'" ); - } - log::trace!("'clap_host_latency::changed()' was called"); - this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); + this.callback_sender.send(CallbackEvent::LatencyChanged).unwrap(); + + Ok(()) + }); } unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_audio_thread("clap_host_tail::changed()"); - this.assert_has_extension("clap_host_tail::changed()", Tail::IDS); - - log::trace!("'clap_host_tail::changed()' was called"); - this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); + Self::wrap(host, "clap_host_tail::changed", |this| { + this.assert_audio_thread()?; + this.assert_has_extension(Tail::IDS)?; + this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); + Ok(()) + }); } unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { - let this = unsafe { PluginShared::from_clap_host(host) }; - - this.assert_main_thread("clap_host_voice_info::changed()"); - this.assert_has_extension("clap_host_voice_info::changed()", VoiceInfo::IDS); - - log::trace!("'clap_host_voice_info::changed()' was called"); - this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); + Self::wrap(host, "clap_host_voice_info::changed", |this| { + this.assert_main_thread()?; + this.assert_has_extension(VoiceInfo::IDS)?; + this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); + Ok(()) + }); } unsafe extern "C" fn ext_thread_pool_request_exec(host: *const clap_host, num_tasks: u32) -> bool { - let this = unsafe { PluginShared::from_clap_host(host) }; - - log::trace!("'clap_host_thread_pool::request_exec()' was called"); - - this.assert_audio_thread("clap_host_thread_pool::request_exec()"); - this.assert_has_extension("clap_host_thread_pool::request_exec()", ThreadPool::IDS); - - // Ensure this is called from within the process() function - // We already checked that we're on the audio thread, so this is sufficient - if !this.is_currently_in_process_call.load() { - this.set_callback_error( - "'clap_host_thread_pool::request_exec()' may only be called from within the audio thread's \ - 'clap_plugin::process()' function.", + Self::wrap(host, "clap_host_thread_pool::request_exec", |this| { + this.assert_audio_thread()?; + this.assert_has_extension(ThreadPool::IDS)?; + + // Ensure this is called from within the process() function + // We already checked that we're on the audio thread, so this is sufficient + anyhow::ensure!( + this.is_currently_in_process_call.load(), + "May only be called from within the audio thread's 'clap_plugin::process' function." ); - return false; - } + let extension = this.get_extension::().unwrap(); + (0..num_tasks).into_par_iter().for_each(|index| { + extension.exec(index); + }); - let Some(extension) = this.get_extension::() else { - return false; - }; - - (0..num_tasks).into_par_iter().for_each(|index| { - extension.exec(index); - }); - - true + Ok(true) + }) + .unwrap_or(false) } } diff --git a/src/plugin/process.rs b/src/plugin/process.rs index aafd964..a3d0db6 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -1,8 +1,5 @@ //! Data structures and functions surrounding audio processing. -use crate::plugin::{ - ext::tail::Tail, - instance::{PluginAudioThread, PluginStatus, ProcessStatus}, -}; +use crate::plugin::instance::{PluginAudioThread, PluginStatus, ProcessStatus}; use anyhow::Result; use clap_sys::process::*; use std::pin::Pin; @@ -17,8 +14,6 @@ pub use transport::*; pub struct ProcessScope<'a> { plugin: &'a PluginAudioThread<'a>, - plugin_tail: Option>, - buffer: &'a mut AudioBuffers, events_input: Pin>, @@ -42,8 +37,6 @@ impl<'a> ProcessScope<'a> { Ok(ProcessScope { plugin, - plugin_tail: plugin.get_extension(), - buffer, events_input: EventQueue::new(), events_output: EventQueue::new(), @@ -101,7 +94,7 @@ impl<'a> ProcessScope<'a> { let sample_rate = self.sample_rate; let buffer_size = self.buffer.samples(); self.plugin - .dispatch_main(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; + .on_main_thread(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; } // start processing if needed @@ -161,12 +154,6 @@ impl<'a> ProcessScope<'a> { // check output audio buffers for NaNs or infinities check_process_call_consistency(&self.buffer[..], &original_buffers, self.output_queue(), samples)?; - if status == ProcessStatus::Tail && self.plugin_tail.is_none() { - anyhow::bail!( - "Plugin returned `CLAP_PROCESS_TAIL` process status but does not implement the 'tail' extension." - ); - } - Ok(status) } @@ -176,7 +163,7 @@ impl<'a> ProcessScope<'a> { } if self.plugin.status() == PluginStatus::Activated { - self.plugin.dispatch_main(|plugin| { + self.plugin.on_main_thread(|plugin| { plugin.deactivate(); }); } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index 9922bd8..8e8fa0d 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -126,4 +126,9 @@ impl ConstantMask { pub fn is_channel_constant(&self, channel: u32) -> bool { self.0 & 1u64.unbounded_shl(channel) != 0 } + + pub fn are_first_n_channels_constant(&self, n: u32) -> bool { + let mask = (1u64.unbounded_shl(n)).wrapping_sub(1); + (self.0 & mask) == mask + } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 1bd98a1..740497a 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -37,8 +37,10 @@ pub enum PluginTestCase { ProcessAudioDoubleOutOfPlace, #[strum(serialize = "process-audio-double-in-place")] ProcessAudioDoubleInPlace, - #[strum(serialize = "process-audio-constant-mask")] - ProcessAudioConstantMask, + #[strum(serialize = "process-sleep-constant-mask")] + ProcessSleepConstantMask, + #[strum(serialize = "process-sleep-process-status")] + ProcessSleepProcessStatus, #[strum(serialize = "process-audio-reset-determinism")] ProcessAudioResetDeterminism, #[strum(serialize = "process-note-out-of-place-basic")] @@ -136,12 +138,13 @@ impl<'a> TestCase<'a> for PluginTestCase { 'audio-ports-config' extension.", PluginTestCase::ProcessAudioBasicInPlace, ), - PluginTestCase::ProcessAudioConstantMask => String::from( + PluginTestCase::ProcessSleepConstantMask => String::from( "Processes random audio through the plugin with its default parameter values while setting the \ constant mask on silent blocks, and tests whether the output does not contain any non-finite or \ subnormal values and that the plugin sets the constant mask correctly. Uses out-of-place audio \ processing.", ), + PluginTestCase::ProcessSleepProcessStatus => String::from("TODO: write ts"), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default parameter values and \ tests the output for consistency. Uses out-of-place audio processing.", @@ -278,8 +281,11 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessAudioDoubleInPlace => { processing::test_process_audio_double(library, plugin_id, true) } - PluginTestCase::ProcessAudioConstantMask => { - processing::test_process_audio_constant_mask(library, plugin_id) + PluginTestCase::ProcessSleepConstantMask => { + processing::test_process_sleep_constant_mask(library, plugin_id) + } + PluginTestCase::ProcessSleepProcessStatus => { + processing::test_process_sleep_process_status(library, plugin_id) } PluginTestCase::ProcessAudioResetDeterminism => { processing::test_process_audio_reset_determinism(library, plugin_id) diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index be70211..51962fa 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -1,18 +1,15 @@ -use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; +use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::audio_ports_activation::AudioPortsActivation; use crate::plugin::ext::audio_ports_config::{AudioPortsConfig, AudioPortsConfigInfo}; -use crate::plugin::ext::configurable_audio_ports::{AudioPortsRequest, ConfigurableAudioPorts}; +use crate::plugin::ext::configurable_audio_ports::ConfigurableAudioPorts; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; -use crate::tests::rng::{NoteGenerator, new_prng}; +use crate::tests::rng::{NoteGenerator, new_prng, random_layout_requests}; use crate::util::{cstr_ptr_to_mandatory_string, cstr_ptr_to_string}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::clap_audio_port_info; -use rand::Rng; -use rand::seq::SliceRandom; -use rand_pcg::Pcg32; const BUFFER_SIZE: u32 = 512; @@ -242,44 +239,6 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) /// The test for `PluginTestCase::LayoutConfigurableAudioPorts`. pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: &str) -> Result { - fn random_layout_requests(prng: &mut Pcg32, config: &AudioPortConfig) -> Vec { - let mut requests = Vec::new(); - - for (i, _) in config.inputs.iter().enumerate() { - requests.push(AudioPortsRequest { - is_input: true, - port_index: i as u32, - channel_count: prng.random_range(0..=8), - }); - } - - for (i, _) in config.outputs.iter().enumerate() { - requests.push(AudioPortsRequest { - is_input: false, - port_index: i as u32, - channel_count: prng.random_range(0..=8), - }); - } - - requests.shuffle(prng); - requests - } - - fn print_layout_requests(requests: &[AudioPortsRequest]) -> String { - let mut result = Vec::new(); - - for request in requests { - result.push(format!( - "{}{}-{}ch", - if request.is_input { "in" } else { "out" }, - request.port_index, - request.channel_count, - )); - } - - result.join(" ") - } - let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) @@ -323,9 +282,10 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: let mut checks_passed = 0; while checks_total < 200 && checks_passed < 20 { - let requests = random_layout_requests(&mut prng, &config_audio_ports); - let can_apply = configurable_audio_ports.can_apply_configuration(requests.iter().cloned()); - let has_applied = configurable_audio_ports.apply_configuration(requests.iter().cloned()); + let requests = random_layout_requests(&config_audio_ports, &mut prng); + + let can_apply = configurable_audio_ports.can_apply_configuration(requests.iter().copied()); + let has_applied = configurable_audio_ports.apply_configuration(requests.iter().copied()); if can_apply != has_applied { anyhow::bail!( @@ -333,7 +293,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: 'apply_configuration' ({}) for the following layout: {}", can_apply, has_applied, - print_layout_requests(&requests), + requests.iter().map(|r| format!("{}", r)).collect::>().join(", ") ); } @@ -368,7 +328,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: .with_context(|| { format!( "Error while processing audio with the following configuration: {}", - print_layout_requests(&requests) + requests.iter().map(|r| format!("{}", r)).collect::>().join(", ") ) })?; } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index d1a90a8..1494ec9 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -1,7 +1,10 @@ //! Contains most of the boilerplate around testing audio processing. use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; -use crate::plugin::ext::note_ports::NotePorts; +use crate::plugin::ext::latency::Latency; +use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; +use crate::plugin::ext::tail::Tail; +use crate::plugin::instance::{CallbackEvent, ProcessStatus}; use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; @@ -176,7 +179,6 @@ pub fn test_process_note_out_of_place( // We'll fill the input event queue with (consistent) random CLAP note and/or MIDI // events depending on what's supported by the plugin supports - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut note_rng = NoteGenerator::new(¬e_ports_config); if !consistent { @@ -207,8 +209,8 @@ pub fn test_process_note_out_of_place( /// The test for `PluginTestCase::ProcessVaryingSampleRates`. pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &str) -> Result { const SAMPLE_RATES: &[f64] = &[ - 1000.0, 10000.0, 22050.0, 32000.0, 44100.0, 48000.0, 88200.0, 96000.0, 192000.0, 384000.0, 768000.0, 1234.5678, - 12345.678, 45678.901, 123456.78, + 8000.0, 22050.0, 44100.0, 48000.0, 88200.0, 96000.0, 192000.0, 384000.0, 768000.0, 1234.5678, 12345.678, + 45678.901, 123456.78, ]; let mut prng = new_prng(); @@ -369,8 +371,110 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) Ok(TestStatus::Success { details: None }) } -/// The test for `PluginTestCase::ProcessAudioConstantMask`. -pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str) -> Result { +/// The test for `PluginTestCase::ProcessResetDeterminism`. +pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: &str) -> Result { + const BUFFER_SIZE: u32 = 4096; + + let plugin = library + .create_plugin(plugin_id) + .context("Could not create the plugin instance")?; + plugin.init().context("Error during initialization")?; + + let audio_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'audio-ports' IO configuration")? + .unwrap_or_default(); + + let note_ports_config = plugin + .get_extension::() + .map(|x| x.config()) + .transpose() + .context("Error while querying 'note-ports' IO configuration")? + .unwrap_or_default(); + + let result = plugin.on_audio_thread(|plugin| -> Result { + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + // first run, "control" run + process.audio_buffers().fill_white_noise(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + + let output_control = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); + + // second run, deactivate and reactivate the plugin, see if the output changes + process.restart(); + process.audio_buffers().fill_white_noise(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + + let output_reactivated = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); + + // third run, reset the plugin, see if the output matches the control run + process.reset(); + process.audio_buffers().fill_white_noise(&mut new_prng()); + process + .input_queue() + .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run()?; + + let output_reset = process + .audio_buffers() + .iter() + .filter(|x| x.port().output().is_some()) + .cloned() + .collect::>(); + + if output_control + .iter() + .zip(output_reactivated.iter()) + .any(|(a, b)| !a.is_same(b)) + { + return Ok(TestStatus::Warning { + details: Some(String::from( + "Plugin output does not seem to be deterministic after reactivation", + )), + }); + } + + if output_control + .iter() + .zip(output_reset.iter()) + .any(|(a, b)| !a.is_same(b)) + { + anyhow::bail!("Plugin output differs after reset"); + } + + Ok(TestStatus::Success { details: None }) + })?; + + plugin + .poll_callback(|_| Ok(())) + .context("An error occured during a callback")?; + + Ok(result) +} + +/// The test for `PluginTestCase::ProcessSleepConstantMask`. +pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); let plugin = library @@ -382,22 +486,16 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str Some(audio_ports) => audio_ports .config() .context("Error while querying 'audio-ports' IO configuration")?, - None => { - return Ok(TestStatus::Skipped { - details: Some(String::from( - "The plugin does not implement the 'audio-ports' extension.", - )), - }); - } + None => AudioPortConfig::default(), }; - if audio_ports_config.inputs.is_empty() { - return Ok(TestStatus::Skipped { - details: Some(String::from("The plugin does not have any audio input ports.")), - }); - } + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut has_received_constant_output = false; let mut has_received_constant_flag = false; @@ -415,7 +513,8 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str if marked_constant && !is_constant { anyhow::bail!( "The plugin has marked output port {output}, channel {channel} as constant, but it contains \ - non-constant data." + non-constant data. {:?}", + buffer.channel(channel) ); } @@ -433,16 +532,22 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str }; plugin.on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); + let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; // block 1: silent inputs, see what the plugin does process.run()?; - check_buffers(process.audio_buffers())?; + check_buffers(process.audio_buffers()).context("Init block (silent)")?; // block 2: randomize inputs, see if the plugin tracks constant channels process.audio_buffers().fill_white_noise(&mut prng); + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.input_queue().add_events(note_rng.stop_all_voices(BUFFER_SIZE)); process.run()?; - check_buffers(process.audio_buffers())?; + check_buffers(process.audio_buffers()).context("Init block (white noise)")?; // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay @@ -470,104 +575,116 @@ pub fn test_process_audio_constant_mask(library: &PluginLibrary, plugin_id: &str Ok(TestStatus::Success { details: None }) } -/// The test for `PluginTestCase::ProcessResetDeterminism`. -pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: &str) -> Result { - const BUFFER_SIZE: u32 = 4096; +/// The test for `PluginTestCase::ProcessSleepProcessStatus`. +pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &str) -> Result { + let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) .context("Could not create the plugin instance")?; plugin.init().context("Error during initialization")?; - let audio_ports_config = plugin - .get_extension::() - .map(|x| x.config()) - .transpose() - .context("Error while querying 'audio-ports' IO configuration")? - .unwrap_or_default(); + let audio_ports_config = match plugin.get_extension::() { + Some(audio_ports) => audio_ports + .config() + .context("Error while querying 'audio-ports' IO configuration")?, + None => AudioPortConfig::default(), + }; - let note_ports_config = plugin - .get_extension::() - .map(|x| x.config()) - .transpose() - .context("Error while querying 'note-ports' IO configuration")? - .unwrap_or_default(); + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + + let mut latency = plugin.get_extension::().map_or(0, |ext| ext.get()); + let mut is_sleeping = false; + let mut has_ever_slept = false; + + plugin.on_audio_thread(|plugin| -> Result<()> { + let tail = plugin.get_extension::(); - let result = plugin.on_audio_thread(|plugin| -> Result { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - // first run, "control" run - process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + let mut quiet_time = 0; - let output_control = process - .audio_buffers() - .iter() - .filter(|x| x.port().output().is_some()) - .cloned() - .collect::>(); + for i in 0..40 { + let is_quiet = (0..5).contains(&i) || (10..20).contains(&i) || (30..40).contains(&i); - // second run, deactivate and reactivate the plugin, see if the output changes - process.restart(); - process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + if is_quiet { + process.input_queue().add_events(note_rng.stop_all_voices(0)); + process.audio_buffers().fill_silence(); + } else { + process + .input_queue() + .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.audio_buffers().fill_white_noise(&mut prng); + } - let output_reactivated = process - .audio_buffers() - .iter() - .filter(|x| x.port().output().is_some()) - .cloned() - .collect::>(); + plugin.poll_callback(|plugin, event| match event { + CallbackEvent::LatencyChanged => { + latency = plugin.get_extension::().map_or(0, |ext| ext.get()); + Ok(()) + } - // third run, reset the plugin, see if the output matches the control run - process.reset(); - process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + CallbackEvent::RequestProcess => { + is_sleeping = false; + Ok(()) + } - let output_reset = process - .audio_buffers() - .iter() - .filter(|x| x.port().output().is_some()) - .cloned() - .collect::>(); + _ => Ok(()), + })?; - if output_control - .iter() - .zip(output_reactivated.iter()) - .any(|(a, b)| !a.is_same(b)) - { - return Ok(TestStatus::Warning { - details: Some(String::from( - "Plugin output does not seem to be deterministic after reactivation", - )), - }); - } + let status = process.run()?; - if output_control - .iter() - .zip(output_reset.iter()) - .any(|(a, b)| !a.is_same(b)) - { - anyhow::bail!("Plugin output differs after reset"); + if is_sleeping && is_quiet { + // TODO: check that the output is silent + } + + match status { + ProcessStatus::Continue => is_sleeping = false, + ProcessStatus::Sleep => is_sleeping = true, + + ProcessStatus::ContinueIfNotQuiet => { + let is_output_quiet = process + .audio_buffers() + .iter() + .filter(|b| b.port().output().is_some()) + .all(|b| b.get_output_constant_mask().are_first_n_channels_constant(b.channels())); + + is_sleeping = is_output_quiet; + } + + ProcessStatus::Tail => { + let tail = match &tail { + Some(tail) => tail.get(), + None => { + anyhow::bail!( + "Plugin returned `CLAP_PROCESS_TAIL` process status but does not implement the 'tail' \ + extension." + ); + } + }; + + is_sleeping = tail + latency < quiet_time; + if is_quiet { + quiet_time += BUFFER_SIZE; + } else { + quiet_time = 0; + } + } + } } - Ok(TestStatus::Success { details: None }) + Ok(()) })?; plugin .poll_callback(|_| Ok(())) .context("An error occured during a callback")?; - Ok(result) + Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/rng.rs b/src/tests/rng.rs index aa09b27..83eda6f 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -1,12 +1,15 @@ //! Utilities for generating pseudo-random data. +use crate::plugin::ext::audio_ports::AudioPortConfig; +use crate::plugin::ext::configurable_audio_ports::{AudioPortsRequest, AudioPortsRequestInfo}; use crate::plugin::ext::note_ports::NotePortConfig; use crate::plugin::ext::params::{Param, ParamInfo}; -use crate::plugin::process::{Event, EventQueue, TransportState}; +use crate::plugin::process::{Event, TransportState}; use clap_sys::events::*; +use clap_sys::ext::ambisonic::*; use midi_consts::channel_event as midi; use rand::Rng; -use rand::seq::IteratorRandom; +use rand::seq::{IndexedRandom, IteratorRandom}; use rand_pcg::Pcg32; use std::ops::RangeInclusive; @@ -595,13 +598,12 @@ impl<'a> NoteGenerator<'a> { panic!("Unable to generate a random note event after 1024 tries"); } - #[allow(unused)] - pub fn stop_all_voices(&mut self, queue: &EventQueue, time_offset: u32) { + pub fn stop_all_voices(&mut self, time_offset: u32) -> Vec { let mut events = vec![]; - for (note_port_idx, active_notes) in self.active_notes.drain(..).enumerate() { + for (note_port_idx, active_notes) in self.active_notes.iter_mut().enumerate() { let supports_clap = self.config.inputs[note_port_idx].supports_clap(); - for note in active_notes { + for note in active_notes.drain(..) { if supports_clap { events.push(Event::Note(clap_event_note { header: clap_event_header { @@ -633,7 +635,7 @@ impl<'a> NoteGenerator<'a> { } } - queue.add_events(events); + events } #[allow(unused)] @@ -873,3 +875,83 @@ impl TransportFuzzer { } } } + +pub fn random_layout_requests(config: &AudioPortConfig, prng: &mut Pcg32) -> Vec> { + fn random_request_info(prng: &mut Pcg32) -> AudioPortsRequestInfo<'static> { + match prng.random_range(0..=4) { + 0 => AudioPortsRequestInfo::Mono, + 1 => AudioPortsRequestInfo::Stereo, + 2 => AudioPortsRequestInfo::Untyped { + channel_count: prng.random_range(1..=16), + }, + 3 => { + const AMBISONIC_ACN_SN3D: clap_ambisonic_config = clap_ambisonic_config { + ordering: CLAP_AMBISONIC_ORDERING_ACN, + normalization: CLAP_AMBISONIC_NORMALIZATION_SN3D, + }; + + const AMBISONIC_FUMA_MAXN: clap_ambisonic_config = clap_ambisonic_config { + ordering: CLAP_AMBISONIC_ORDERING_FUMA, + normalization: CLAP_AMBISONIC_NORMALIZATION_MAXN, + }; + + let channel_count = prng.random_range(1..=4u32).pow(2); + let is_acn_sn3d = prng.random_bool(0.5); + + AudioPortsRequestInfo::Ambisonic { + channel_count, + config: if is_acn_sn3d { + &AMBISONIC_ACN_SN3D + } else { + &AMBISONIC_FUMA_MAXN + }, + } + } + _ => { + const SURROUND_MAPS: &[&[u8]] = &[ + &[0, 1], // Stereo; FL FR + &[0, 2, 1], // 3.0; FL FC FR + &[0, 2, 1, 3], // 3.1; FL FC FR LFE + &[0, 2, 1, 8], // 4.0; FL FC FR BC + &[0, 2, 1, 8, 3], // 4.1; FL FC FR BC LFE + &[0, 2, 1, 9, 10], // 5.0; FL FC FR SL SR + &[0, 2, 1, 9, 10, 3], // 5.1; FL FC FR SL SR LFE + ]; + + AudioPortsRequestInfo::Surround { + channel_map: SURROUND_MAPS.choose(prng).unwrap(), + } + } + } + } + + let mut requests = vec![]; + + for index in 0..config.inputs.len() { + if prng.random_bool(0.1) { + // skip request for some inputs + continue; + } + + requests.push(AudioPortsRequest { + is_input: true, + port_index: index as u32, + request_info: random_request_info(prng), + }); + } + + for index in 0..config.outputs.len() { + if prng.random_bool(0.1) { + // skip request for some outputs + continue; + } + + requests.push(AudioPortsRequest { + is_input: false, + port_index: index as u32, + request_info: random_request_info(prng), + }); + } + + requests +} diff --git a/src/util.rs b/src/util.rs index e9c0936..aaecb65 100644 --- a/src/util.rs +++ b/src/util.rs @@ -169,7 +169,8 @@ pub fn validator_version() -> &'static CStr { } pub fn install_panic_hook() { - std::panic::set_hook(Box::new(move |info| { + #[track_caller] + fn hook(info: &std::panic::PanicHookInfo) { let backtrace = std::backtrace::Backtrace::capture(); let backtrace = if backtrace.status() == std::backtrace::BacktraceStatus::Disabled { String::from(". Set RUST_BACKTRACE=1 for a backtrace.") @@ -207,7 +208,9 @@ pub fn install_panic_hook() { backtrace ), } - })); + } + + std::panic::set_hook(Box::new(hook)); } impl IteratorExt for T where T: Iterator {} From 4ac9aef3a81a921d4b7cd0f7cf368f1afb4d7f90 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Mon, 2 Feb 2026 03:19:15 +0400 Subject: [PATCH 051/114] better panic handling; object lifetime tracking; fixed sleep tests (more lenient) --- Cargo.lock | 10 + Cargo.toml | 1 + rustfmt.toml | 3 +- src/main.rs | 3 +- src/panic.rs | 67 ++ src/plugin.rs | 1 + src/plugin/ext/ambisonic.rs | 11 +- src/plugin/ext/audio_ports.rs | 2 +- src/plugin/ext/audio_ports_activation.rs | 10 +- src/plugin/ext/audio_ports_config.rs | 2 +- src/plugin/ext/configurable_audio_ports.rs | 2 +- src/plugin/ext/latency.rs | 2 +- src/plugin/ext/note_ports.rs | 2 +- src/plugin/ext/params.rs | 2 +- src/plugin/ext/preset_load.rs | 2 +- src/plugin/ext/state.rs | 20 +- src/plugin/ext/surround.rs | 10 +- src/plugin/ext/tail.rs | 2 +- src/plugin/ext/thread_pool.rs | 10 +- src/plugin/ext/voice_info.rs | 2 +- src/plugin/instance/audio_thread.rs | 7 +- src/plugin/instance/main_thread.rs | 27 +- src/plugin/instance/shared.rs | 115 ++-- src/plugin/library.rs | 2 +- src/plugin/preset_discovery.rs | 33 +- src/plugin/preset_discovery/indexer.rs | 241 ++++---- .../preset_discovery/metadata_receiver.rs | 571 ++++++++---------- src/plugin/preset_discovery/provider.rs | 61 +- src/plugin/process/buffer.rs | 18 +- src/plugin/process/events.rs | 23 +- src/plugin/process/transport.rs | 13 +- src/plugin/util.rs | 185 ++++++ src/tests/plugin.rs | 11 +- src/tests/plugin/layout.rs | 10 +- src/tests/plugin/params.rs | 44 +- src/tests/plugin/processing.rs | 149 +++-- src/tests/plugin/state.rs | 68 +-- src/tests/plugin/transport.rs | 32 +- src/tests/plugin_library/preset_discovery.rs | 4 +- src/util.rs | 193 ------ src/validator.rs | 42 +- tests/clack-synth/src/lib.rs | 16 +- 42 files changed, 1015 insertions(+), 1014 deletions(-) create mode 100644 src/panic.rs create mode 100644 src/plugin/util.rs diff --git a/Cargo.lock b/Cargo.lock index 966c79c..0634de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,7 @@ dependencies = [ "tempfile", "textwrap", "time", + "wait-timeout", "walkdir", ] @@ -846,6 +847,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.3" diff --git a/Cargo.toml b/Cargo.toml index 1574639..0e4c0e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ tempfile = "3.3" textwrap = { version = "0.16.2", features = ["terminal_size"] } time = { version = "0.3", features = ["serde"]} walkdir = "2.3" +wait-timeout = "0.2.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" diff --git a/rustfmt.toml b/rustfmt.toml index b8ffc7c..f0a4196 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,4 @@ format_strings = true comment_width = 120 -max_width = 120 \ No newline at end of file +max_width = 120 +imports_granularity = "Module" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f40f3f0..2adf678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::process::ExitCode; mod commands; mod index; +mod panic; mod plugin; mod tests; mod util; @@ -74,7 +75,7 @@ fn main() -> ExitCode { .expect("Could not initialize logger"); // Install the panic hook to log panics instead of printing them to stderr. - util::install_panic_hook(); + panic::install_panic_hook(); // Mark the main thread as such for plugin instance creation checks. unsafe { diff --git a/src/panic.rs b/src/panic.rs new file mode 100644 index 0000000..9bb60c3 --- /dev/null +++ b/src/panic.rs @@ -0,0 +1,67 @@ +//! Panic handling utilities. +//! +//! When testing, validator should return an `Error` if the plugin is misbehaving, not panic. +//! Any panics (except for [`fail_test!`]) while testing are considered a bug in the validator itself, making them worth logging. + +pub fn install_panic_hook() { + #[track_caller] + fn hook(info: &std::panic::PanicHookInfo) { + let backtrace = std::backtrace::Backtrace::capture(); + let backtrace = if backtrace.status() == std::backtrace::BacktraceStatus::Disabled { + String::from(". Set RUST_BACKTRACE=1 for a backtrace.") + } else { + format!("\n{}", backtrace) + }; + + let thread = std::thread::current().name().unwrap_or("").to_owned(); + let message = panic_message(info.payload()); + + match info.location() { + Some(location) => { + log::error!( + target: "panic", "thread '{}' panicked at '{}': {}:{}{}", + thread, + message, + location.file(), + location.line(), + backtrace + ); + } + None => log::error!( + target: "panic", + "thread '{}' panicked at '{}'{:?}", + thread, + message, + backtrace + ), + } + } + + std::panic::set_hook(Box::new(hook)); +} + +pub fn panic_message(panic: &dyn std::any::Any) -> String { + if let Some(s) = panic.downcast_ref::<&'static str>() { + format!("{}. This is a bug in the validator", s) + } else if let Some(s) = panic.downcast_ref::() { + format!("{}. This is a bug in the validator", s) + } else if let Some(message) = panic.downcast_ref::() { + message.0.clone() + } else { + "A panic occurred. This is a bug in the validator".to_string() + } +} + +#[doc(hidden)] +pub struct TestFailure(pub String); + +/// Fails the current test with a panic, taking down the whole process. +/// This is a last-resort mechanism for when a test cannot continue due to an error in the plugin being tested. +/// Prefer regular error handling where possible. +macro_rules! fail_test { + ($($arg:tt)*) => { + std::panic::panic_any($crate::panic::TestFailure(format!($($arg)*))) + }; +} + +pub(crate) use fail_test; diff --git a/src/plugin.rs b/src/plugin.rs index fd42604..0583094 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -5,3 +5,4 @@ pub mod instance; pub mod library; pub mod preset_discovery; pub mod process; +pub mod util; diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index 512bdb4..e982680 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -1,9 +1,10 @@ -use crate::{ - plugin::{ext::Extension, instance::Plugin}, - util::clap_call, -}; +use crate::plugin::ext::Extension; +use crate::plugin::instance::Plugin; +use crate::plugin::util::clap_call; use clap_sys::ext::ambisonic::*; -use std::{ffi::CStr, mem::zeroed, ptr::NonNull}; +use std::ffi::CStr; +use std::mem::zeroed; +use std::ptr::NonNull; pub struct Ambisonic<'a> { plugin: &'a Plugin<'a>, diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 757874e..4b1278b 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -4,7 +4,7 @@ use super::Extension; use crate::plugin::ext::ambisonic::Ambisonic; use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use anyhow::{Context, Result}; use clap_sys::ext::ambisonic::CLAP_PORT_AMBISONIC; use clap_sys::ext::audio_ports::*; diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index e4ec90c..5a5db5f 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -1,9 +1,9 @@ -use crate::{ - plugin::{ext::Extension, instance::Plugin}, - util::clap_call, -}; +use crate::plugin::ext::Extension; +use crate::plugin::instance::Plugin; +use crate::plugin::util::clap_call; use clap_sys::ext::audio_ports_activation::*; -use std::{ffi::CStr, ptr::NonNull}; +use std::ffi::CStr; +use std::ptr::NonNull; /// Abstraction for the `audio-ports-activation` extension covering the main thread functionality. pub struct AudioPortsActivation<'a> { diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 492c90f..7441c74 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -3,7 +3,7 @@ use crate::plugin::ext::ambisonic::Ambisonic; use crate::plugin::ext::audio_ports::check_audio_port_type_consistent; use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; -use crate::util::{c_char_slice_to_string, clap_call}; +use crate::plugin::util::{c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::clap_audio_port_info; use clap_sys::ext::audio_ports_config::*; diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index 4366588..6f5f969 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use clap_sys::ext::ambisonic::{CLAP_PORT_AMBISONIC, clap_ambisonic_config}; use clap_sys::ext::audio_ports::{CLAP_PORT_MONO, CLAP_PORT_STEREO}; use clap_sys::ext::configurable_audio_ports::{ diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 75f6f47..88cff50 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::{Plugin, PluginStatus}; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_plugin_latency}; use std::ffi::CStr; use std::ptr::NonNull; diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 82f974e..1eaf203 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -2,7 +2,7 @@ use super::Extension; use crate::plugin::instance::Plugin; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use anyhow::{Context, Result}; use clap_sys::ext::note_ports::*; use clap_sys::id::CLAP_INVALID_ID; diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 39d169b..b73d73e 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -3,7 +3,7 @@ use super::Extension; use crate::plugin::instance::{Plugin, PluginStatus}; use crate::plugin::process::EventQueue; -use crate::util::{self, c_char_slice_to_string, clap_call}; +use crate::plugin::util::{self, c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; use clap_sys::ext::params::*; use clap_sys::id::{CLAP_INVALID_ID, clap_id}; diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 1f3818e..7610fd7 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -8,7 +8,7 @@ use std::ptr::NonNull; use super::Extension; use crate::plugin::instance::Plugin; use crate::plugin::preset_discovery::LocationValue; -use crate::util::clap_call; +use crate::plugin::util::clap_call; /// Abstraction for the `preset-load` extension covering the main thread functionality. pub struct PresetLoad<'a> { diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 3a15bde..7b876e9 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -1,5 +1,9 @@ //! Abstractions for interacting with the `state` extension. +use super::Extension; +use crate::panic::fail_test; +use crate::plugin::instance::Plugin; +use crate::plugin::util::clap_call; use anyhow::Result; use clap_sys::ext::state::{CLAP_EXT_STATE, clap_plugin_state}; use clap_sys::stream::{clap_istream, clap_ostream}; @@ -9,10 +13,6 @@ use std::ptr::NonNull; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; -use super::Extension; -use crate::plugin::instance::Plugin; -use crate::util::{check_null_ptr, clap_call}; - /// Abstraction for the `state` extension covering the main thread functionality. pub struct State<'a> { plugin: &'a Plugin<'a>, @@ -175,10 +175,12 @@ impl<'a> InputStream<'a> { unsafe extern "C" fn read(stream: *const clap_istream, buffer: *mut c_void, size: u64) -> i64 { unsafe { - check_null_ptr!(stream, (*stream).ctx, buffer); - let this = &*((*stream).ctx as *const Self); + if stream.is_null() || (*stream).ctx.is_null() || buffer.is_null() { + fail_test!("'clap_istream::read' was called with a null pointer"); + } // The reads may be limited to a certain buffering size to test the plugin's capabilities + let this = &*((*stream).ctx as *const Self); let size = match this.max_read_size { Some(max_read_size) => size.min(max_read_size as u64), None => size, @@ -235,10 +237,12 @@ impl OutputStream { unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { unsafe { - check_null_ptr!(stream, (*stream).ctx, buffer); - let this = &*((*stream).ctx as *const Self); + if stream.is_null() || (*stream).ctx.is_null() || buffer.is_null() { + fail_test!("'clap_ostream::write' was called with a null pointer"); + } // The writes may be limited to a certain buffering size to test the plugin's capabilities + let this = &*((*stream).ctx as *const Self); let size = match this.max_write_size { Some(max_write_size) => size.min(max_write_size as u64), None => size, diff --git a/src/plugin/ext/surround.rs b/src/plugin/ext/surround.rs index a61b552..f5037ee 100644 --- a/src/plugin/ext/surround.rs +++ b/src/plugin/ext/surround.rs @@ -1,9 +1,9 @@ -use crate::{ - plugin::{ext::Extension, instance::Plugin}, - util::clap_call, -}; +use crate::plugin::ext::Extension; +use crate::plugin::instance::Plugin; +use crate::plugin::util::clap_call; use clap_sys::ext::surround::*; -use std::{ffi::CStr, ptr::NonNull}; +use std::ffi::CStr; +use std::ptr::NonNull; pub struct Surround<'a> { plugin: &'a Plugin<'a>, diff --git a/src/plugin/ext/tail.rs b/src/plugin/ext/tail.rs index 33e0450..c000d1f 100644 --- a/src/plugin/ext/tail.rs +++ b/src/plugin/ext/tail.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::PluginAudioThread; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_plugin_tail}; use std::ffi::CStr; use std::ptr::NonNull; diff --git a/src/plugin/ext/thread_pool.rs b/src/plugin/ext/thread_pool.rs index 3b52b6f..330da3d 100644 --- a/src/plugin/ext/thread_pool.rs +++ b/src/plugin/ext/thread_pool.rs @@ -1,9 +1,9 @@ -use crate::{ - plugin::{ext::Extension, instance::PluginShared}, - util::clap_call, -}; +use crate::plugin::ext::Extension; +use crate::plugin::instance::PluginShared; +use crate::plugin::util::clap_call; use clap_sys::ext::thread_pool::{CLAP_EXT_THREAD_POOL, clap_plugin_thread_pool}; -use std::{ffi::CStr, ptr::NonNull}; +use std::ffi::CStr; +use std::ptr::NonNull; pub struct ThreadPool<'a> { plugin: &'a PluginShared, diff --git a/src/plugin/ext/voice_info.rs b/src/plugin/ext/voice_info.rs index 69edacb..6135289 100644 --- a/src/plugin/ext/voice_info.rs +++ b/src/plugin/ext/voice_info.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use clap_sys::ext::voice_info::*; use std::ffi::CStr; use std::mem::zeroed; diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index aca78f6..175b487 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -3,13 +3,10 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; use crate::plugin::instance::{CallbackEvent, MainThreadTask, PluginShared}; -use crate::util::clap_call; +use crate::plugin::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; -use clap_sys::process::{ - CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR, CLAP_PROCESS_SLEEP, - CLAP_PROCESS_TAIL, clap_process, -}; +use clap_sys::process::*; use std::any::Any; use std::marker::PhantomData; use std::mem::MaybeUninit; diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index 9317975..a0ed28a 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -1,20 +1,15 @@ -use crate::{ - plugin::{ - ext::Extension, - instance::{CallbackEvent, PluginAudioThread, PluginShared, PluginStatus}, - library::PluginMetadata, - }, - util::clap_call, -}; +use crate::plugin::ext::Extension; +use crate::plugin::instance::{CallbackEvent, PluginAudioThread, PluginShared, PluginStatus}; +use crate::plugin::library::PluginMetadata; +use crate::plugin::util::clap_call; use anyhow::Result; use clap_sys::plugin::clap_plugin; -use std::{ - marker::PhantomData, - panic::resume_unwind, - pin::Pin, - ptr::NonNull, - sync::{Arc, mpsc::Receiver}, -}; +use std::marker::PhantomData; +use std::panic::resume_unwind; +use std::pin::Pin; +use std::ptr::NonNull; +use std::sync::Arc; +use std::sync::mpsc::Receiver; pub enum MainThreadTask { Dispatch { @@ -151,7 +146,7 @@ impl<'lib> Plugin<'lib> { let shared = self.shared.clone(); let thread = s .builder() - .name("audio_thread".into()) + .name("audio-thread".into()) .spawn(move |_| f(PluginAudioThread::new(shared))) .unwrap(); diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index 9536a8e..ff2af84 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -1,3 +1,4 @@ +use crate::panic::fail_test; use crate::plugin::ext::Extension; use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::latency::Latency; @@ -10,7 +11,7 @@ use crate::plugin::ext::thread_pool::ThreadPool; use crate::plugin::ext::voice_info::VoiceInfo; use crate::plugin::instance::{CallbackEvent, MainThreadTask, Plugin, PluginStatus}; use crate::plugin::preset_discovery::LocationValue; -use crate::util::{self, check_null_ptr, clap_call, validator_version}; +use crate::plugin::util::{self, clap_call, object_tracker, validator_version}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::*; use clap_sys::ext::latency::*; @@ -31,6 +32,7 @@ use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::ffi::{CStr, c_char, c_void}; +use std::mem::offset_of; use std::ptr::NonNull; use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, Mutex}; @@ -41,7 +43,7 @@ use std::thread::ThreadId; pub struct PluginShared { pub task_sender: Sender, pub callback_sender: Sender, - pub callback_error: Mutex>, + pub callback_error: Mutex>, /// The plugin's current state in terms of activation and processing status. pub status: AtomicCell, @@ -71,6 +73,12 @@ pub struct PluginShared { unsafe impl Send for PluginShared {} unsafe impl Sync for PluginShared {} +impl Drop for PluginShared { + fn drop(&mut self) { + object_tracker::untrack(self.clap_host_ptr()); + } +} + impl PluginShared { /// Create a plugin instance and return the still uninitialized plugin. Returns an error if the /// plugin could not be created. The plugin instance will be registered with the host, and @@ -119,6 +127,9 @@ impl PluginShared { .write(&*shared as *const _ as *mut std::ffi::c_void); } + // Add the clap_host to the tracker so it can be validated in callbacks + object_tracker::track(shared.clap_host_ptr()); + let clap_plugin = unsafe { clap_call! { factory=>create_plugin(factory, shared.clap_host_ptr(), plugin_id.as_ptr()) @@ -177,15 +188,24 @@ impl PluginShared { fn wrap(host: *const clap_host, function_name: &str, f: impl FnOnce(&Self) -> Result) -> Option { log::trace!("'{}' was called by the plugin", function_name); - check_null_ptr!(host, (*host).host_data); - let state = unsafe { &*((*host).host_data as *const PluginShared) }; + let state = unsafe { + if let Err(e) = object_tracker::check(host) { + fail_test!("{}: {}", function_name, e); + } + + if (*host).host_data.wrapping_byte_add(offset_of!(Self, clap_host)) != host as *mut _ { + fail_test!("{}: Malformed 'clap_host.host_data' pointer", function_name); + } + + &*((*host).host_data as *const Self) + }; match f(state) { Ok(result) => Some(result), Err(error) => { let mut guard = state.callback_error.lock().unwrap(); if guard.is_none() { - *guard = Some(format!("{}: {}", function_name, error)); + *guard = Some(error.context(function_name.to_string())); } None @@ -245,9 +265,10 @@ impl PluginShared { /// Checks whether the plugin has the required extension(s). If it does not, then an error /// will be set. Subsequent errors will not overwrite earlier ones. fn assert_has_extension(&self, ids: &[&CStr]) -> Result<()> { - if self.status() == PluginStatus::Uninitialized { - anyhow::bail!("Called while the plugin is uninitialized."); - } + anyhow::ensure!( + self.status() != PluginStatus::Uninitialized, + "Called while the plugin is uninitialized" + ); for id in ids { let extension_ptr = unsafe { @@ -312,34 +333,41 @@ impl PluginShared { }; unsafe extern "C" fn clap_get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { - check_null_ptr!(host, (*host).host_data, extension_id); - // Right now there's no way to have the host only expose certain extensions. We can always // add that when test cases need it. - let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; - if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { - &Self::EXT_AUDIO_PORTS as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { - &Self::EXT_NOTE_PORTS as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { - &Self::EXT_PRESET_LOAD as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_PARAMS { - &Self::EXT_PARAMS as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_STATE { - &Self::EXT_STATE as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { - &Self::EXT_THREAD_CHECK as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_THREAD_POOL { - &Self::EXT_THREAD_POOL as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_LATENCY { - &Self::EXT_LATENCY as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_TAIL { - &Self::EXT_TAIL as *const _ as *const c_void - } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { - &Self::EXT_VOICE_INFO as *const _ as *const c_void - } else { - std::ptr::null() - } + Self::wrap(host, "clap_host::get_extension", |_| { + if extension_id.is_null() { + anyhow::bail!("Null extension ID"); + } + + let extension_id_cstr = unsafe { CStr::from_ptr(extension_id) }; + let extension_ptr = if extension_id_cstr == CLAP_EXT_AUDIO_PORTS { + &Self::EXT_AUDIO_PORTS as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_NOTE_PORTS { + &Self::EXT_NOTE_PORTS as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PRESET_LOAD { + &Self::EXT_PRESET_LOAD as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_PARAMS { + &Self::EXT_PARAMS as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_STATE { + &Self::EXT_STATE as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_THREAD_CHECK { + &Self::EXT_THREAD_CHECK as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_THREAD_POOL { + &Self::EXT_THREAD_POOL as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_LATENCY { + &Self::EXT_LATENCY as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_TAIL { + &Self::EXT_TAIL as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { + &Self::EXT_VOICE_INFO as *const _ as *const c_void + } else { + std::ptr::null() + }; + + Ok(extension_ptr) + }) + .unwrap_or_default() } unsafe extern "C" fn clap_request_restart(host: *const clap_host) { @@ -383,9 +411,10 @@ impl PluginShared { } if flags & !CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { - if this.status() > PluginStatus::Activated { - anyhow::bail!("Called while the plugin is activate"); - } + anyhow::ensure!( + this.status() <= PluginStatus::Activated, + "Called while the plugin is active" + ); this.callback_sender.send(CallbackEvent::AudioPortsRescanAll).unwrap(); } @@ -413,9 +442,10 @@ impl PluginShared { } if flags & CLAP_NOTE_PORTS_RESCAN_ALL != 0 { - if this.status() > PluginStatus::Activated { - anyhow::bail!("Called while the plugin is activate"); - } + anyhow::ensure!( + this.status() <= PluginStatus::Activated, + "Called while the plugin is active" + ); this.callback_sender.send(CallbackEvent::NotePortsRescanAll).unwrap(); } @@ -595,10 +625,7 @@ impl PluginShared { ); let extension = this.get_extension::().unwrap(); - (0..num_tasks).into_par_iter().for_each(|index| { - extension.exec(index); - }); - + (0..num_tasks).into_par_iter().for_each(|index| extension.exec(index)); Ok(true) }) .unwrap_or(false) diff --git a/src/plugin/library.rs b/src/plugin/library.rs index 728d4f4..b90102f 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -2,8 +2,8 @@ use super::instance::Plugin; use super::preset_discovery::PresetDiscoveryFactory; +use super::util::{self, clap_call}; use crate::plugin::instance::PluginShared; -use crate::util::{self, clap_call}; use anyhow::{Context, Result}; use clap_sys::entry::clap_plugin_entry; use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory}; diff --git a/src/plugin/preset_discovery.rs b/src/plugin/preset_discovery.rs index dbbf981..9ceb8aa 100644 --- a/src/plugin/preset_discovery.rs +++ b/src/plugin/preset_discovery.rs @@ -1,13 +1,14 @@ //! An abstraction for the preset discovery factory. +use super::library::PluginLibrary; +use super::util::{self, clap_call}; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::{clap_preset_discovery_factory, clap_preset_discovery_provider_descriptor}; +use clap_sys::timestamp::{CLAP_TIMESTAMP_UNKNOWN, clap_timestamp}; use clap_sys::version::{clap_version, clap_version_is_compatible}; use std::collections::HashSet; use std::ptr::NonNull; - -use super::library::PluginLibrary; -use crate::util::{self, clap_call}; +use time::OffsetDateTime; mod indexer; mod metadata_receiver; @@ -50,7 +51,14 @@ pub struct ProviderMetadata { impl ProviderMetadata { /// Parse the metadata from a `clap_preset_discovery_provider_descriptor`. - pub fn from_descriptor(descriptor: &clap_preset_discovery_provider_descriptor) -> Result { + pub unsafe fn from_descriptor(descriptor: *const clap_preset_discovery_provider_descriptor) -> Result { + anyhow::ensure!( + !descriptor.is_null(), + "The preset discovery provider descriptor is a null pointer." + ); + + let descriptor = unsafe { &*descriptor }; + Ok(ProviderMetadata { version: ( descriptor.clap_version.major, @@ -113,7 +121,7 @@ impl<'lib> PresetDiscoveryFactory<'lib> { ); } - metadata.push(ProviderMetadata::from_descriptor(unsafe { &*descriptor })?); + metadata.push(unsafe { ProviderMetadata::from_descriptor(descriptor)? }); } // As a sanity check we'll make sure there are no duplicate IDs in here @@ -144,3 +152,18 @@ impl<'lib> PresetDiscoveryFactory<'lib> { Provider::new(self, &metadata.id) } } + +/// Convert a `clap_timestamp` to an `Option`. A value of `CLAP_TIMESTAMP_UNKNOWN` +/// gets translated to `None`. +pub fn parse_timestamp(timestamp: clap_timestamp) -> Result> { + let parsed = if timestamp == CLAP_TIMESTAMP_UNKNOWN { + None + } else { + Some( + OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128 * 1_000_000) + .map_err(|_| anyhow::anyhow!("Could not parse the timestamp."))?, + ) + }; + + Ok(parsed) +} diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index e3489cc..999fe5a 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -1,21 +1,18 @@ //! The indexer abstraction for a CLAP plugin's preset discovery factory. During initialization the //! plugin fills this object with its supported locations, file types, and sound packs. -use crate::util::{self, check_null_ptr, validator_version}; +use crate::panic::fail_test; +use crate::plugin::preset_discovery::parse_timestamp; +use crate::plugin::util::{self, object_tracker, validator_version}; use anyhow::{Context, Result}; -use clap_sys::factory::preset_discovery::{ - CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, - CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, CLAP_PRESET_DISCOVERY_LOCATION_FILE, CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN, - clap_preset_discovery_filetype, clap_preset_discovery_indexer, clap_preset_discovery_location, - clap_preset_discovery_location_kind, clap_preset_discovery_soundpack, -}; +use clap_sys::factory::preset_discovery::*; use clap_sys::version::CLAP_VERSION; use serde::Serialize; -use std::cell::RefCell; use std::ffi::{CStr, CString, c_char, c_void}; use std::fmt::Display; use std::path::Path; use std::pin::Pin; +use std::sync::Mutex; use std::thread::ThreadId; use time::OffsetDateTime; @@ -24,13 +21,9 @@ pub struct Indexer { /// The thread ID for the thread this object was created on. This object is not thread-safe, so /// we'll assert that all callbacks are made from this thread. expected_thread_id: ThreadId, - /// A description of the first error encountered by this `Indexer`, if any. This is used to - /// store thread safety errors and other errors as the result of callbacks. In those cases we - /// can only handle the error after the callback has been mode. - callback_error: RefCell>, /// The data written to this object by the plugin. - results: RefCell, + result: Mutex>, /// The vtable that's passed to the provider. The `indexer_data` field is populated with a /// pointer to this object. @@ -62,7 +55,10 @@ pub struct FileType { impl FileType { /// Parse a `clap_preset_discovery_fileType`, returning an error if the data is not valid. - pub fn from_descriptor(descriptor: &clap_preset_discovery_filetype) -> Result { + pub unsafe fn from_descriptor(descriptor: *const clap_preset_discovery_filetype) -> Result { + anyhow::ensure!(!descriptor.is_null(), "Filetype is null"); + let descriptor = unsafe { &*descriptor }; + let file_type = FileType { name: unsafe { util::cstr_ptr_to_mandatory_string(descriptor.name) } .context("Error parsing the file extension's 'name' field")?, @@ -146,7 +142,10 @@ impl Display for Flags { impl Location { /// Parse a `clap_preset_discovery_location`, returning an error if the data is not valid. - pub fn from_descriptor(descriptor: &clap_preset_discovery_location) -> Result { + pub unsafe fn from_descriptor(descriptor: *const clap_preset_discovery_location) -> Result { + anyhow::ensure!(!descriptor.is_null(), "Location is null"); + let descriptor = unsafe { &*descriptor }; + Ok(Location { flags: Flags { is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, @@ -294,7 +293,10 @@ pub struct Soundpack { impl Soundpack { /// Parse a `clap_preset_discovery_soundpack`, returning an error if the data is not valid. - pub fn from_descriptor(descriptor: &clap_preset_discovery_soundpack) -> Result { + pub unsafe fn from_descriptor(descriptor: *const clap_preset_discovery_soundpack) -> Result { + anyhow::ensure!(!descriptor.is_null(), "Soundpack is null"); + let descriptor = unsafe { &*descriptor }; + Ok(Soundpack { flags: Flags { is_factory_content: (descriptor.flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, @@ -315,7 +317,7 @@ impl Soundpack { .context("Error parsing the soundpack's 'vendor' field")?, image_path: unsafe { util::cstr_ptr_to_optional_string(descriptor.image_path) } .context("Error parsing the soundpack's 'image_path' field")?, - release_timestamp: util::parse_timestamp(descriptor.release_timestamp) + release_timestamp: parse_timestamp(descriptor.release_timestamp) .context("Error parsing the soundpack's 'release_timestamp' field")?, }) } @@ -323,22 +325,7 @@ impl Soundpack { impl Drop for Indexer { fn drop(&mut self) { - // The results will have been moved out of `self.results` when initializing the provider, so - // if this does contain values then the plugin did something shady - let results = self.results.borrow(); - if !results.file_types.is_empty() || !results.locations.is_empty() || !results.soundpacks.is_empty() { - log::warn!( - "The plugin declared more file types, locations, or soundpacks after its initialization. This is \ - invalid behavior, but there is currently no test to check for this." - ) - } - - if let Some(error) = self.callback_error.borrow_mut().take() { - log::error!( - "The validator's 'clap_preset_indexer' has detected an error during a callback that is going to be \ - thrown away. This is a clap-validator bug. The error message is: {error}" - ) - } + object_tracker::untrack(&self.clap_preset_discovery_indexer); } } @@ -346,9 +333,7 @@ impl Indexer { pub fn new() -> Pin> { let mut indexer = Box::pin(Self { expected_thread_id: std::thread::current().id(), - callback_error: RefCell::new(None), - - results: RefCell::default(), + result: Mutex::new(Ok(IndexerResults::default())), clap_preset_discovery_indexer: clap_preset_discovery_indexer { clap_version: CLAP_VERSION, @@ -365,8 +350,8 @@ impl Indexer { }, }); + object_tracker::track(&indexer.clap_preset_discovery_indexer); indexer.clap_preset_discovery_indexer.indexer_data = &*indexer as *const Self as *mut c_void; - indexer } @@ -377,130 +362,138 @@ impl Indexer { } /// Get the values written to this indexer by the plugin during the - /// `clap_preset_discovery_provider::init()` call. Returns any error that would be returned by - /// [`callback_error_check()`][Self::callback_error_check()]. + /// `clap_preset_discovery_provider::init()` call. This also checks for errors that + /// happened during the indexer callbacks. /// - /// This moves the values out of this object. - pub fn results(&self) -> Result { - self.callback_error_check()?; - - Ok(std::mem::take(&mut self.results.borrow_mut())) + /// This can only be called once. + pub fn finish(&self) -> Result { + std::mem::replace( + &mut *self.result.lock().unwrap(), + Err(anyhow::anyhow!("Indexer already finished")), + ) } - /// Check whether errors happened during the plugin's callbacks. Returns the first error if - /// there were any. Automatically called when calling [`results()`][Self::results()]. If there - /// are errors and this function is not called before the object is destroyed, an error will be - /// logged. - pub fn callback_error_check(&self) -> Result<()> { - match self.callback_error.borrow_mut().take() { - Some(err) => anyhow::bail!(err), - None => Ok(()), + #[track_caller] + fn wrap( + indexer: *const clap_preset_discovery_indexer, + function_name: &str, + f: impl FnOnce(&Self) -> Result, + ) -> Option { + log::trace!("'{}' was called by the plugin", function_name); + + let state = unsafe { + if indexer.is_null() || (*indexer).indexer_data.is_null() { + fail_test!( + "'{}' was called with a null 'clap_preset_discovery_indexer' pointer", + function_name + ); + } + + &*((*indexer).indexer_data as *const Self) + }; + + match f(state) { + Ok(result) => Some(result), + Err(error) => { + let mut guard = state.result.lock().unwrap(); + if guard.is_ok() { + *guard = Err(error.context(function_name.to_string())); + } + + None + } } } /// Checks that this function is called from the same thread the indexer was created on. If it /// is not, then an error indicating this can be retrieved using - /// [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread safety errors + /// [`check_errors()`][Self::check_errors()]. Subsequent thread safety errors /// will not overwrite earlier ones. - fn assert_same_thread(&self, function_name: &str) { + fn assert_same_thread(&self) -> Result<()> { let current_thread_id = std::thread::current().id(); - if current_thread_id != self.expected_thread_id { - self.set_callback_error(format!( - "'{}' may only be called from the same thread the 'clap_preset_indexer' was created on (thread {:?}), \ - but it was called from thread {:?}", - function_name, self.expected_thread_id, current_thread_id - )); - } - } + anyhow::ensure!( + current_thread_id == self.expected_thread_id, + "A 'clap_preset_indexer::*' method may only be called from the same thread the 'clap_preset_indexer' was \ + created on (thread {:?}), but it was called from thread {:?}", + self.expected_thread_id, + current_thread_id + ); - /// Set the callback error field if it does not already contain a value. Earlier errors are not - /// overwritten. - fn set_callback_error(&self, error: impl Into) { - let mut callback_error = self.callback_error.borrow_mut(); - if callback_error.is_none() { - *callback_error = Some(error.into()); - } + Ok(()) } unsafe extern "C" fn declare_filetype( indexer: *const clap_preset_discovery_indexer, filetype: *const clap_preset_discovery_filetype, ) -> bool { - check_null_ptr!(indexer, (*indexer).indexer_data, filetype); - let this = unsafe { &*((*indexer).indexer_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_indexer::declare_filetype()"); - match FileType::from_descriptor(unsafe { &*filetype }) { - Ok(file_type) => { - this.results.borrow_mut().file_types.push(file_type); - - true - } - Err(err) => { - this.set_callback_error(format!( - "Error in 'clap_preset_discovery_indexer::declare_filetype()' call: {err:#}" - )); - - false - } - } + Self::wrap(indexer, "clap_preset_discovery_indexer::declare_filetype", |this| { + this.assert_same_thread()?; + + let mut results = this.result.lock().unwrap(); + let Ok(results) = results.as_mut() else { + // The indexer has already been finished, or an error has occurred + // If the error has already occurred, we wont overwrite it + anyhow::bail!("Attempt to add to the indexer after the 'clap_preset_discovery_factory::init' call"); + }; + + results.file_types.push(unsafe { FileType::from_descriptor(filetype)? }); + Ok(true) + }) + .unwrap_or(false) } unsafe extern "C" fn declare_location( indexer: *const clap_preset_discovery_indexer, location: *const clap_preset_discovery_location, ) -> bool { - check_null_ptr!(indexer, (*indexer).indexer_data, location); - let this = unsafe { &*((*indexer).indexer_data as *const Self) }; + Self::wrap(indexer, "clap_preset_discovery_indexer::declare_location", |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_indexer::declare_location()"); - match Location::from_descriptor(unsafe { &*location }) { - Ok(location) => { - this.results.borrow_mut().locations.push(location); + let mut results = this.result.lock().unwrap(); + let Ok(results) = results.as_mut() else { + // Same as above + anyhow::bail!("Attempt to add to the indexer after the 'clap_preset_discovery_factory::init' call"); + }; - true - } - Err(err) => { - this.set_callback_error(format!( - "Error in 'clap_preset_discovery_indexer::declare_location()' call: {err:#}" - )); - - false - } - } + results.locations.push(unsafe { Location::from_descriptor(location)? }); + Ok(true) + }) + .unwrap_or(false) } unsafe extern "C" fn declare_soundpack( indexer: *const clap_preset_discovery_indexer, soundpack: *const clap_preset_discovery_soundpack, ) -> bool { - check_null_ptr!(indexer, (*indexer).indexer_data, soundpack); - let this = unsafe { &*((*indexer).indexer_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_indexer::declare_soundpack()"); - match Soundpack::from_descriptor(unsafe { &*soundpack }) { - Ok(soundpack) => { - this.results.borrow_mut().soundpacks.push(soundpack); - - true - } - Err(err) => { - this.set_callback_error(format!( - "Error in 'clap_preset_discovery_indexer::declare_soundpack()' call: {err:#}" - )); - - false - } - } + Self::wrap(indexer, "clap_preset_discovery_indexer::declare_soundpack", |this| { + this.assert_same_thread()?; + + let mut results = this.result.lock().unwrap(); + let Ok(results) = results.as_mut() else { + // Same as above + anyhow::bail!("Attempt to add to the indexer after the 'clap_preset_discovery_factory::init' call"); + }; + + results + .soundpacks + .push(unsafe { Soundpack::from_descriptor(soundpack)? }); + Ok(true) + }) + .unwrap_or(false) } unsafe extern "C" fn get_extension( indexer: *const clap_preset_discovery_indexer, extension_id: *const c_char, ) -> *const c_void { - check_null_ptr!(indexer, (*indexer).indexer_data, extension_id); + Self::wrap(indexer, "clap_preset_discovery_indexer::get_extension", |_| { + if extension_id.is_null() { + anyhow::bail!("Null extension ID"); + } - // There are currently no extensions for the preset discovery factory - std::ptr::null() + // There are currently no extensions for the preset discovery factory + Ok(std::ptr::null()) + }) + .unwrap_or_default() } } diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index 36d03e3..f404c56 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -3,12 +3,11 @@ //! one or more presets to. use super::{Flags, LocationValue}; -use crate::util::{self, check_null_ptr}; +use crate::panic::fail_test; +use crate::plugin::preset_discovery::parse_timestamp; +use crate::plugin::util; use anyhow::{Context, Result}; -use clap_sys::factory::preset_discovery::{ - CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT, CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT, CLAP_PRESET_DISCOVERY_IS_FAVORITE, - CLAP_PRESET_DISCOVERY_IS_USER_CONTENT, clap_preset_discovery_metadata_receiver, -}; +use clap_sys::factory::preset_discovery::*; use clap_sys::timestamp::clap_timestamp; use clap_sys::universal_plugin_id::clap_universal_plugin_id; use serde::Serialize; @@ -17,6 +16,7 @@ use std::collections::BTreeMap; use std::ffi::{c_char, c_void}; use std::fmt::Display; use std::pin::Pin; +use std::sync::Mutex; use std::thread::ThreadId; use time::OffsetDateTime; @@ -34,7 +34,7 @@ use time::OffsetDateTime; /// /// IO errors returned by the plugin are treated as hard errors for now. #[derive(Debug)] -pub struct MetadataReceiver<'a> { +pub struct MetadataReceiver { /// The thread ID for the thread this object was created on. This object is not thread-safe, so /// we'll assert that all callbacks are made from this thread. expected_thread_id: ThreadId, @@ -42,18 +42,16 @@ pub struct MetadataReceiver<'a> { /// The location this metadata receiver was created for. If this is a single-file preset and a /// name has not been explicitly set, then the preset's name becomes the file name including the /// file extensions. - location: &'a LocationValue, + location: LocationValue, /// The crawled location's flags. This is used as a fallback for the preset flags if the /// provider does not explicitly set flags for a preset. location_flags: Flags, + /// See this object's docstring. If an error occurs, then the error is written here immediately. /// If the object is dropped and all presets have been written to `pending_presets` without any /// errors occurring, then this will contain a [`PresetFile`] describing the preset(s) added by /// the plugin. - /// - /// Stored in a `RefCell` in the off chance that the plugin doesn't use this in a thread safe - /// way. - result: RefCell<&'a mut Option>>, + result: Mutex>>, /// The data for the next preset. This is `None` until the plugin starts calling one of the data /// setter functions. After that point the preset's data is filled in piece by piece like in a @@ -246,38 +244,15 @@ impl Preset { } } -impl Drop for MetadataReceiver<'_> { - fn drop(&mut self) { - // If the plugin declared a(nother) preset file, then this will be added to `self.result` - // now. If an error occurred at any point, then the result will instead contain that error. - self.maybe_write_preset(); - } -} - -impl<'a> MetadataReceiver<'a> { - /// Create a new metadata receiver that will write the results to the provided `result`. This is - /// needed because the actual writing happens when this object is dropped. After that point - /// `result` is either: - /// - /// - `None` if the plugin didn't write any presets. - /// - `Some(Err(err))` if an error occurred while declaring presets. - /// - `Some(Ok(preset_file))` if the plugin declared one or more presets successfully. - pub fn new( - result: &'a mut Option>, - location: &'a LocationValue, - location_flags: Flags, - ) -> Pin> { - // In the event that the caller reuses result objects this needs to be initialized to a - // non-error value, since if it does contain an error at some point then nothing will be - // written to it in the `Drop` implementation - *result = None; - +impl MetadataReceiver { + /// Create a new metadata receiver. + pub fn new(location: LocationValue, location_flags: Flags) -> Pin> { let mut metadata_receiver = Box::pin(Self { expected_thread_id: std::thread::current().id(), location, location_flags, - result: RefCell::new(result), + result: Mutex::new(Ok(None)), next_preset_data: RefCell::new(None), next_load_key: RefCell::new(None), @@ -299,7 +274,6 @@ impl<'a> MetadataReceiver<'a> { metadata_receiver.clap_preset_discovery_metadata_receiver.receiver_data = &*metadata_receiver as *const Self as *mut c_void; - metadata_receiver } @@ -311,27 +285,55 @@ impl<'a> MetadataReceiver<'a> { &self.clap_preset_discovery_metadata_receiver } - /// Checks that this function is called from the same thread the indexer was created on. If it - /// is not, then an error indicating this can be retrieved using - /// [`callback_error_check()`][Self::callback_error_check()]. Subsequent thread safety errors - /// will not overwrite earlier ones. - fn assert_same_thread(&self, function_name: &str) { + /// Finish the preset declaration process and return the result. This finishes any pending + /// presets and returns the [`PresetFile`]. + pub fn finish(self: Pin>) -> Result> { + self.flush_preset()?; + std::mem::replace(&mut *self.result.lock().unwrap(), Ok(None)) + } + + /// Checks that this function is called from the same thread the indexer was created on. + fn assert_same_thread(&self) -> Result<()> { let current_thread_id = std::thread::current().id(); - if current_thread_id != self.expected_thread_id { - self.set_callback_error(format!( - "'{}' may only be called from the same thread the 'clap_preset_indexer' was created on (thread {:?}), \ - but it was called from thread {:?}", - function_name, self.expected_thread_id, current_thread_id - )); - } + anyhow::ensure!( + current_thread_id == self.expected_thread_id, + "'clap_preset_discovery_metadata_receiver' methods may only be called from the same thread the \ + 'clap_preset_indexer' was created on (thread {:?}), but it was called from thread {:?}", + self.expected_thread_id, + current_thread_id + ); + Ok(()) } - /// Write an error to the result field if it did not already contain a value. Earlier errors are - /// not overwritten. - fn set_callback_error(&self, error: impl Into) { - match &mut *self.result.borrow_mut() { - Some(Err(_)) => (), - result => **result = Some(Err(anyhow::anyhow!(error.into()))), + #[track_caller] + fn wrap( + receiver: *const clap_preset_discovery_metadata_receiver, + function_name: &str, + f: impl FnOnce(&Self) -> Result, + ) -> Option { + log::trace!("'{}' was called by the plugin", function_name); + + let state = unsafe { + if receiver.is_null() || (*receiver).receiver_data.is_null() { + fail_test!( + "'{}' was called with a null 'clap_preset_discovery_metadata_receiver' pointer", + function_name + ); + } + + &*((*receiver).receiver_data as *const Self) + }; + + match f(state) { + Ok(result) => Some(result), + Err(error) => { + let mut guard = state.result.lock().unwrap(); + if guard.is_ok() { + *guard = Err(error.context(function_name.to_string())); + } + + None + } } } @@ -340,37 +342,33 @@ impl<'a> MetadataReceiver<'a> { /// depending on whether a load key was passed to the `begin_preset()` function. If multiple /// presets are written for a single-file preset, then an error will be written to the result. /// If an error was previously written, then it will not be overwritten. - fn maybe_write_preset(&self) { - if let Some(partial_preset) = self.next_preset_data.borrow_mut().take() { - match ( - &mut *self.result.borrow_mut(), - partial_preset.finalize(&self.location_flags), - // The `take()` is important here to catch the situation where the plugin adds a - // load key on the first `begin_preset()` call but not in subsequent calls - self.next_load_key.borrow_mut().take(), - ) { - // If an error was already produced then it should be preserved, and new errors - // should be written to the Result if there wasn't already one - (Some(Err(_)), _, _) => (), - (_, Err(err), _) => self.set_callback_error(format!("{err:#}")), - (result @ None, Ok(preset), None) => **result = Some(Ok(PresetFile::Single(preset))), - (result @ None, Ok(preset), Some(load_key)) => { - let mut presets = BTreeMap::new(); - presets.insert(load_key, preset); - - **result = Some(Ok(PresetFile::Container(presets))); - } - (Some(Ok(PresetFile::Container(presets))), Ok(preset), Some(load_key)) => { - presets.insert(load_key, preset); - } - // These situations have been caught in `begin_preset()`. If a second preset has - // been started when the first preset didn't have a load key this is a validator - // bug. - (Some(Ok(PresetFile::Single(_))), Ok(_), _) | (Some(Ok(PresetFile::Container(_))), Ok(_), None) => { - unreachable!("Inconsistent state in the validator's metadata receiver found.") - } + fn flush_preset(&self) -> Result<()> { + let Some(partial_preset) = self.next_preset_data.borrow_mut().take() else { + return Ok(()); // No preset to flush + }; + + let mut result = self.result.lock().unwrap(); + let Ok(result) = result.as_mut() else { + return Ok(()); // An error was already produced, no one cares + }; + + let preset = partial_preset.finalize(&self.location_flags)?; + let load_key = self.next_load_key.borrow_mut().take(); + + match (result, load_key) { + (result @ None, None) => *result = Some(PresetFile::Single(preset)), + (result @ None, Some(load_key)) => { + let mut presets = BTreeMap::new(); + presets.insert(load_key, preset); + *result = Some(PresetFile::Container(presets)); + } + (Some(PresetFile::Container(presets)), Some(load_key)) => { + presets.insert(load_key, preset); } + _ => unreachable!(), } + + Ok(()) } unsafe extern "C" fn on_error( @@ -378,22 +376,20 @@ impl<'a> MetadataReceiver<'a> { os_error: i32, error_message: *const c_char, ) { - // We'll have a dedicated error message for a missing `error_message` - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_metadata_receiver::on_error()"); - - let error_message = unsafe { util::cstr_ptr_to_mandatory_string(error_message) } - .context("'clap_preset_discovery_metadata_receiver::on_error()' called with an invalid error message"); - match error_message { - Ok(error_message) => this.set_callback_error(format!( - "'clap_preset_discovery_metadata_receiver::on_error()' called for OS error code {os_error} with the \ - following error message: {error_message}" - )), - // This would be quite ironic - Err(err) => this.set_callback_error(format!("{err:#}")), - } + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::on_error", + |this| -> Result<()> { + this.assert_same_thread()?; + + let error_message = + unsafe { util::cstr_ptr_to_mandatory_string(error_message) }.context("Error message is invalid")?; + + anyhow::bail!( + "Load error occurred: OS error code {os_error} with the following error message: {error_message}" + ); + }, + ); } unsafe extern "C" fn begin_preset( @@ -401,74 +397,47 @@ impl<'a> MetadataReceiver<'a> { name: *const c_char, load_key: *const c_char, ) -> bool { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_metadata_receiver::begin_preset()"); - - let name = unsafe { util::cstr_ptr_to_optional_string(name) } - .context("'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid name parameter"); - let load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) }.context( - "'clap_preset_discovery_metadata_receiver::begin_preset()' called with an invalid load_key parameter", - ); - match (name, load_key) { - (Ok(name), Ok(load_key)) => { - // We'll check for some errorous situations first. The `result` borrow needs to be - // dropped before calling `maybe_write_preset()` as it will try to borrow it mutably - { - let result = this.result.borrow(); - let error_message = match (&*result, &load_key) { - // If there was an error then just immediately exit since nothing will change that - (Some(Err(_)), _) => return false, - (Some(Ok(PresetFile::Single(_))), None) => Some( - "calling 'begin_preset()' a second time for a non-container preset file with no load key \ - is not allowed.", - ), - (Some(Ok(PresetFile::Single(_))), Some(_)) => Some( - "'begin_preset()' was called without a load key for the first time, and with a load key \ - the second time. This is invalid behavior.", - ), - (Some(Ok(PresetFile::Container(_))), None) => Some( - "'begin_preset()' was called with a load key for the first time, and without a load key \ - the second time. This is invalid behavior.", - ), - // If this is the first call and there are no errors then everything's fine - (None, _) | (Some(Ok(PresetFile::Container(_))), Some(_)) => None, - }; - - if let Some(error_message) = error_message { - this.set_callback_error(format!( - "Error in 'clap_preset_discovery_metadata_receiver::begin_preset()' call: {error_message}" - )); - return false; - } + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::begin_preset", + |this| { + this.assert_same_thread()?; + + let name = unsafe { util::cstr_ptr_to_optional_string(name) }.context("Name argument is invalid")?; + let load_key = + unsafe { util::cstr_ptr_to_optional_string(load_key) }.context("Load key argument is invalid")?; + + let result = this.result.lock().unwrap(); + match (&*result, &load_key) { + (Err(_), _) => return Ok(false), + (Ok(Some(PresetFile::Single(_))), _) => anyhow::bail!( + "Calling 'begin_preset()' a second time for a non-container (no load key) preset file is not \ + allowed" + ), + (Ok(Some(PresetFile::Container(_))), None) => anyhow::bail!( + "'begin_preset()' was called with a load key for the first time, and without a load key the \ + second time. This is invalid behavior" + ), + _ => {} } // Container presets have a load key, single-preset files don't have a load key. The // name field is mandatory for container presets, and optional for non-container // presets. If it's not specified we'll use the file name instead. let preset_name = match (name, &load_key) { - (None, None) => PresetName::Filename(match this.location.file_name() { - Ok(file_name) => file_name, - Err(err) => { - this.set_callback_error(format!( - "Could not derive a file name from {}: {:#}", - this.location, err - )); - return false; - } - }), (Some(name), _) => PresetName::Explicit(name), - (None, Some(_)) => { - this.set_callback_error("Container presets must specify a preset name.".to_string()); - return false; - } + (None, Some(_)) => anyhow::bail!("Container presets must specify a preset name"), + (None, None) => PresetName::Filename( + this.location + .file_name() + .with_context(|| format!("Could not derive a file name from {}", this.location))?, + ), }; // If this is a subsequent `begin_preset()` call for a container preset, then the // old preset is written to `self.result` before starting a new one. if load_key.is_some() { - this.maybe_write_preset(); + this.flush_preset()?; } // This starts the declaration of a new preset. The methods below this write to this @@ -477,41 +446,31 @@ impl<'a> MetadataReceiver<'a> { *this.next_load_key.borrow_mut() = load_key; *this.next_preset_data.borrow_mut() = Some(PartialPreset::new(preset_name)); - true - } - (Err(err), _) | (_, Err(err)) => { - this.set_callback_error(format!("{err:#}")); - - false - } - } + Ok(true) + }, + ) + .unwrap_or(false) } unsafe extern "C" fn add_plugin_id( receiver: *const clap_preset_discovery_metadata_receiver, plugin_id: *const clap_universal_plugin_id, ) { - check_null_ptr!(receiver, (*receiver).receiver_data, plugin_id); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::add_plugin_id", + |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_plugin_id()"); + let abi = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).abi) } + .context("'plugin_id.abi' is invalid")?; + let id = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).id) } + .context("'plugin_id.id' is invalid")?; - let abi = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).abi) } - .context("'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid abi field"); - let id = unsafe { util::cstr_ptr_to_mandatory_string((*plugin_id).id) } - .context("'clap_preset_discovery_metadata_receiver::add_plugin_id()' called with an invalid id field"); - match (abi, id) { - (Ok(abi), Ok(id)) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; if abi == "clap" { @@ -521,137 +480,112 @@ impl<'a> MetadataReceiver<'a> { }); } else if abi.trim().eq_ignore_ascii_case("clap") { // Let's just assume noone comes up with a painfully sarcastic 'ClAp' standard - this.set_callback_error(format!( - "'{abi}' was provided as an ABI argument to \ - 'clap_preset_discovery_metadata_receiver::add_plugin_id()'. This is probably a typo. The \ - expected value is 'clap' in all lowercase." - )); + anyhow::bail!( + "'{abi}' was provided as an ABI argument. This is probably a typo. The expected value is \ + 'clap' in all lowercase." + ); } else { next_preset_data.plugin_ids.push(PluginId { abi: PluginAbi::Other(abi), id, }); } - } - (Err(err), _) | (_, Err(err)) => this.set_callback_error(format!("{err:#}")), - } + + Ok(()) + }, + ); } unsafe extern "C" fn set_soundpack_id( receiver: *const clap_preset_discovery_metadata_receiver, soundpack_id: *const c_char, ) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::set_soundpack_id", + |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_soundpack_id()"); + let soundpack_id = + unsafe { util::cstr_ptr_to_mandatory_string(soundpack_id) }.context("Soundpack ID is invalid")?; - let soundpack_id = unsafe { util::cstr_ptr_to_mandatory_string(soundpack_id) } - .context("'clap_preset_discovery_metadata_receiver::set_soundpack_id()' called with an invalid parameter"); - match soundpack_id { - Ok(soundpack_id) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_soundpack_id()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.soundpack_id = Some(soundpack_id); - } - Err(err) => this.set_callback_error(format!("{err:#}")), - } + Ok(()) + }, + ); } unsafe extern "C" fn set_flags(receiver: *const clap_preset_discovery_metadata_receiver, flags: u32) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_flags()"); - - let mut next_preset_data = this.next_preset_data.borrow_mut(); - let next_preset_data = match &mut *next_preset_data { - Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_flags()' with no preceding 'begin_preset()' call. \ - This is not valid.", - ); - return; - } - }; - - next_preset_data.flags = Some(Flags { - is_factory_content: (flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, - is_user_content: (flags & CLAP_PRESET_DISCOVERY_IS_USER_CONTENT) != 0, - is_demo_content: (flags & CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT) != 0, - is_favorite: (flags & CLAP_PRESET_DISCOVERY_IS_FAVORITE) != 0, + Self::wrap(receiver, "clap_preset_discovery_metadata_receiver::set_flags", |this| { + this.assert_same_thread()?; + + let mut next_preset_data = this.next_preset_data.borrow_mut(); + let next_preset_data = match &mut *next_preset_data { + Some(next_preset_data) => next_preset_data, + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), + }; + + next_preset_data.flags = Some(Flags { + is_factory_content: (flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) != 0, + is_user_content: (flags & CLAP_PRESET_DISCOVERY_IS_USER_CONTENT) != 0, + is_demo_content: (flags & CLAP_PRESET_DISCOVERY_IS_DEMO_CONTENT) != 0, + is_favorite: (flags & CLAP_PRESET_DISCOVERY_IS_FAVORITE) != 0, + }); + + Ok(()) }); } unsafe extern "C" fn add_creator(receiver: *const clap_preset_discovery_metadata_receiver, creator: *const c_char) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::add_creator", + |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_creator()"); + let creator = unsafe { util::cstr_ptr_to_mandatory_string(creator) }.context("Creator is invalid")?; - let creator = unsafe { util::cstr_ptr_to_mandatory_string(creator) } - .context("'clap_preset_discovery_metadata_receiver::set_creator()' called with an invalid parameter"); - - match creator { - Ok(creator) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_creator()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.creators.push(creator); - } - Err(err) => this.set_callback_error(format!("{err:#}")), - } + Ok(()) + }, + ); } unsafe extern "C" fn set_description( receiver: *const clap_preset_discovery_metadata_receiver, description: *const c_char, ) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::set_description", + |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_description()"); + let description = + unsafe { util::cstr_ptr_to_mandatory_string(description) }.context("Description is invalid")?; - let description = unsafe { util::cstr_ptr_to_mandatory_string(description) } - .context("'clap_preset_discovery_metadata_receiver::set_description()' called with an invalid parameter"); - match description { - Ok(description) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_description()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.description = Some(description); - } - Err(err) => this.set_callback_error(format!("{err:#}")), - } + Ok(()) + }, + ); } unsafe extern "C" fn set_timestamps( @@ -659,72 +593,53 @@ impl<'a> MetadataReceiver<'a> { creation_time: clap_timestamp, modification_time: clap_timestamp, ) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_metadata_receiver::set_timestamps()"); + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::set_timestamps", + |this| { + this.assert_same_thread()?; + + // These are parsed to `None` values if the timestamp is 0/CLAP_TIMESTAMP_UNKNOWN + let creation_time = parse_timestamp(creation_time).context("Creation time is invalid")?; + let modification_time = parse_timestamp(modification_time).context("Modification time is invalid")?; + + anyhow::ensure!( + creation_time.is_some() || modification_time.is_some(), + "Both arguments are set to 'CLAP_TIMESTAMP_UNKNOWN'" + ); - // These are parsed to `None` values if the timestamp is 0/CLAP_TIMESTAMP_UNKNOWN - let creation_time = util::parse_timestamp(creation_time).context( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid creation_time \ - parameter", - ); - let modification_time = util::parse_timestamp(modification_time).context( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with an invalid modification_time \ - parameter", - ); - match (creation_time, modification_time) { - // Calling the function like htis doesn't make any sense, so we'll point that out - (Ok(None), Ok(None)) => this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' called with both arguments set to \ - 'CLAP_TIMESTAMP_UNKNOWN'.", - ), - (Ok(creation_time), Ok(modification_time)) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::set_timestamps()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.creation_time = creation_time; next_preset_data.modification_time = modification_time; - } - (Err(err), _) | (_, Err(err)) => this.set_callback_error(format!("{err:#}")), - } + + Ok(()) + }, + ); } unsafe extern "C" fn add_feature(receiver: *const clap_preset_discovery_metadata_receiver, feature: *const c_char) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; - - this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_feature()"); + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::add_feature", + |this| { + this.assert_same_thread()?; + let feature = unsafe { util::cstr_ptr_to_mandatory_string(feature) }.context("Feature is invalid")?; - let feature = unsafe { util::cstr_ptr_to_mandatory_string(feature) } - .context("'clap_preset_discovery_metadata_receiver::add_feature()' called with an invalid parameter"); - match feature { - Ok(feature) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_plugin_id()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.features.push(feature); - } - Err(err) => this.set_callback_error(format!("{err:#}")), - } + Ok(()) + }, + ); } unsafe extern "C" fn add_extra_info( @@ -732,34 +647,24 @@ impl<'a> MetadataReceiver<'a> { key: *const c_char, value: *const c_char, ) { - check_null_ptr!(receiver, (*receiver).receiver_data); - let this = unsafe { &*((*receiver).receiver_data as *const Self) }; + Self::wrap( + receiver, + "clap_preset_discovery_metadata_receiver::add_extra_info", + |this| { + this.assert_same_thread()?; - this.assert_same_thread("clap_preset_discovery_metadata_receiver::add_extra_info()"); + let key = unsafe { util::cstr_ptr_to_mandatory_string(key) }.context("Key is invalid")?; + let value = unsafe { util::cstr_ptr_to_mandatory_string(value) }.context("Value is invalid")?; - let key = unsafe { util::cstr_ptr_to_mandatory_string(key) }.context( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid key parameter", - ); - let value = unsafe { util::cstr_ptr_to_mandatory_string(value) }.context( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' called with an invalid value parameter", - ); - match (key, value) { - (Ok(key), Ok(value)) => { let mut next_preset_data = this.next_preset_data.borrow_mut(); let next_preset_data = match &mut *next_preset_data { Some(next_preset_data) => next_preset_data, - None => { - this.set_callback_error( - "'clap_preset_discovery_metadata_receiver::add_extra_info()' with no preceding \ - 'begin_preset()' call. This is not valid.", - ); - return; - } + None => anyhow::bail!("No preceding 'begin_preset()' call. This is not valid."), }; next_preset_data.extra_info.insert(key, value); - } - (Err(err), _) | (_, Err(err)) => this.set_callback_error(format!("{err:#}")), - } + Ok(()) + }, + ); } } diff --git a/src/plugin/preset_discovery/provider.rs b/src/plugin/preset_discovery/provider.rs index 704c4aa..679a60b 100644 --- a/src/plugin/preset_discovery/provider.rs +++ b/src/plugin/preset_discovery/provider.rs @@ -1,5 +1,9 @@ //! A wrapper around `clap_preset_discovery_provider`. +use super::indexer::{Indexer, IndexerResults}; +use super::metadata_receiver::{MetadataReceiver, PresetFile}; +use super::{Location, LocationValue, PresetDiscoveryFactory, ProviderMetadata}; +use crate::plugin::util::clap_call; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::clap_preset_discovery_provider; use std::collections::{BTreeMap, HashSet}; @@ -9,11 +13,6 @@ use std::pin::Pin; use std::ptr::NonNull; use walkdir::WalkDir; -use super::indexer::{Indexer, IndexerResults}; -use super::metadata_receiver::{MetadataReceiver, PresetFile}; -use super::{Location, LocationValue, PresetDiscoveryFactory, ProviderMetadata}; -use crate::util::clap_call; - /// A preset discovery provider created from a preset discovery factory. The provider is initialized /// and the declared contents are read when the object is created, and the provider is destroyed /// when this object is dropped. @@ -80,9 +79,7 @@ impl<'a> Provider<'a> { ); } - // TODO: After this point the provider should not declare any more data. We don't - // currently test for this. - indexer.results().with_context(|| { + indexer.finish().with_context(|| { format!( "Errors produced during 'clap_preset_discovery_indexer' callbacks made by the provider with ID \ '{provider_id}'" @@ -106,11 +103,12 @@ impl<'a> Provider<'a> { pub fn descriptor(&self) -> Result { let provider = self.as_ptr(); let descriptor = unsafe { (*provider).desc }; + if descriptor.is_null() { anyhow::bail!("The 'desc' field on the 'clap_preset_provider' struct is a null pointer."); } - ProviderMetadata::from_descriptor(unsafe { &*descriptor }) + unsafe { ProviderMetadata::from_descriptor(descriptor) } } /// Get the raw pointer to the `clap_preset_discovery_provider` instance. @@ -136,37 +134,28 @@ impl<'a> Provider<'a> { let mut crawl = |location: LocationValue| -> Result<()> { let (location_kind, location_ptr) = location.to_raw(); - // There is no 'end of preset' kind of function in the metadata provider, so when - // the `MetadataReceiver` is dropped it may still need to write a preset file or - // emit some errors. That's why it borrows this result, and writes the output - // theere. This can happen during the drop. - let mut result = None; - { - let metadata_receiver = MetadataReceiver::new(&mut result, &location, location_flags); - - let provider = self.as_ptr(); - let success = unsafe { - clap_call! { - provider=>get_metadata( - provider, - location_kind, - location_ptr, - metadata_receiver.clap_preset_discovery_metadata_receiver_ptr() - ) - } - }; - - if !success { - // TODO: Is the plugin allowed to return false here? If it doesn't have any - // presets it should just not declare any, right? - anyhow::bail!("The preset provider returned false when fetching metadata for {location}.",); + let metadata_receiver = MetadataReceiver::new(location.clone(), location_flags); + let provider = self.as_ptr(); + let success = unsafe { + clap_call! { + provider=>get_metadata( + provider, + location_kind, + location_ptr, + metadata_receiver.clap_preset_discovery_metadata_receiver_ptr() + ) } + }; + + if !success { + anyhow::bail!("The preset provider returned false when fetching metadata for {location}.",); } - if let Some(preset_file) = result { - let preset_file = - preset_file.with_context(|| format!("Error while fetching fetching metadata for {location}"))?; + let result = metadata_receiver + .finish() + .with_context(|| format!("Error while fetching fetching metadata for {location}"))?; + if let Some(preset_file) = result { results.insert(location, preset_file); } diff --git a/src/plugin/process/buffer.rs b/src/plugin/process/buffer.rs index ad720de..720ea1d 100644 --- a/src/plugin/process/buffer.rs +++ b/src/plugin/process/buffer.rs @@ -1,15 +1,14 @@ -use crate::plugin::{ext::audio_ports::AudioPortConfig, process::ConstantMask}; +use crate::plugin::ext::audio_ports::AudioPortConfig; +use crate::plugin::process::ConstantMask; use anyhow::Result; use clap_sys::audio_buffer::*; use either::Either; use rand::Rng; use rand_pcg::Pcg32; -use std::{ - collections::HashMap, - fmt::Debug, - ops::{Deref, DerefMut}, - ptr::null_mut, -}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; +use std::ptr::null_mut; /// Audio buffers for audio processing. These contain both input and output buffers, that can be either in-place /// or out-of-place, single or double precision. @@ -162,6 +161,11 @@ impl AudioBuffers { self.clap_inputs[input].constant_mask = buffer.input_constant_mask.0; self.clap_inputs[input].latency = buffer.input_latency; } + + if let Some(output) = buffer.port().output() { + self.clap_outputs[output].constant_mask = 0; + self.clap_outputs[output].latency = 0; + } } let result = f(&self.clap_inputs, &mut self.clap_outputs); diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index bf8f292..d98caae 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -1,7 +1,8 @@ use clap_sys::events::*; -use std::{pin::Pin, sync::Mutex}; +use std::pin::Pin; +use std::sync::Mutex; -use crate::util::check_null_ptr; +use crate::panic::fail_test; /// An event queue that can be used as either an input queue or an output queue. This is always /// allocated through a `Pin>` so the pointers are stable. The `VTable` type @@ -98,7 +99,10 @@ impl EventQueue { unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { unsafe { - check_null_ptr!(list, (*list).ctx); + if list.is_null() || (*list).ctx.is_null() { + fail_test!("'clap_input_events::size' was called with a null pointer"); + } + let this = &*((*list).ctx as *const Self); this.events.lock().unwrap().len() as u32 } @@ -106,9 +110,11 @@ impl EventQueue { unsafe extern "C" fn get(list: *const clap_input_events, index: u32) -> *const clap_event_header { unsafe { - check_null_ptr!(list, (*list).ctx); - let this = &*((*list).ctx as *const Self); + if list.is_null() || (*list).ctx.is_null() { + fail_test!("'clap_input_events::get' was called with a null pointer"); + } + let this = &*((*list).ctx as *const Self); let events = this.events.lock().unwrap(); match events.get(index as usize) { Some(event) => event.header(), @@ -125,13 +131,14 @@ impl EventQueue { unsafe extern "C" fn try_push(list: *const clap_output_events, event: *const clap_event_header) -> bool { unsafe { - check_null_ptr!(list, (*list).ctx, event); - let this = &*((*list).ctx as *const Self); + if list.is_null() || (*list).ctx.is_null() || event.is_null() { + fail_test!("'clap_output_events::try_push' was called with a null pointer"); + } // The monotonicity of the plugin's event insertion order is checked as part of the output // consistency checks + let this = &*((*list).ctx as *const Self); this.events.lock().unwrap().push(Event::from_raw(event)); - true } } diff --git a/src/plugin/process/transport.rs b/src/plugin/process/transport.rs index 8e8fa0d..59355aa 100644 --- a/src/plugin/process/transport.rs +++ b/src/plugin/process/transport.rs @@ -1,4 +1,5 @@ -use clap_sys::{events::*, fixedpoint::*}; +use clap_sys::events::*; +use clap_sys::fixedpoint::*; /// The current transport state. This can be modified between process calls to simulate /// transport changes. @@ -115,7 +116,7 @@ impl TransportState { /// A constant mask for audio processing. Each bit represents whether the corresponding audio channel /// is constant (1) or not (0). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct ConstantMask(pub u64); impl ConstantMask { @@ -127,8 +128,14 @@ impl ConstantMask { self.0 & 1u64.unbounded_shl(channel) != 0 } - pub fn are_first_n_channels_constant(&self, n: u32) -> bool { + pub fn are_all_channels_constant(&self, n: u32) -> bool { let mask = (1u64.unbounded_shl(n)).wrapping_sub(1); (self.0 & mask) == mask } } + +impl std::fmt::Debug for ConstantMask { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConstantMask(0b{:064b})", self.0) + } +} diff --git a/src/plugin/util.rs b/src/plugin/util.rs new file mode 100644 index 0000000..81e67d9 --- /dev/null +++ b/src/plugin/util.rs @@ -0,0 +1,185 @@ +//! Various utility functions for the plugin host. + +use anyhow::{Context, Result}; +use std::ffi::{CStr, CString, c_char}; +use std::sync::OnceLock; + +/// Call a CLAP function. This is needed because even though none of CLAP's functions are allowed to +/// be null pointers, people will still use null pointers for some of the function arguments. This +/// also happens in the official `clap-helpers`. As such, these functions are now `Option` +/// optional function pointers in `clap-sys`. This macro asserts that the pointer is not null, and +/// prints a nicely formatted error message containing the struct and funciton name if it is. It +/// also emulates C's syntax for accessing fields struct through a pointer. Except that it uses `=>` +/// instead of `->`. Because that sounds like it would be hilarious. +macro_rules! clap_call { + { $obj_ptr:expr=>$function_name:ident($($args:expr),* $(, )?) } => { + match (*$obj_ptr).$function_name { + Some(function_ptr) => function_ptr($($args),*), + None => $crate::panic::fail_test!("'{}::{}' is a null pointer, but this is not allowed", $crate::plugin::util::type_name_of_ptr($obj_ptr), stringify!($function_name)), + } + } +} + +pub(crate) use clap_call; + +/// Similar to, [`std::any::type_name_of_val()`], but on stable Rust, and stripping away the pointer +/// part. +#[must_use] +#[doc(hidden)] +pub fn type_name_of_ptr(_ptr: *const T) -> &'static str { + std::any::type_name::() +} + +/// Convert a `*const c_char` to a `String`. Returns `Ok(None)` if the pointer is a null pointer or +/// if the string is not valid UTF-8. This only returns an error if the string contains invalid +/// UTF-8. +/// +/// # Safety +/// +/// `ptr` should point to a valid null terminated C-string. +pub unsafe fn cstr_ptr_to_string(ptr: *const c_char) -> Result> { + if ptr.is_null() { + return Ok(None); + } + + unsafe { + CStr::from_ptr(ptr) + .to_str() + .map(|str| Some(String::from(str))) + .context("Error while parsing UTF-8") + } +} + +/// The same as [`cstr_ptr_to_string()`], but it returns an error if the string is empty. +pub unsafe fn cstr_ptr_to_mandatory_string(ptr: *const c_char) -> Result { + unsafe { + match cstr_ptr_to_string(ptr)? { + Some(string) if string.is_empty() => anyhow::bail!("The string is empty."), + Some(string) => Ok(string), + None => anyhow::bail!("The string is a null pointer."), + } + } +} + +/// The same as [`cstr_ptr_to_string()`], but it treats empty strings as missing. Useful for parsing +/// optional fields from structs. +pub unsafe fn cstr_ptr_to_optional_string(ptr: *const c_char) -> Result> { + unsafe { + match cstr_ptr_to_string(ptr)? { + Some(string) if string.is_empty() => Ok(None), + x => Ok(x), + } + } +} + +/// Convert a null terminated `*const *const c_char` array to a `Vec`. Returns `None` if the +/// first pointer is a null pointer. Returns an error if any of the strings are not valid UTF-8. +/// +/// # Safety +/// +/// `ptr` should point to a valid null terminated C-string array. +pub unsafe fn cstr_array_to_vec(mut ptr: *const *const c_char) -> Result>> { + unsafe { + if ptr.is_null() { + return Ok(None); + } + + let mut strings = Vec::new(); + while !(*ptr).is_null() { + // We already checked for null pointers, so we can safely unwrap this + strings.push(cstr_ptr_to_string(*ptr)?.unwrap()); + ptr = ptr.offset(1); + } + + Ok(Some(strings)) + } +} + +/// Convert a `c_char` slice to a `String`. Returns an error if the slice did not contain a null +/// byte, or if the string is not valid UTF-8. +pub fn c_char_slice_to_string(slice: &[c_char]) -> Result { + // `from_bytes_until_nul` is still unstable, so we'll YOLO it for now by checking if the slice + // contains a null byte and then treating it as a pointer if it does + if !slice.contains(&0) { + anyhow::bail!("The string buffer does not contain a null byte.") + } + + unsafe { CStr::from_ptr(slice.as_ptr()) } + .to_str() + .context("Error while parsing UTF-8") + .map(String::from) +} + +pub fn validator_version() -> &'static CStr { + static VERSION: OnceLock = OnceLock::new(); + VERSION + .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).unwrap()) + .as_c_str() +} + +/// Utility module for tracking CLAP object lifetimes during validation. +/// This is useful for checking that the plugin calls host-provided functions with valid pointers. +pub mod object_tracker { + use anyhow::Result; + use std::any::{TypeId, type_name}; + use std::collections::HashMap; + use std::sync::RwLock; + + struct TrackStatus { + type_id: TypeId, + type_name: &'static str, + is_alive: bool, + } + + static OBJECTS: RwLock>> = RwLock::new(None); + + /// Start tracking the given object pointer. + pub fn track(obj: *const T) { + let mut objects = OBJECTS.write().unwrap(); + objects.get_or_insert_default().insert( + obj.addr(), + TrackStatus { + type_id: TypeId::of::(), + type_name: type_name::(), + is_alive: true, + }, + ); + } + + /// Stop tracking the given object pointer, any subsequent use will be considered invalid. + pub fn untrack(obj: *const T) { + let mut objects = OBJECTS.write().unwrap(); + match objects.as_mut().and_then(|x| x.get_mut(&obj.addr())) { + Some(status) if TypeId::of::() == status.type_id => status.is_alive = false, + _ => panic!( + "Untrack failed: {} at {:p} was not being tracked", + type_name::(), + obj + ), + } + } + + /// Check that the given object pointer is valid, of the correct type, and is still alive. + pub fn check(obj: *const T) -> Result<()> { + if obj.is_null() { + anyhow::bail!("null pointer to {}", type_name::()); + } + + let objects = OBJECTS.read().unwrap(); + let object = objects.as_ref().and_then(|x| x.get(&obj.addr())); + + let Some(object) = object else { + anyhow::bail!("invalid pointer to {}", type_name::()); + }; + + if object.type_id != TypeId::of::() { + anyhow::bail!("expected pointer to {}, got {}", type_name::(), object.type_name); + } + + if !object.is_alive { + anyhow::bail!("{} has expired", type_name::()); + } + + Ok(()) + } +} diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 740497a..5339984 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -141,10 +141,13 @@ impl<'a> TestCase<'a> for PluginTestCase { PluginTestCase::ProcessSleepConstantMask => String::from( "Processes random audio through the plugin with its default parameter values while setting the \ constant mask on silent blocks, and tests whether the output does not contain any non-finite or \ - subnormal values and that the plugin sets the constant mask correctly. Uses out-of-place audio \ - processing.", + subnormal values and that the plugin sets the constant mask correctly", + ), + PluginTestCase::ProcessSleepProcessStatus => String::from( + "Processes random audio through the plugin with its default parameter values while checking if the \ + output is consistent with the returned process status, and tests whether the output does not contain \ + any non-finite or subnormal values and that the plugin sets the process status correctly", ), - PluginTestCase::ProcessSleepProcessStatus => String::from("TODO: write ts"), PluginTestCase::ProcessNoteOutOfPlaceBasic => String::from( "Sends audio and random note and MIDI events to the plugin with its default parameter values and \ tests the output for consistency. Uses out-of-place audio processing.", @@ -161,7 +164,7 @@ impl<'a> TestCase<'a> for PluginTestCase { ), PluginTestCase::ProcessVaryingBlockSizes => String::from( "Processes random audio and random note events through the plugin with its default parameter values \ - while trying different maximum block sizes ranging from 1 to 32768, including non-power-of-two ones, \ + while trying different maximum block sizes ranging from 1 to 16k, including non-power-of-two ones, \ and tests whether the output does not contain any non-finite or subnormal values. Uses out-of-place \ audio processing.", ), diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 51962fa..4fdee2f 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -5,9 +5,9 @@ use crate::plugin::ext::configurable_audio_ports::ConfigurableAudioPorts; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, ProcessScope}; +use crate::plugin::util::{cstr_ptr_to_mandatory_string, cstr_ptr_to_string}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng, random_layout_requests}; -use crate::util::{cstr_ptr_to_mandatory_string, cstr_ptr_to_string}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::clap_audio_port_info; @@ -230,9 +230,7 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) })?; } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -333,9 +331,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: })?; } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if checks_passed == 0 { return Ok(TestStatus::Warning { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 3fba588..724f033 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -69,9 +69,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let param_infos = params .info() @@ -143,9 +141,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul } } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if num_supported_value_to_text == 0 || num_supported_text_to_value == 0 { return Ok(TestStatus::Skipped { @@ -195,9 +191,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -281,9 +275,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b std::mem::swap(&mut previous_events, &mut current_events); } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -310,9 +302,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let audio_ports_config = audio_ports .map(|ports| ports.config()) @@ -364,9 +354,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) })?; } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -421,9 +409,7 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -451,9 +437,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let param_infos = params .info() @@ -493,9 +477,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) .collect::>>()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if actual_param_values == initial_param_values { Ok(TestStatus::Success { details: None }) @@ -526,9 +508,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let param_infos = params .info() @@ -550,9 +530,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re } } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 1494ec9..f01b020 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -1,7 +1,6 @@ //! Contains most of the boilerplate around testing audio processing. use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; -use crate::plugin::ext::latency::Latency; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::ext::tail::Tail; use crate::plugin::instance::{CallbackEvent, ProcessStatus}; @@ -10,6 +9,7 @@ use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng}; use anyhow::{Context, Result}; +use either::Either; use rand::Rng; const BUFFER_SIZE: u32 = 512; @@ -53,9 +53,7 @@ pub fn test_process_audio_basic(library: &PluginLibrary, plugin_id: &str, in_pla Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -89,9 +87,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl .context("Error while querying 'note-ports' IO configuration")? .unwrap_or_default(); - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let has_double_support = audio_ports_config .inputs @@ -126,9 +122,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -199,9 +193,7 @@ pub fn test_process_note_out_of_place( Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -237,6 +229,8 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for &sample_rate in SAMPLE_RATES { + log::trace!("Testing processing with sample rate: {:.2}hz", sample_rate); + plugin .on_audio_thread(|plugin| -> Result<()> { let mut note_rng = NoteGenerator::new(¬e_ports_config); @@ -255,16 +249,14 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st .with_context(|| format!("Error while processing with {:.2}hz sample rate", sample_rate))?; } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } /// The test for `PluginTestCase::ProcessVaryingBlockSizes`. pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str) -> Result { - const BLOCK_SIZES: &[u32] = &[1, 8, 32, 256, 512, 1024, 2048, 4096, 8192, 32768, 1536, 10, 17, 2027]; + const BLOCK_SIZES: &[u32] = &[1, 256, 1024, 4096, 16384, 1536, 10, 17, 2027]; let mut prng = new_prng(); @@ -288,12 +280,14 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str .unwrap_or_default(); for &buffer_size in BLOCK_SIZES { + log::trace!("Testing processing with max buffer size: {}", buffer_size); + plugin .on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size); let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - let num_iters = (32768 / buffer_size).min(5); + let num_iters = (16384 / buffer_size).min(5); for _ in 0..num_iters { process.audio_buffers().fill_white_noise(&mut prng); @@ -308,9 +302,7 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str .with_context(|| format!("Error while processing with buffer size of {}", buffer_size))?; } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -364,9 +356,7 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(TestStatus::Success { details: None }) } @@ -466,9 +456,7 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: Ok(TestStatus::Success { details: None }) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; Ok(result) } @@ -506,15 +494,13 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str }; for channel in 0..buffer.channels() { - let is_constant = - (0..buffer.samples()).all(|sample| buffer.get(channel, sample) == buffer.get(channel, 0)); // TODO: relax, allow small variations? + let is_constant = check_channel_quiet(buffer.channel(channel)); let marked_constant = buffer.get_output_constant_mask().is_channel_constant(channel); - if marked_constant && !is_constant { + if marked_constant && let Err(db) = is_constant { anyhow::bail!( "The plugin has marked output port {output}, channel {channel} as constant, but it contains \ - non-constant data. {:?}", - buffer.channel(channel) + non-constant data ({db:.2} dBFS)", ); } @@ -522,7 +508,7 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str has_received_constant_flag |= true; } - if is_constant { + if is_constant.is_ok() { has_received_constant_output |= true; } } @@ -538,20 +524,20 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str // block 1: silent inputs, see what the plugin does process.run()?; - check_buffers(process.audio_buffers()).context("Init block (silent)")?; + check_buffers(process.audio_buffers()).context("Block 0")?; // block 2: randomize inputs, see if the plugin tracks constant channels process.audio_buffers().fill_white_noise(&mut prng); process .input_queue() .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); - process.input_queue().add_events(note_rng.stop_all_voices(BUFFER_SIZE)); process.run()?; - check_buffers(process.audio_buffers()).context("Init block (white noise)")?; + check_buffers(process.audio_buffers()).context("Block 1")?; // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay process.audio_buffers().fill_silence(); + process.input_queue().add_events(note_rng.stop_all_voices(0)); for _ in 3..=40 { process.run()?; check_buffers(process.audio_buffers())?; @@ -560,9 +546,7 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if !has_received_constant_flag && has_received_constant_output { return Ok(TestStatus::Warning { @@ -598,9 +582,8 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st None => NotePortConfig::default(), }; - let mut latency = plugin.get_extension::().map_or(0, |ext| ext.get()); - let mut is_sleeping = false; let mut has_ever_slept = false; + let mut has_ever_returned_continue_if_not_quiet = false; plugin.on_audio_thread(|plugin| -> Result<()> { let tail = plugin.get_extension::(); @@ -609,10 +592,11 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st let mut note_rng = NoteGenerator::new(¬e_ports_config); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + let mut is_sleeping = false; let mut quiet_time = 0; - for i in 0..40 { - let is_quiet = (0..5).contains(&i) || (10..20).contains(&i) || (30..40).contains(&i); + for i in 0..80 { + let is_quiet = (0..5).contains(&i) || (10..20).contains(&i) || (30..).contains(&i); if is_quiet { process.input_queue().add_events(note_rng.stop_all_voices(0)); @@ -624,12 +608,7 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st process.audio_buffers().fill_white_noise(&mut prng); } - plugin.poll_callback(|plugin, event| match event { - CallbackEvent::LatencyChanged => { - latency = plugin.get_extension::().map_or(0, |ext| ext.get()); - Ok(()) - } - + plugin.poll_callback(|_, event| match event { CallbackEvent::RequestProcess => { is_sleeping = false; Ok(()) @@ -641,21 +620,37 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st let status = process.run()?; if is_sleeping && is_quiet { - // TODO: check that the output is silent + for buffer in process.audio_buffers().iter() { + let Some(output) = buffer.port().output() else { + continue; + }; + + for channel in 0..buffer.channels() { + let is_constant = check_channel_quiet(buffer.channel(channel)); + if let Err(db) = is_constant { + anyhow::bail!( + "The plugin is sleeping but output port {output}, channel {channel} contains \ + non-constant data ({db:.2} dBFS)", + ); + } + } + } } + has_ever_slept |= is_sleeping; + match status { ProcessStatus::Continue => is_sleeping = false, ProcessStatus::Sleep => is_sleeping = true, - ProcessStatus::ContinueIfNotQuiet => { let is_output_quiet = process .audio_buffers() .iter() .filter(|b| b.port().output().is_some()) - .all(|b| b.get_output_constant_mask().are_first_n_channels_constant(b.channels())); + .all(|b| b.get_output_constant_mask().are_all_channels_constant(b.channels())); is_sleeping = is_output_quiet; + has_ever_returned_continue_if_not_quiet = true; } ProcessStatus::Tail => { @@ -669,7 +664,7 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st } }; - is_sleeping = tail + latency < quiet_time; + is_sleeping = tail < quiet_time; if is_quiet { quiet_time += BUFFER_SIZE; } else { @@ -682,9 +677,51 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st Ok(()) })?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; + + if !has_ever_slept { + if has_ever_returned_continue_if_not_quiet { + return Ok(TestStatus::Warning { + details: Some(String::from( + "The plugin never went to sleep during the test. Make sure to set the output constant mask \ + correctly when returning `CLAP_PROCESS_CONTINUE_IF_NOT_QUIET`.", + )), + }); + } + + return Ok(TestStatus::Warning { + details: Some(String::from("The plugin never went to sleep during the test.")), + }); + } Ok(TestStatus::Success { details: None }) } + +/// A channel is considered quiet if the signal (excluding first 32 samples) is below -80 dbfs, ignoring DC. +/// +/// This function is designed to be very lenient in what it considers "quiet", to avoid false positives. +/// Returns `Ok(())` if the channel is quiet, or `Err(max_amplitude_in_db)` if not. +fn check_channel_quiet(channel: Either<&[f32], &[f64]>) -> Result<(), f64> { + /// -60 dbfs + const QUIET_THRESHOLD: f64 = 0.001; + + let (min, max) = match channel { + Either::Right(x) => x.iter().fold((f64::MAX, f64::MIN), |(min, max), &sample| { + (min.min(sample.abs()), max.max(sample.abs())) + }), + Either::Left(x) => { + let (min, max) = x.iter().fold((f32::MAX, f32::MIN), |(min, max), &sample| { + (min.min(sample.abs()), max.max(sample.abs())) + }); + + (min as f64, max as f64) + } + }; + + let range = (max - min) * 0.5; + if range < QUIET_THRESHOLD { + Ok(()) + } else { + Err(20.0 * range.log10()) + } +} diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index ea7a85c..d4c1d2e 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -43,9 +43,7 @@ pub fn test_state_invalid_empty(library: &PluginLibrary, plugin_id: &str) -> Res let result = state.load(&[]); - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; match result { Ok(_) => Ok(TestStatus::Warning { @@ -77,9 +75,7 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let mut random_data = vec![0u8; 1024 * 1024]; let mut succeeded = false; @@ -89,9 +85,7 @@ pub fn test_state_invalid_random(library: &PluginLibrary, plugin_id: &str) -> Re succeeded |= state.load(&random_data).is_ok(); } - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; match succeeded { false => Ok(TestStatus::Success { details: None }), @@ -149,9 +143,7 @@ pub fn test_state_reproducibility_basic( } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let param_infos = params .info() @@ -196,9 +188,7 @@ pub fn test_state_reproducibility_basic( let expected_state = state.save()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; (expected_state, expected_param_values) }; @@ -237,15 +227,11 @@ pub fn test_state_reproducibility_basic( } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; state.load(&expected_state)?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -272,9 +258,7 @@ pub fn test_state_reproducibility_basic( // Now for the moment of truth let actual_state = state.save()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) @@ -329,9 +313,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let param_infos = params .info() @@ -355,9 +337,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; // We'll compare against these values in that second pass let expected_param_values: BTreeMap = param_infos @@ -366,9 +346,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str .collect::>>()?; let expected_state = state.save()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; // Plugins with no parameters at all should of course not trigger this error if expected_param_values == initial_param_values && !random_param_set_events.is_empty() { @@ -422,9 +400,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; // NOTE: We can reuse random parameter set events, except that the cookie pointers may be // different if the plugin uses those. So we need to update these cookies first. @@ -481,9 +457,7 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let actual_state = state.save()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) @@ -566,9 +540,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> // treating this as the ground truth. let expected_state = state.save()?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; (expected_state, expected_param_values) }; @@ -606,16 +578,12 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> } }; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; let actual_param_values: BTreeMap = expected_param_values .keys() @@ -640,9 +608,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a callback")?; + plugin.poll_callback(|_| Ok(()))?; if actual_state == expected_state { Ok(TestStatus::Success { details: None }) diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index c118bcc..83f82e1 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -1,17 +1,9 @@ -use crate::{ - plugin::{ - ext::{ - audio_ports::{AudioPortConfig, AudioPorts}, - note_ports::{NotePortConfig, NotePorts}, - }, - library::PluginLibrary, - process::{AudioBuffers, Event, ProcessScope, TransportState}, - }, - tests::{ - TestStatus, - rng::{NoteGenerator, TransportFuzzer, new_prng}, - }, -}; +use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; +use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; +use crate::plugin::library::PluginLibrary; +use crate::plugin::process::{AudioBuffers, Event, ProcessScope, TransportState}; +use crate::tests::TestStatus; +use crate::tests::rng::{NoteGenerator, TransportFuzzer, new_prng}; use anyhow::{Context, Result}; const BUFFER_SIZE: u32 = 128; @@ -56,9 +48,7 @@ pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result Result Result // We'll try to run some audio through the plugin to make sure the preset change was // successful, but it doesn't matter if the plugin doesn't have any audio ports let audio_ports = plugin.get_extension::(); - plugin - .poll_callback(|_| Ok(())) - .context("An error occured during a host callback")?; + plugin.poll_callback(|_| Ok(()))?; let audio_ports_config = audio_ports .map(|ports| ports.config()) diff --git a/src/util.rs b/src/util.rs index aaecb65..ec48634 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,148 +1,7 @@ //! Miscellaneous functions for data conversions. -use anyhow::{Context, Result}; -use clap_sys::timestamp::{CLAP_TIMESTAMP_UNKNOWN, clap_timestamp}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::ffi::CString; -use std::os::raw::c_char; use std::path::PathBuf; -use std::{ffi::CStr, sync::OnceLock}; -use time::OffsetDateTime; - -/// Assert that the specified pointers are non-null. Panics if this is not the case. -macro_rules! check_null_ptr { - ($ptr:expr) => { - #[allow(unused_unsafe)] - unsafe { - if $ptr.is_null() { - panic!("'{}' is not allowed to be a null pointer", stringify!($ptr)) - } - } - }; - ($($ptrs:expr),*) => { - $($crate::util::check_null_ptr!($ptrs));* - }; -} - -/// Call a CLAP function. This is needed because even though none of CLAP's functions are allowed to -/// be null pointers, people will still use null pointers for some of the function arguments. This -/// also happens in the official `clap-helpers`. As such, these functions are now `Option` -/// optional function pointers in `clap-sys`. This macro asserts that the pointer is not null, and -/// prints a nicely formatted error message containing the struct and funciton name if it is. It -/// also emulates C's syntax for accessing fields struct through a pointer. Except that it uses `=>` -/// instead of `->`. Because that sounds like it would be hilarious. -macro_rules! clap_call { - { $obj_ptr:expr=>$function_name:ident($($args:expr),* $(, )?) } => { - match (*$obj_ptr).$function_name { - Some(function_ptr) => function_ptr($($args),*), - None => panic!("'{}::{}' is a null pointer, but this is not allowed", $crate::util::type_name_of_ptr($obj_ptr), stringify!($function_name)), - } - } -} - -pub(crate) use {check_null_ptr, clap_call}; - -/// Similar to, [`std::any::type_name_of_val()`], but on stable Rust, and stripping away the pointer -/// part. -#[must_use] -pub fn type_name_of_ptr(_ptr: *const T) -> &'static str { - std::any::type_name::() -} - -/// Convert a `*const c_char` to a `String`. Returns `Ok(None)` if the pointer is a null pointer or -/// if the string is not valid UTF-8. This only returns an error if the string contains invalid -/// UTF-8. -/// -/// # Safety -/// -/// `ptr` should point to a valid null terminated C-string. -pub unsafe fn cstr_ptr_to_string(ptr: *const c_char) -> Result> { - if ptr.is_null() { - return Ok(None); - } - - unsafe { - CStr::from_ptr(ptr) - .to_str() - .map(|str| Some(String::from(str))) - .context("Error while parsing UTF-8") - } -} - -/// The same as [`cstr_ptr_to_string()`], but it returns an error if the string is empty. -pub unsafe fn cstr_ptr_to_mandatory_string(ptr: *const c_char) -> Result { - unsafe { - match cstr_ptr_to_string(ptr)? { - Some(string) if string.is_empty() => anyhow::bail!("The string is empty."), - Some(string) => Ok(string), - None => anyhow::bail!("The string is a null pointer."), - } - } -} - -/// The same as [`cstr_ptr_to_string()`], but it treats empty strings as missing. Useful for parsing -/// optional fields from structs. -pub unsafe fn cstr_ptr_to_optional_string(ptr: *const c_char) -> Result> { - unsafe { - match cstr_ptr_to_string(ptr)? { - Some(string) if string.is_empty() => Ok(None), - x => Ok(x), - } - } -} - -/// Convert a null terminated `*const *const c_char` array to a `Vec`. Returns `None` if the -/// first pointer is a null pointer. Returns an error if any of the strings are not valid UTF-8. -/// -/// # Safety -/// -/// `ptr` should point to a valid null terminated C-string array. -pub unsafe fn cstr_array_to_vec(mut ptr: *const *const c_char) -> Result>> { - unsafe { - if ptr.is_null() { - return Ok(None); - } - - let mut strings = Vec::new(); - while !(*ptr).is_null() { - // We already checked for null pointers, so we can safely unwrap this - strings.push(cstr_ptr_to_string(*ptr)?.unwrap()); - ptr = ptr.offset(1); - } - - Ok(Some(strings)) - } -} - -/// Convert a `c_char` slice to a `String`. Returns an error if the slice did not contain a null -/// byte, or if the string is not valid UTF-8. -pub fn c_char_slice_to_string(slice: &[c_char]) -> Result { - // `from_bytes_until_nul` is still unstable, so we'll YOLO it for now by checking if the slice - // contains a null byte and then treating it as a pointer if it does - if !slice.contains(&0) { - anyhow::bail!("The string buffer does not contain a null byte.") - } - - unsafe { CStr::from_ptr(slice.as_ptr()) } - .to_str() - .context("Error while parsing UTF-8") - .map(String::from) -} - -/// Convert a `clap_timestamp` to an `Option`. A value of `CLAP_TIMESTAMP_UNKNOWN` -/// gets translated to `None`. -pub fn parse_timestamp(timestamp: clap_timestamp) -> Result> { - let parsed = if timestamp == CLAP_TIMESTAMP_UNKNOWN { - None - } else { - Some( - OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128 * 1_000_000) - .map_err(|_| anyhow::anyhow!("Could not parse the timestamp."))?, - ) - }; - - Ok(parsed) -} /// A temporary directory used by the validator. This is cleared when launching the validator. pub fn validator_temp_dir() -> PathBuf { @@ -161,58 +20,6 @@ pub fn validator_temp_dir() -> PathBuf { temp_dir().join("clap-validator") } -pub fn validator_version() -> &'static CStr { - static VERSION: OnceLock = OnceLock::new(); - VERSION - .get_or_init(|| CString::new(env!("CARGO_PKG_VERSION")).unwrap()) - .as_c_str() -} - -pub fn install_panic_hook() { - #[track_caller] - fn hook(info: &std::panic::PanicHookInfo) { - let backtrace = std::backtrace::Backtrace::capture(); - let backtrace = if backtrace.status() == std::backtrace::BacktraceStatus::Disabled { - String::from(". Set RUST_BACKTRACE=1 for a backtrace.") - } else { - format!("\n{}", backtrace) - }; - - let thread = std::thread::current(); - let thread = thread.name().unwrap_or(""); - - let msg = match info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; - - match info.location() { - Some(location) => { - log::error!( - target: "panic", "thread '{}' panicked at '{}': {}:{}{}", - thread, - msg, - location.file(), - location.line(), - backtrace - ); - } - None => log::error!( - target: "panic", - "thread '{}' panicked at '{}'{:?}", - thread, - msg, - backtrace - ), - } - } - - std::panic::set_hook(Box::new(hook)); -} - impl IteratorExt for T where T: Iterator {} pub trait IteratorExt: Iterator { /// Map the iterator in parallel if `parallel` is `true`, or sequentially if it is `false`. diff --git a/src/validator.rs b/src/validator.rs index 0529f17..39e9fb4 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -3,6 +3,7 @@ use crate::Verbosity; use crate::commands::validate::{SingleTestSettings, ValidatorSettings}; +use crate::panic::panic_message; use crate::plugin::library::{PluginLibrary, PluginMetadata}; use crate::tests::{PluginLibraryTestCase, PluginTestCase, SerializedTest, TestCase, TestResult, TestStatus}; use crate::util::{self, IteratorExt}; @@ -17,8 +18,9 @@ use std::fs; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::time::Instant; +use std::time::{Duration, Instant}; use strum::IntoEnumIterator; +use wait_timeout::ChildExt; /// The results of running the validation test suite on one or more plugins. Use the /// [`tally()`][Self::tally()] method to compute the number of successful and failed tests. @@ -239,6 +241,8 @@ fn run_test<'a, T: TestCase<'a>>( }) } +const WAIT_TIMEOUT: Duration = Duration::from_secs(30); + fn run_test_out_of_process<'a, T: TestCase<'a>>( test: &T, args: T::TestArgs, @@ -283,13 +287,23 @@ fn run_test_out_of_process<'a, T: TestCase<'a>>( .context("Could not call clap-validator for out-of-process validation")? // The docs make it seem like this can only fail if the process isn't running, but if // spawn succeeds then this can never fail: - .wait() + .wait_timeout(WAIT_TIMEOUT) .context("Error while waiting on clap-validator to finish running the test")?; - if !exit_status.success() { - return Ok(TestStatus::Crashed { - details: exit_status.to_string(), - }); + match exit_status { + None => { + return Ok(TestStatus::Crashed { + details: format!("Timed out after {} seconds", WAIT_TIMEOUT.as_secs()), + }); + } + + Some(status) if !status.success() => { + return Ok(TestStatus::Crashed { + details: status.to_string(), + }); + } + + _ => {} } // At this point, the child process _should_ have written its output to `output_file_path`, @@ -311,19 +325,9 @@ fn run_test_in_process(test: impl FnOnce() -> Result) -> TestStatus Ok(Err(err)) => TestStatus::Failed { details: Some(format!("{err:#}")), }, - Err(panic) => { - let message = if let Some(s) = panic.downcast_ref::<&str>() { - s.to_string() - } else if let Some(s) = panic.downcast_ref::() { - s.clone() - } else { - "A panic occurred".to_string() - }; - - TestStatus::Crashed { - details: format!("{message}. This is a bug in clap-validator"), - } - } + Err(panic) => TestStatus::Crashed { + details: panic_message(&*panic), + }, } } diff --git a/tests/clack-synth/src/lib.rs b/tests/clack-synth/src/lib.rs index 9e48187..664d583 100644 --- a/tests/clack-synth/src/lib.rs +++ b/tests/clack-synth/src/lib.rs @@ -1,5 +1,6 @@ use crate::params::{PolySynthParamModulations, PolySynthParams}; use crate::poly_oscillator::PolyOscillator; +use clack_extensions::audio_ports::*; use clack_extensions::audio_ports_activation::{ PluginAudioPortsActivation, PluginAudioPortsActivationImpl, PluginAudioPortsActivationSetImpl, SampleSize, }; @@ -10,8 +11,9 @@ use clack_extensions::audio_ports_config::{ use clack_extensions::configurable_audio_ports::{ AudioPortsRequestList, PluginConfigurableAudioPorts, PluginConfigurableAudioPortsImpl, }; +use clack_extensions::note_ports::*; +use clack_extensions::params::*; use clack_extensions::state::PluginState; -use clack_extensions::{audio_ports::*, note_ports::*, params::*}; use clack_plugin::events::spaces::CoreEventSpace; use clack_plugin::prelude::*; use clack_plugin::process::ConstantMask; @@ -110,6 +112,7 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre output_buffer.fill(0.0); + let mut is_non_silent = false; for event_batch in events.input.batch() { for event in event_batch.events() { self.handle_event(event); @@ -121,6 +124,8 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre self.shared.params.get_volume(), self.modulation_values.volume(), ); + + is_non_silent |= self.poly_osc.has_active_voices() } assert!(output_channels.channel_count() == self.channels); @@ -135,13 +140,16 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre } } - if self.poly_osc.has_active_voices() { - Ok(ProcessStatus::Continue) - } else { + if !is_non_silent { audio .output_port(0) .unwrap() .set_constant_mask(ConstantMask::FULLY_CONSTANT); + } + + if self.poly_osc.has_active_voices() { + Ok(ProcessStatus::Continue) + } else { Ok(ProcessStatus::Sleep) } } From 8a068a7b606d135bb9f9feebe8e8b4b33f58ef0e Mon Sep 17 00:00:00 2001 From: Quant1um Date: Mon, 2 Feb 2026 20:59:13 +0400 Subject: [PATCH 052/114] refactor extensions a little; add `Proxy` helper --- Cargo.lock | 7 + Cargo.toml | 1 + src/plugin/ext.rs | 12 +- src/plugin/ext/ambisonic.rs | 3 +- src/plugin/ext/audio_ports.rs | 45 +++-- src/plugin/ext/audio_ports_activation.rs | 3 +- src/plugin/ext/audio_ports_config.rs | 33 ++-- src/plugin/ext/configurable_audio_ports.rs | 3 +- src/plugin/ext/latency.rs | 3 +- src/plugin/ext/note_ports.rs | 3 +- src/plugin/ext/params.rs | 19 +- src/plugin/ext/preset_load.rs | 3 +- src/plugin/ext/state.rs | 179 ++++++++--------- src/plugin/ext/surround.rs | 3 +- src/plugin/ext/tail.rs | 3 +- src/plugin/ext/thread_pool.rs | 5 +- src/plugin/ext/voice_info.rs | 3 +- src/plugin/instance/audio_thread.rs | 31 +-- src/plugin/instance/main_thread.rs | 28 +-- src/plugin/instance/shared.rs | 140 ++++++-------- src/plugin/preset_discovery/indexer.rs | 73 +++---- .../preset_discovery/metadata_receiver.rs | 81 ++++---- src/plugin/preset_discovery/provider.rs | 9 +- src/plugin/process.rs | 46 ++--- src/plugin/process/events.rs | 183 +++++++++--------- src/plugin/util.rs | 106 ++++++++-- src/tests/plugin.rs | 2 +- src/tests/plugin/layout.rs | 124 ++++-------- src/tests/plugin/params.rs | 48 ++--- src/tests/plugin/processing.rs | 44 ++--- src/tests/plugin/state.rs | 34 +++- src/tests/plugin/transport.rs | 16 +- 32 files changed, 619 insertions(+), 674 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0634de0..c9c95f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,6 +161,7 @@ dependencies = [ "rand_pcg", "rayon", "regex", + "rustc-hash", "serde", "serde_json", "simplelog", @@ -586,6 +587,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.37.23" diff --git a/Cargo.toml b/Cargo.toml index 0e4c0e9..12ad7d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ textwrap = { version = "0.16.2", features = ["terminal_size"] } time = { version = "0.3", features = ["serde"]} walkdir = "2.3" wait-timeout = "0.2.1" +rustc-hash = "2.1.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" diff --git a/src/plugin/ext.rs b/src/plugin/ext.rs index 8062870..59c0bc7 100644 --- a/src/plugin/ext.rs +++ b/src/plugin/ext.rs @@ -20,15 +20,13 @@ pub mod tail; pub mod thread_pool; pub mod voice_info; -/// An abstraction for a CLAP plugin extension. `P` here is the plugin type. In practice, this is -/// either `Plugin`, `PluginShared` or `PluginAudioThread`. Abstractions for main thread functions will implement -/// this trait for `Plugin`, abstractions for audio thread functions will implement this trait -/// for `PluginAudioThread` and abstractions for thread-safe functions will implement this trait for -/// `PluginShared`. -pub trait Extension

{ +/// An abstraction for a CLAP plugin extension. +pub trait Extension { /// The list of C-string IDs for the extension. const IDS: &'static [&'static CStr]; + /// The plugin type (`Plugin` for main-thread, `PluginShared` for shared, `PluginAudioThread` for audio-thread) for which this extension is implemented. + type Plugin; /// The type of the C-struct for the extension. type Struct; @@ -38,5 +36,5 @@ pub trait Extension

{ /// # Safety /// The extension struct pointer must be a valid pointer to the correct extension struct for /// the plugin instance and given `IDS`. - unsafe fn new(plugin: P, extension_struct: NonNull) -> Self; + unsafe fn new(plugin: Self::Plugin, extension_struct: NonNull) -> Self; } diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index e982680..2fd76ba 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -11,9 +11,10 @@ pub struct Ambisonic<'a> { ambisonic: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for Ambisonic<'a> { +impl<'a> Extension for Ambisonic<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_AMBISONIC, CLAP_EXT_AMBISONIC_COMPAT]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_ambisonic; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index 4b1278b..b366099 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -48,14 +48,18 @@ pub struct AudioPort { /// Supports 64 bit processing pub supports_double_sample_size: bool, + /// Prefers 64 bit processing + pub prefers_double_sample_size: bool, + /// All ports with this flag require common sample size #[allow(unused)] // TODO: use for future mixed precision processing tests pub requires_common_sample_size: bool, } -impl<'a> Extension<&'a Plugin<'a>> for AudioPorts<'a> { +impl<'a> Extension for AudioPorts<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_AUDIO_PORTS]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_audio_ports; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { @@ -70,6 +74,20 @@ impl AudioPorts<'_> { /// Get the audio port configuration for this plugin. This automatically performs a number of /// consistency checks on the plugin's audio port configuration. pub fn config(&self) -> Result { + fn get_raw_port_info(this: &AudioPorts, is_input: bool, port_index: u32) -> Option { + let audio_ports = this.audio_ports.as_ptr(); + let plugin = this.plugin.as_ptr(); + + unsafe { + let mut info = clap_audio_port_info { ..zeroed() }; + if !clap_call! { audio_ports=>get(plugin, port_index, is_input, &mut info) } { + return None; + } + + Some(info) + } + } + let mut config = AudioPortConfig::default(); let audio_ports = self.audio_ports.as_ptr(); @@ -82,12 +100,12 @@ impl AudioPorts<'_> { }; for index in 0..num_inputs { - let info = match self.get_raw_port_info(true, index) { + let info = match get_raw_port_info(self, true, index) { Some(info) => info, None => { anyhow::bail!( "Plugin returned false when querying audio port info for input port {index} (out of \ - {num_inputs} total)." + {num_inputs} total)" ); } }; @@ -99,12 +117,12 @@ impl AudioPorts<'_> { } for index in 0..num_outputs { - let info = match self.get_raw_port_info(false, index) { + let info = match get_raw_port_info(self, false, index) { Some(info) => info, None => { anyhow::bail!( "Plugin returned false when querying audio port info for output port {index} (out of \ - {num_outputs} total)." + {num_outputs} total)" ); } }; @@ -155,22 +173,6 @@ impl AudioPorts<'_> { Ok(config) } - - /// Get the raw audio port information for the given port index. This does not perform any - /// consistency checks. - pub fn get_raw_port_info(&self, is_input: bool, port_index: u32) -> Option { - let audio_ports = self.audio_ports.as_ptr(); - let plugin = self.plugin.as_ptr(); - - unsafe { - let mut info = clap_audio_port_info { ..zeroed() }; - if !clap_call! { audio_ports=>get(plugin, port_index, is_input, &mut info) } { - return None; - } - - Some(info) - } - } } pub fn check_audio_port_info_valid( @@ -228,6 +230,7 @@ pub fn check_audio_port_info_valid( supports_double_sample_size, requires_common_sample_size, + prefers_double_sample_size, }) } diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index 5a5db5f..fcdf0ba 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -11,9 +11,10 @@ pub struct AudioPortsActivation<'a> { audio_ports_activation: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for AudioPortsActivation<'a> { +impl<'a> Extension for AudioPortsActivation<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_AUDIO_PORTS_ACTIVATION, CLAP_EXT_AUDIO_PORTS_ACTIVATION_COMPAT]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_audio_ports_activation; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 7441c74..555d87c 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -1,6 +1,6 @@ use crate::plugin::ext::Extension; use crate::plugin::ext::ambisonic::Ambisonic; -use crate::plugin::ext::audio_ports::check_audio_port_type_consistent; +use crate::plugin::ext::audio_ports::{AudioPort, check_audio_port_info_valid, check_audio_port_type_consistent}; use crate::plugin::ext::surround::Surround; use crate::plugin::instance::Plugin; use crate::plugin::util::{c_char_slice_to_string, clap_call}; @@ -35,9 +35,10 @@ pub struct AudioPortsConfigConfig { pub main_output_channel_count: Option, } -impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfig<'a> { +impl<'a> Extension for AudioPortsConfig<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_AUDIO_PORTS_CONFIG]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_audio_ports_config; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { @@ -48,12 +49,13 @@ impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfig<'a> { } } -impl<'a> Extension<&'a Plugin<'a>> for AudioPortsConfigInfo<'a> { +impl<'a> Extension for AudioPortsConfigInfo<'a> { const IDS: &'static [&'static CStr] = &[ CLAP_EXT_AUDIO_PORTS_CONFIG_INFO, CLAP_EXT_AUDIO_PORTS_CONFIG_INFO_COMPAT, ]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_audio_ports_config_info; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { @@ -147,6 +149,7 @@ impl AudioPortsConfig<'_> { } impl AudioPortsConfigInfo<'_> { + /// Get the current selected audio ports configuration ID. pub fn current(&self) -> clap_id { let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -156,24 +159,20 @@ impl AudioPortsConfigInfo<'_> { } } - /// Get the raw audio port information for the given port index. This does not perform any - /// consistency checks. - pub fn get_raw_port_info( - &self, - config_id: clap_id, - is_input: bool, - port_index: u32, - ) -> Option { - let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); - let plugin = self.plugin.as_ptr(); + /// Get information about an audio port for a configuration. + pub fn get(&self, config_id: clap_id, is_input: bool, port_index: u32) -> Result { + let info = unsafe { + let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); + let plugin = self.plugin.as_ptr(); - unsafe { let mut info = clap_audio_port_info { ..zeroed() }; if !clap_call! { audio_ports_config_info=>get(plugin, config_id, port_index, is_input, &mut info) } { - return None; + anyhow::bail!("audio_ports_config_info::get() returned false"); } - Some(info) - } + info + }; + + check_audio_port_info_valid(self.plugin, is_input, port_index, &info) } } diff --git a/src/plugin/ext/configurable_audio_ports.rs b/src/plugin/ext/configurable_audio_ports.rs index 6f5f969..f7dcccc 100644 --- a/src/plugin/ext/configurable_audio_ports.rs +++ b/src/plugin/ext/configurable_audio_ports.rs @@ -43,12 +43,13 @@ pub struct ConfigurableAudioPorts<'a> { configurable_audio_ports: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for ConfigurableAudioPorts<'a> { +impl<'a> Extension for ConfigurableAudioPorts<'a> { const IDS: &'static [&'static CStr] = &[ CLAP_EXT_CONFIGURABLE_AUDIO_PORTS, CLAP_EXT_CONFIGURABLE_AUDIO_PORTS_COMPAT, ]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_configurable_audio_ports; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 88cff50..08b67d6 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -11,9 +11,10 @@ pub struct Latency<'a> { latency: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for Latency<'a> { +impl<'a> Extension for Latency<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_LATENCY]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_latency; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index 1eaf203..b9ff26c 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -34,9 +34,10 @@ pub struct NotePort { pub supported_dialects: Vec, } -impl<'a> Extension<&'a Plugin<'a>> for NotePorts<'a> { +impl<'a> Extension for NotePorts<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_NOTE_PORTS]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_note_ports; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index b73d73e..2abb3ca 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -2,8 +2,8 @@ use super::Extension; use crate::plugin::instance::{Plugin, PluginStatus}; -use crate::plugin::process::EventQueue; -use crate::plugin::util::{self, c_char_slice_to_string, clap_call}; +use crate::plugin::process::{InputEventQueue, OutputEventQueue}; +use crate::plugin::util::{self, Proxy, c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; use clap_sys::ext::params::*; use clap_sys::id::{CLAP_INVALID_ID, clap_id}; @@ -11,7 +11,6 @@ use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::collections::BTreeMap; use std::ffi::{CStr, CString, c_void}; use std::ops::RangeInclusive; -use std::pin::Pin; use std::ptr::NonNull; pub type ParamInfo = BTreeMap; @@ -22,9 +21,10 @@ pub struct Params<'a> { params: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for Params<'a> { +impl<'a> Extension for Params<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_PARAMS]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_params; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { @@ -321,15 +321,10 @@ impl Params<'_> { } /// Perform a parameter flush. - /// - /// # Panics - /// - /// Panics if the plugin is active. - pub fn flush(&self, input_events: &Pin>, output_events: &Pin>) { + pub fn flush(&self, input_events: &Proxy, output_events: &Proxy) { // This may only be called on the audio thread when the plugin is active. This object is the // main thread interface for the parameters extension. self.status().assert_inactive(); - assert!(input_events.is_sorted(), "Input event queue must be sorted."); let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -337,8 +332,8 @@ impl Params<'_> { clap_call! { params=>flush( plugin, - input_events.vtable_input(), - output_events.vtable_output(), + Proxy::vtable(input_events), + Proxy::vtable(output_events), ) }; } diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 7610fd7..60bee05 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -16,9 +16,10 @@ pub struct PresetLoad<'a> { preset_load: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for PresetLoad<'a> { +impl<'a> Extension for PresetLoad<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_PRESET_LOAD]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_preset_load; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 7b876e9..8d2f98f 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -3,15 +3,15 @@ use super::Extension; use crate::panic::fail_test; use crate::plugin::instance::Plugin; -use crate::plugin::util::clap_call; +use crate::plugin::util::{CHECK_POINTER, Proxy, Proxyable, clap_call}; use anyhow::Result; use clap_sys::ext::state::{CLAP_EXT_STATE, clap_plugin_state}; use clap_sys::stream::{clap_istream, clap_ostream}; use std::ffi::{CStr, c_void}; -use std::pin::Pin; use std::ptr::NonNull; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread::ThreadId; /// Abstraction for the `state` extension covering the main thread functionality. pub struct State<'a> { @@ -22,10 +22,13 @@ pub struct State<'a> { /// An input stream backed by a slice. #[derive(Debug)] struct InputStream<'a> { - // The `ctx` pointer is set to this struct after creating the object - vtable: clap_istream, + /// The thread ID that created this stream. Used to verify that the plugin is calling the stream + /// methods from the same thread. + expected_thread_id: ThreadId, + + /// The buffer to read from. + read_buffer: &'a [u8], - buffer: &'a [u8], /// The current position when reading from the buffer. This is needed because the plugin /// provides the buffer we should copy data into, and subsequent reads should continue from /// where we were left off. @@ -38,22 +41,25 @@ struct InputStream<'a> { /// An output stream backed by a vector. #[derive(Debug)] struct OutputStream { - // The `ctx` pointer is set to this struct after creating the object - vtable: clap_ostream, + /// The thread ID that created this stream. Used to verify that the plugin is calling the stream + /// methods from the same thread. + expected_thread_id: ThreadId, // In Rust-land this function is object is only used from a single thread and there's absolutely // no reason for the plugin to be calling the stream read and write methods from multiple // threads, but better be safe than sorry. - buffer: Mutex>, + write_buffer: Mutex>, + /// The maximum number of bytes the plugin is allowed to write to this stream at a time, if the /// stream pretends to be buffered. This is used to test whether the plugin handles buffered /// streams correctly. max_write_size: Option, } -impl<'a> Extension<&'a Plugin<'a>> for State<'a> { +impl<'a> Extension for State<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_STATE]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_state; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { @@ -67,16 +73,16 @@ impl<'a> Extension<&'a Plugin<'a>> for State<'a> { impl State<'_> { /// Retrieve the plugin's state. Returns an error if the plugin returned `false`. pub fn save(&self) -> Result> { - let stream = OutputStream::new(); + let stream = OutputStream::new(None); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { - clap_call! { state=>save(plugin, &stream.vtable) } + clap_call! { state=>save(plugin, Proxy::vtable(&stream)) } }; if result { - Ok(stream.into_vec()) + Ok(stream.take()) } else { anyhow::bail!("'clap_plugin_state::save()' returned false."); } @@ -85,16 +91,16 @@ impl State<'_> { /// Retrieve the plugin's state while limiting the number of bytes the plugin can write at a /// time. Returns an error if the plugin returned `false`. pub fn save_buffered(&self, max_bytes: usize) -> Result> { - let stream = OutputStream::new().with_buffering(max_bytes); + let stream = OutputStream::new(Some(max_bytes)); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { - clap_call! { state=>save(plugin, stream.vtable()) } + clap_call! { state=>save(plugin, Proxy::vtable(&stream)) } }; if result { - Ok(stream.into_vec()) + Ok(stream.take()) } else { anyhow::bail!( "'clap_plugin_state::save()' returned false when only allowing the plugin to write {max_bytes} bytes \ @@ -105,12 +111,12 @@ impl State<'_> { /// Restore previously stored state. Returns an error if the plugin returned `false`. pub fn load(&self, state: &[u8]) -> Result<()> { - let stream = InputStream::new(state); + let stream = InputStream::new(state, None); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { - clap_call! { state=>load(plugin, stream.vtable()) } + clap_call! { state=>load(plugin, Proxy::vtable(&stream)) } }; if result { @@ -123,12 +129,12 @@ impl State<'_> { /// Restore previously stored state while limiting the number of bytes the plugin can read at a /// time. Returns an error if the plugin returned `false`. pub fn load_buffered(&self, state: &[u8], max_bytes: usize) -> Result<()> { - let stream = InputStream::new(state).with_buffering(max_bytes); + let stream = InputStream::new(state, Some(max_bytes)); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { - clap_call! { state=>load(plugin, &stream.vtable) } + clap_call! { state=>load(plugin, Proxy::vtable(&stream)) } }; if result { @@ -142,56 +148,65 @@ impl State<'_> { } } -impl<'a> InputStream<'a> { - /// Create a new input stream backed by a slice. - pub fn new(buffer: &'a [u8]) -> Pin> { - let mut stream = Box::pin(InputStream { - vtable: clap_istream { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - read: Some(Self::read), - }, - - buffer, - read_position: AtomicUsize::new(0), - max_read_size: None, - }); - - stream.vtable.ctx = &*stream as *const Self as *mut c_void; +impl<'a> Proxyable for InputStream<'a> { + type Vtable = clap_istream; - stream + fn init(&self) -> Self::Vtable { + clap_istream { + ctx: CHECK_POINTER, + read: Some(Self::read), + } } +} - /// The stream's `clap_istream` vtable. - pub fn vtable(self: &Pin>) -> *const clap_istream { - &self.vtable +impl Proxyable for OutputStream { + type Vtable = clap_ostream; + + fn init(&self) -> Self::Vtable { + clap_ostream { + ctx: CHECK_POINTER, + write: Some(Self::write), + } } +} - /// Only allow `max_bytes` bytes to be read at a time. Useful for simulating buffered streams. - pub fn with_buffering(mut self: Pin>, max_bytes: usize) -> Pin> { - self.max_read_size = Some(max_bytes); - self +impl<'a> InputStream<'a> { + /// Create a new input stream backed by a slice. + pub fn new(buffer: &'a [u8], max_read_size: Option) -> Proxy { + Proxy::new(InputStream { + read_buffer: buffer, + expected_thread_id: std::thread::current().id(), + read_position: AtomicUsize::new(0), + max_read_size, + }) } unsafe extern "C" fn read(stream: *const clap_istream, buffer: *mut c_void, size: u64) -> i64 { unsafe { - if stream.is_null() || (*stream).ctx.is_null() || buffer.is_null() { - fail_test!("'clap_istream::read' was called with a null pointer"); + let state = Proxy::::from_vtable(stream).unwrap_or_else(|e| { + fail_test!("clap_istream::read: {}", e); + }); + + if Proxy::vtable(&state).ctx != CHECK_POINTER { + fail_test!("clap_istream::read: plugin messed with the 'ctx' pointer"); + } + + if state.expected_thread_id != std::thread::current().id() { + fail_test!("clap_istream::read: called from a different thread than the one that created the stream"); } // The reads may be limited to a certain buffering size to test the plugin's capabilities - let this = &*((*stream).ctx as *const Self); - let size = match this.max_read_size { + let size = match state.max_read_size { Some(max_read_size) => size.min(max_read_size as u64), None => size, }; - let current_pos = this.read_position.load(Ordering::Relaxed); - let bytes_to_read = (this.buffer.len() - current_pos).min(size as usize); - this.read_position.fetch_add(bytes_to_read, Ordering::Relaxed); + let current_pos = state.read_position.load(Ordering::Relaxed); + let bytes_to_read = (state.read_buffer.len() - current_pos).min(size as usize); + state.read_position.fetch_add(bytes_to_read, Ordering::Relaxed); std::slice::from_raw_parts_mut(buffer as *mut u8, bytes_to_read) - .copy_from_slice(&this.buffer[current_pos..current_pos + bytes_to_read]); + .copy_from_slice(&state.read_buffer[current_pos..current_pos + bytes_to_read]); bytes_to_read as i64 } @@ -200,55 +215,45 @@ impl<'a> InputStream<'a> { impl OutputStream { /// Create a new output stream backed by a vector. - pub fn new() -> Pin> { - let mut stream = Box::pin(OutputStream { - vtable: clap_ostream { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - write: Some(Self::write), - }, - - buffer: Mutex::new(Vec::new()), - max_write_size: None, - }); - - stream.vtable.ctx = &*stream as *const Self as *mut c_void; - - stream - } - - /// The stream's `clap_ostream` vtable. - pub fn vtable(self: &Pin>) -> *const clap_ostream { - &self.vtable + pub fn new(max_write_size: Option) -> Proxy { + Proxy::new(OutputStream { + expected_thread_id: std::thread::current().id(), + write_buffer: Mutex::new(Vec::new()), + max_write_size, + }) } - /// Only allow `max_bytes` bytes to be written at a time. Useful for simulating buffered - /// streams. - pub fn with_buffering(mut self: Pin>, max_bytes: usize) -> Pin> { - self.max_write_size = Some(max_bytes); - self - } - - /// Get the byte buffer from this stream. - pub fn into_vec(self: Pin>) -> Vec { - // SAFETY: We can safely grab this inner buffer because this consumes the Box - unsafe { Pin::into_inner_unchecked(self) }.buffer.into_inner().unwrap() + /// Take the contents of the write buffer. + pub fn take(&self) -> Vec { + std::mem::take(&mut *self.write_buffer.lock().unwrap()) } unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { unsafe { - if stream.is_null() || (*stream).ctx.is_null() || buffer.is_null() { - fail_test!("'clap_ostream::write' was called with a null pointer"); + let state = Proxy::::from_vtable(stream).unwrap_or_else(|e| { + fail_test!("clap_ostream::write: {}", e); + }); + + if Proxy::vtable(&state).ctx != CHECK_POINTER { + fail_test!("clap_ostream::write: plugin messed with the 'ctx' pointer"); + } + + if buffer.is_null() { + fail_test!("clap_ostream::write: 'buffer' pointer is null"); + } + + if state.expected_thread_id != std::thread::current().id() { + fail_test!("clap_ostream::write: called from a different thread than the one that created the stream"); } // The writes may be limited to a certain buffering size to test the plugin's capabilities - let this = &*((*stream).ctx as *const Self); - let size = match this.max_write_size { + let size = match state.max_write_size { Some(max_write_size) => size.min(max_write_size as u64), None => size, }; - this.buffer + state + .write_buffer .lock() .unwrap() .extend_from_slice(std::slice::from_raw_parts(buffer as *const u8, size as usize)); diff --git a/src/plugin/ext/surround.rs b/src/plugin/ext/surround.rs index f5037ee..8c3988c 100644 --- a/src/plugin/ext/surround.rs +++ b/src/plugin/ext/surround.rs @@ -10,9 +10,10 @@ pub struct Surround<'a> { surround: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for Surround<'a> { +impl<'a> Extension for Surround<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_SURROUND, CLAP_EXT_SURROUND_COMPAT]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_surround; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/tail.rs b/src/plugin/ext/tail.rs index c000d1f..7b3bcda 100644 --- a/src/plugin/ext/tail.rs +++ b/src/plugin/ext/tail.rs @@ -11,9 +11,10 @@ pub struct Tail<'a> { tail: NonNull, } -impl<'a> Extension<&'a PluginAudioThread<'a>> for Tail<'a> { +impl<'a> Extension for Tail<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_TAIL]; + type Plugin = &'a PluginAudioThread<'a>; type Struct = clap_plugin_tail; unsafe fn new(plugin: &'a PluginAudioThread<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/ext/thread_pool.rs b/src/plugin/ext/thread_pool.rs index 330da3d..0a2cd30 100644 --- a/src/plugin/ext/thread_pool.rs +++ b/src/plugin/ext/thread_pool.rs @@ -13,9 +13,10 @@ pub struct ThreadPool<'a> { unsafe impl Send for ThreadPool<'_> {} unsafe impl Sync for ThreadPool<'_> {} -impl<'a> Extension<&'a PluginShared> for ThreadPool<'a> { +impl<'a> Extension for ThreadPool<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_THREAD_POOL]; + type Plugin = &'a PluginShared; type Struct = clap_plugin_thread_pool; unsafe fn new(plugin: &'a PluginShared, extension_struct: NonNull) -> Self { @@ -29,7 +30,7 @@ impl<'a> Extension<&'a PluginShared> for ThreadPool<'a> { impl<'a> ThreadPool<'a> { pub fn exec(&self, task: u32) { let thread_pool = self.tail.as_ptr(); - let plugin = self.plugin.clap_plugin_ptr(); + let plugin = self.plugin.clap_plugin; unsafe { clap_call! { thread_pool=>exec(plugin, task) } } diff --git a/src/plugin/ext/voice_info.rs b/src/plugin/ext/voice_info.rs index 6135289..8cfcc11 100644 --- a/src/plugin/ext/voice_info.rs +++ b/src/plugin/ext/voice_info.rs @@ -12,9 +12,10 @@ pub struct VoiceInfo<'a> { voice_info: NonNull, } -impl<'a> Extension<&'a Plugin<'a>> for VoiceInfo<'a> { +impl<'a> Extension for VoiceInfo<'a> { const IDS: &'static [&'static CStr] = &[CLAP_EXT_VOICE_INFO]; + type Plugin = &'a Plugin<'a>; type Struct = clap_plugin_voice_info; unsafe fn new(plugin: &'a Plugin<'a>, extension_struct: NonNull) -> Self { diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 175b487..5919d77 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -3,7 +3,7 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; use crate::plugin::instance::{CallbackEvent, MainThreadTask, PluginShared}; -use crate::plugin::util::clap_call; +use crate::plugin::util::{Proxy, clap_call}; use anyhow::Result; use clap_sys::plugin::clap_plugin; use clap_sys::process::*; @@ -11,9 +11,6 @@ use std::any::Any; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::panic::{AssertUnwindSafe, catch_unwind}; -use std::pin::Pin; -use std::ptr::NonNull; -use std::sync::Arc; use std::sync::mpsc::SyncSender; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be @@ -21,7 +18,7 @@ use std::sync::mpsc::SyncSender; pub struct PluginAudioThread<'a> { /// Information about this plugin instance stored on the host. This keeps track of things like /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - shared: Pin>, + shared: Proxy, _plugin_marker: PhantomData<&'a Plugin<'a>>, @@ -48,7 +45,7 @@ impl Drop for PluginAudioThread<'_> { } impl<'a> PluginAudioThread<'a> { - pub(super) fn new(shared: Pin>) -> PluginAudioThread<'a> { + pub(super) fn new(shared: Proxy) -> PluginAudioThread<'a> { shared.audio_thread_id.store(Some(std::thread::current().id())); PluginAudioThread { shared, @@ -59,7 +56,7 @@ impl<'a> PluginAudioThread<'a> { /// Get the raw pointer to the `clap_plugin` instance. pub fn as_ptr(&self) -> *const clap_plugin { - self.shared.clap_plugin_ptr() + self.shared.clap_plugin } /// Get the plugin's current initialization status. @@ -73,23 +70,9 @@ impl<'a> PluginAudioThread<'a> { } /// Get the _audio thread_ extension abstraction for the extension `T`, if the plugin supports - /// this extension. Returns `None` if it does not. The plugin needs to be initialized using - /// [`init()`][Self::init()] before this may be called. - pub fn get_extension>(&'a self) -> Option { - self.status().assert_is_not(PluginStatus::Uninitialized); - - let plugin = self.as_ptr(); - for id in T::IDS { - let extension_ptr = unsafe { - clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } - }; - - if !extension_ptr.is_null() { - return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; - } - } - - None + /// this extension. Returns `None` if it does not. + pub fn get_extension>(&'a self) -> Option { + unsafe { self.shared.raw_extension::().map(|ptr| T::new(self, ptr)) } } /// Dispatch a task to be executed on the main thread. This is a blocking call that will wait diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index a0ed28a..c7df29f 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -1,14 +1,11 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::{CallbackEvent, PluginAudioThread, PluginShared, PluginStatus}; use crate::plugin::library::PluginMetadata; -use crate::plugin::util::clap_call; +use crate::plugin::util::{Proxy, clap_call}; use anyhow::Result; use clap_sys::plugin::clap_plugin; use std::marker::PhantomData; use std::panic::resume_unwind; -use std::pin::Pin; -use std::ptr::NonNull; -use std::sync::Arc; use std::sync::mpsc::Receiver; pub enum MainThreadTask { @@ -33,7 +30,7 @@ pub struct Plugin<'lib> { /// Information about this plugin instance stored on the host. This keeps track of things like /// audio thread IDs, whether the plugin has pending callbacks, and what state it is in. - pub(super) shared: Pin>, + pub(super) shared: Proxy, /// The CLAP plugin library this plugin instance was created from. This field is not used /// directly, but keeping a reference to the library here prevents the plugin instance from @@ -75,7 +72,7 @@ impl Drop for Plugin<'_> { impl<'lib> Plugin<'lib> { /// Get the raw pointer to the `clap_plugin` instance. pub fn as_ptr(&self) -> *const clap_plugin { - self.shared.clap_plugin_ptr() + self.shared.clap_plugin } /// Get this plugin's metadata descriptor. In theory this should be the same as the one @@ -96,7 +93,7 @@ impl<'lib> Plugin<'lib> { } /// Handle any pending main-thread callbacks for this plugin and pending callback events. - /// Returns an error if there is a callback error pending. + /// Returns an error if a callback error occurred. pub fn poll_callback(&self, mut f: impl FnMut(CallbackEvent) -> Result<()>) -> Result<()> { self.poll_callback_unchecked(); @@ -114,21 +111,8 @@ impl<'lib> Plugin<'lib> { /// Get the _main thread_ extension abstraction for the extension `T`, if the plugin supports /// this extension. Returns `None` if it does not. The plugin needs to be initialized using /// [`init()`][Self::init()] before this may be called. - pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { - self.status().assert_is_not(PluginStatus::Uninitialized); - - let plugin = self.as_ptr(); - for id in T::IDS { - let extension_ptr = unsafe { - clap_call! { plugin=>get_extension(plugin, id.as_ptr()) } - }; - - if !extension_ptr.is_null() { - return unsafe { Some(T::new(self, NonNull::new(extension_ptr as *mut T::Struct).unwrap())) }; - } - } - - None + pub fn get_extension<'a, T: Extension>(&'a self) -> Option { + unsafe { self.shared.raw_extension::().map(|ptr| T::new(self, ptr)) } } /// Execute some code for this plugin from an audio thread context. The closure receives a diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index ff2af84..8f9d2e4 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -11,7 +11,7 @@ use crate::plugin::ext::thread_pool::ThreadPool; use crate::plugin::ext::voice_info::VoiceInfo; use crate::plugin::instance::{CallbackEvent, MainThreadTask, Plugin, PluginStatus}; use crate::plugin::preset_discovery::LocationValue; -use crate::plugin::util::{self, clap_call, object_tracker, validator_version}; +use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable, clap_call, validator_version}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::*; use clap_sys::ext::latency::*; @@ -32,10 +32,9 @@ use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::ffi::{CStr, c_char, c_void}; -use std::mem::offset_of; use std::ptr::NonNull; +use std::sync::Mutex; use std::sync::mpsc::{Sender, channel}; -use std::sync::{Arc, Mutex}; use std::thread::ThreadId; /// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. @@ -66,16 +65,28 @@ pub struct PluginShared { /// check that certain functions (like thread_pool::request_exec()) are called from the process function. pub is_currently_in_process_call: AtomicCell, - clap_plugin: *const clap_plugin, - clap_host: clap_host, + pub clap_plugin: *const clap_plugin, } unsafe impl Send for PluginShared {} unsafe impl Sync for PluginShared {} -impl Drop for PluginShared { - fn drop(&mut self) { - object_tracker::untrack(self.clap_host_ptr()); +impl Proxyable for PluginShared { + type Vtable = clap_host; + + fn init(&self) -> Self::Vtable { + clap_host { + clap_version: CLAP_VERSION, + host_data: CHECK_POINTER, + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), + version: validator_version().as_ptr(), + get_extension: Some(Self::clap_get_extension), + request_restart: Some(Self::clap_request_restart), + request_process: Some(Self::clap_request_process), + request_callback: Some(Self::clap_request_callback), + } } } @@ -91,7 +102,7 @@ impl PluginShared { let (callback_sender, callback_receiver) = channel(); let (task_sender, task_receiver) = channel(); - let shared = Arc::pin(PluginShared { + let shared = Proxy::new(PluginShared { task_sender, callback_sender, callback_error: Mutex::new(None), @@ -104,35 +115,11 @@ impl PluginShared { is_currently_in_process_call: AtomicCell::new(false), clap_plugin: std::ptr::null(), - clap_host: clap_host { - clap_version: CLAP_VERSION, - // This is populated with a pointer to the `Arc`'s data after creating the Arc - host_data: std::ptr::null_mut(), - name: c"clap-validator".as_ptr(), - vendor: c"Robbert van der Helm".as_ptr(), - url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: validator_version().as_ptr(), - get_extension: Some(Self::clap_get_extension), - request_restart: Some(Self::clap_request_restart), - request_process: Some(Self::clap_request_process), - request_callback: Some(Self::clap_request_callback), - }, }); - // Now that the Arc is pinned in memory, we can store a pointer to it in the clap_host struct - // so it can be retrieved in host callbacks - unsafe { - (&raw const shared.clap_host.host_data) - .cast_mut() - .write(&*shared as *const _ as *mut std::ffi::c_void); - } - - // Add the clap_host to the tracker so it can be validated in callbacks - object_tracker::track(shared.clap_host_ptr()); - let clap_plugin = unsafe { clap_call! { - factory=>create_plugin(factory, shared.clap_host_ptr(), plugin_id.as_ptr()) + factory=>create_plugin(factory, Proxy::vtable(&shared), plugin_id.as_ptr()) } }; @@ -154,31 +141,28 @@ impl PluginShared { }) } - /// Get a pointer to the `clap_host` struct for this plugin instance. - pub fn clap_host_ptr(&self) -> *const clap_host { - &self.clap_host as *const clap_host - } - - /// Get a pointer to the plugin-provided `clap_plugin` struct for this plugin instance. - pub fn clap_plugin_ptr(&self) -> *const clap_plugin { - self.clap_plugin - } + /// Get the raw extension pointer for the extension `T`, if the plugin supports this extension. + pub fn raw_extension(&self) -> Option> { + self.status().assert_is_not(PluginStatus::Uninitialized); - /// Get a shared extension abstraction for the extension `T`, if the plugin supports this extension. - pub fn get_extension<'a, T: Extension<&'a Self>>(&'a self) -> Option { for id in T::IDS { let extension_ptr = unsafe { - clap_call! { self.clap_plugin_ptr()=>get_extension(self.clap_plugin_ptr(), id.as_ptr()) } + clap_call! { self.clap_plugin=>get_extension(self.clap_plugin, id.as_ptr()) } }; if !extension_ptr.is_null() { - return unsafe { Some(T::new(self, NonNull::new_unchecked(extension_ptr as *mut _))) }; + return NonNull::new(extension_ptr as *mut T::Struct); } } None } + /// Get a shared extension abstraction for the extension `T`, if the plugin supports this extension. + pub fn get_extension<'a, T: Extension>(&'a self) -> Option { + unsafe { self.raw_extension::().map(|ptr| T::new(self, ptr)) } + } + /// The plugin's current initialization status. pub fn status(&self) -> PluginStatus { self.status.load() @@ -189,18 +173,16 @@ impl PluginShared { log::trace!("'{}' was called by the plugin", function_name); let state = unsafe { - if let Err(e) = object_tracker::check(host) { + Proxy::::from_vtable(host).unwrap_or_else(|e| { fail_test!("{}: {}", function_name, e); - } - - if (*host).host_data.wrapping_byte_add(offset_of!(Self, clap_host)) != host as *mut _ { - fail_test!("{}: Malformed 'clap_host.host_data' pointer", function_name); - } - - &*((*host).host_data as *const Self) + }) }; - match f(state) { + if Proxy::vtable(&state).host_data != CHECK_POINTER { + fail_test!("{}: plugin messed with the 'host_data' pointer", function_name); + } + + match f(&state) { Ok(result) => Some(result), Err(error) => { let mut guard = state.callback_error.lock().unwrap(); @@ -264,23 +246,19 @@ impl PluginShared { /// Checks whether the plugin has the required extension(s). If it does not, then an error /// will be set. Subsequent errors will not overwrite earlier ones. - fn assert_has_extension(&self, ids: &[&CStr]) -> Result<()> { + fn assert_has_extension(&self) -> Result<()> { anyhow::ensure!( self.status() != PluginStatus::Uninitialized, "Called while the plugin is uninitialized" ); - for id in ids { - let extension_ptr = unsafe { - clap_call! { self.clap_plugin_ptr()=>get_extension(self.clap_plugin_ptr(), id.as_ptr()) } - }; - - if !extension_ptr.is_null() { - return Ok(()); // found it! - } - } + anyhow::ensure!( + self.raw_extension::().is_some(), + "Plugin does not implement extension '{}'", + T::IDS[0].to_string_lossy() + ); - anyhow::bail!("Plugin does not implement extension {}", ids[0].to_string_lossy()); + Ok(()) } } @@ -395,7 +373,7 @@ impl PluginShared { unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { Self::wrap(host, "clap_host_audio_ports::is_rescan_flag_supported", |this| { this.assert_main_thread()?; - this.assert_has_extension(AudioPorts::IDS)?; + this.assert_has_extension::()?; Ok(true) }) .unwrap_or(false) @@ -404,7 +382,7 @@ impl PluginShared { unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { Self::wrap(host, "clap_host_audio_ports::rescan", |this| { this.assert_main_thread()?; - this.assert_has_extension(AudioPorts::IDS)?; + this.assert_has_extension::()?; if flags & CLAP_AUDIO_PORTS_RESCAN_NAMES != 0 { this.callback_sender.send(CallbackEvent::AudioPortsRescanNames).unwrap(); @@ -426,7 +404,7 @@ impl PluginShared { unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { Self::wrap(host, "clap_host_note_ports::supported_dialects", |this| { this.assert_main_thread()?; - this.assert_has_extension(NotePorts::IDS)?; + this.assert_has_extension::()?; Ok(CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE) }) .unwrap_or(0) @@ -435,7 +413,7 @@ impl PluginShared { unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { Self::wrap(host, "clap_host_note_ports::rescan", |this| { this.assert_main_thread()?; - this.assert_has_extension(NotePorts::IDS)?; + this.assert_has_extension::()?; if flags & CLAP_NOTE_PORTS_RESCAN_NAMES != 0 { this.callback_sender.send(CallbackEvent::NotePortsRescanNames).unwrap(); @@ -464,7 +442,7 @@ impl PluginShared { ) { Self::wrap(host, "clap_host_preset_load::on_error", |this| -> Result<()> { this.assert_main_thread()?; - this.assert_has_extension(PresetLoad::IDS)?; + this.assert_has_extension::()?; let location = unsafe { LocationValue::new(location_kind, location) } .context("'clap_host_preset_load::on_error()' called with invalid location parameters")?; @@ -495,7 +473,7 @@ impl PluginShared { ) { Self::wrap(host, "clap_host_preset_load::loaded", |this| { this.assert_main_thread()?; - this.assert_has_extension(PresetLoad::IDS)?; + this.assert_has_extension::()?; let _location = unsafe { LocationValue::new(location_kind, location) } .context("'Called with invalid location parameters")?; @@ -510,7 +488,7 @@ impl PluginShared { unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { Self::wrap(host, "clap_host_params::rescan", |this| { this.assert_main_thread()?; - this.assert_has_extension(Params::IDS)?; + this.assert_has_extension::()?; if flags & CLAP_PARAM_RESCAN_VALUES != 0 { this.callback_sender.send(CallbackEvent::ParamsRescanValues).unwrap(); @@ -540,7 +518,7 @@ impl PluginShared { unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { Self::wrap(host, "clap_host_params::clear", |this| { this.assert_main_thread()?; - this.assert_has_extension(Params::IDS)?; + this.assert_has_extension::()?; log::debug!("TODO: Handle 'clap_host_params::clear()'"); Ok(()) }); @@ -549,7 +527,7 @@ impl PluginShared { unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { Self::wrap(host, "clap_host_params::request_flush", |this| { this.assert_not_audio_thread()?; - this.assert_has_extension(Params::IDS)?; + this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); Ok(()) }); @@ -558,7 +536,7 @@ impl PluginShared { unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { Self::wrap(host, "clap_host_state::mark_dirty", |this| { this.assert_main_thread()?; - this.assert_has_extension(State::IDS)?; + this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); Ok(()) }); @@ -581,7 +559,7 @@ impl PluginShared { unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { Self::wrap(host, "clap_host_latency::changed", |this| { this.assert_main_thread()?; - this.assert_has_extension(Latency::IDS)?; + this.assert_has_extension::()?; anyhow::ensure!( this.status() == PluginStatus::Activating, @@ -597,7 +575,7 @@ impl PluginShared { unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { Self::wrap(host, "clap_host_tail::changed", |this| { this.assert_audio_thread()?; - this.assert_has_extension(Tail::IDS)?; + this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); Ok(()) }); @@ -606,7 +584,7 @@ impl PluginShared { unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { Self::wrap(host, "clap_host_voice_info::changed", |this| { this.assert_main_thread()?; - this.assert_has_extension(VoiceInfo::IDS)?; + this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); Ok(()) }); @@ -615,7 +593,7 @@ impl PluginShared { unsafe extern "C" fn ext_thread_pool_request_exec(host: *const clap_host, num_tasks: u32) -> bool { Self::wrap(host, "clap_host_thread_pool::request_exec", |this| { this.assert_audio_thread()?; - this.assert_has_extension(ThreadPool::IDS)?; + this.assert_has_extension::()?; // Ensure this is called from within the process() function // We already checked that we're on the audio thread, so this is sufficient diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 999fe5a..5614386 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -3,7 +3,7 @@ use crate::panic::fail_test; use crate::plugin::preset_discovery::parse_timestamp; -use crate::plugin::util::{self, object_tracker, validator_version}; +use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable, validator_version}; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::*; use clap_sys::version::CLAP_VERSION; @@ -11,7 +11,6 @@ use serde::Serialize; use std::ffi::{CStr, CString, c_char, c_void}; use std::fmt::Display; use std::path::Path; -use std::pin::Pin; use std::sync::Mutex; use std::thread::ThreadId; use time::OffsetDateTime; @@ -24,10 +23,6 @@ pub struct Indexer { /// The data written to this object by the plugin. result: Mutex>, - - /// The vtable that's passed to the provider. The `indexer_data` field is populated with a - /// pointer to this object. - clap_preset_discovery_indexer: clap_preset_discovery_indexer, } /// The data written to the indexer by the plugin during the @@ -323,42 +318,31 @@ impl Soundpack { } } -impl Drop for Indexer { - fn drop(&mut self) { - object_tracker::untrack(&self.clap_preset_discovery_indexer); +impl Proxyable for Indexer { + type Vtable = clap_preset_discovery_indexer; + + fn init(&self) -> Self::Vtable { + clap_preset_discovery_indexer { + clap_version: CLAP_VERSION, + indexer_data: CHECK_POINTER, + name: c"clap-validator".as_ptr(), + vendor: c"Robbert van der Helm".as_ptr(), + url: c"https://github.com/free-audio/clap-validator".as_ptr(), + version: validator_version().as_ptr(), + declare_filetype: Some(Self::declare_filetype), + declare_location: Some(Self::declare_location), + declare_soundpack: Some(Self::declare_soundpack), + get_extension: Some(Self::get_extension), + } } } impl Indexer { - pub fn new() -> Pin> { - let mut indexer = Box::pin(Self { + pub fn new() -> Proxy { + Proxy::new(Self { expected_thread_id: std::thread::current().id(), result: Mutex::new(Ok(IndexerResults::default())), - - clap_preset_discovery_indexer: clap_preset_discovery_indexer { - clap_version: CLAP_VERSION, - name: c"clap-validator".as_ptr(), - vendor: c"Robbert van der Helm".as_ptr(), - url: c"https://github.com/free-audio/clap-validator".as_ptr(), - version: validator_version().as_ptr(), - // This is filled with a pointer to this struct after the `Box` has been allocated - indexer_data: std::ptr::null_mut(), - declare_filetype: Some(Self::declare_filetype), - declare_location: Some(Self::declare_location), - declare_soundpack: Some(Self::declare_soundpack), - get_extension: Some(Self::get_extension), - }, - }); - - object_tracker::track(&indexer.clap_preset_discovery_indexer); - indexer.clap_preset_discovery_indexer.indexer_data = &*indexer as *const Self as *mut c_void; - indexer - } - - /// Get a `clap_preset_discovery_indexer` vtable pointer that can be passed to the - /// `clap_preset_discovery_factory` when creating a provider. - pub fn clap_preset_discovery_indexer_ptr(self: &Pin>) -> *const clap_preset_discovery_indexer { - &self.clap_preset_discovery_indexer + }) } /// Get the values written to this indexer by the plugin during the @@ -382,17 +366,16 @@ impl Indexer { log::trace!("'{}' was called by the plugin", function_name); let state = unsafe { - if indexer.is_null() || (*indexer).indexer_data.is_null() { - fail_test!( - "'{}' was called with a null 'clap_preset_discovery_indexer' pointer", - function_name - ); - } - - &*((*indexer).indexer_data as *const Self) + Proxy::::from_vtable(indexer).unwrap_or_else(|e| { + fail_test!("{}: {}", function_name, e); + }) }; - match f(state) { + if Proxy::vtable(&state).indexer_data != CHECK_POINTER { + fail_test!("{}: plugin messed with the 'indexer_data' pointer", function_name); + } + + match f(&state) { Ok(result) => Some(result), Err(error) => { let mut guard = state.result.lock().unwrap(); diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index f404c56..30ad2df 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -5,7 +5,7 @@ use super::{Flags, LocationValue}; use crate::panic::fail_test; use crate::plugin::preset_discovery::parse_timestamp; -use crate::plugin::util; +use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable}; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::*; use clap_sys::timestamp::clap_timestamp; @@ -13,9 +13,8 @@ use clap_sys::universal_plugin_id::clap_universal_plugin_id; use serde::Serialize; use std::cell::RefCell; use std::collections::BTreeMap; -use std::ffi::{c_char, c_void}; +use std::ffi::c_char; use std::fmt::Display; -use std::pin::Pin; use std::sync::Mutex; use std::thread::ThreadId; use time::OffsetDateTime; @@ -65,10 +64,6 @@ pub struct MetadataReceiver { /// on the presence of `load_key`. If this is not set, then subsequent `begin_preset()` calls /// are treated as errors. Used in `maybe_write_preset()`. next_load_key: RefCell>, - - /// The vtable that's passed to the provider. The `receiver_data` field is populated with a - /// pointer to this object. - clap_preset_discovery_metadata_receiver: clap_preset_discovery_metadata_receiver, } /// One or more presets declared by the plugin through a preset provider metadata receiver. @@ -244,10 +239,30 @@ impl Preset { } } +impl Proxyable for MetadataReceiver { + type Vtable = clap_preset_discovery_metadata_receiver; + + fn init(&self) -> Self::Vtable { + clap_preset_discovery_metadata_receiver { + receiver_data: CHECK_POINTER, + on_error: Some(Self::on_error), + begin_preset: Some(Self::begin_preset), + add_plugin_id: Some(Self::add_plugin_id), + set_soundpack_id: Some(Self::set_soundpack_id), + set_flags: Some(Self::set_flags), + add_creator: Some(Self::add_creator), + set_description: Some(Self::set_description), + set_timestamps: Some(Self::set_timestamps), + add_feature: Some(Self::add_feature), + add_extra_info: Some(Self::add_extra_info), + } + } +} + impl MetadataReceiver { /// Create a new metadata receiver. - pub fn new(location: LocationValue, location_flags: Flags) -> Pin> { - let mut metadata_receiver = Box::pin(Self { + pub fn new(location: LocationValue, location_flags: Flags) -> Proxy { + Proxy::new(Self { expected_thread_id: std::thread::current().id(), location, @@ -255,39 +270,12 @@ impl MetadataReceiver { result: Mutex::new(Ok(None)), next_preset_data: RefCell::new(None), next_load_key: RefCell::new(None), - - clap_preset_discovery_metadata_receiver: clap_preset_discovery_metadata_receiver { - // This is set to a pointer to this pinned data structure later - receiver_data: std::ptr::null_mut(), - on_error: Some(Self::on_error), - begin_preset: Some(Self::begin_preset), - add_plugin_id: Some(Self::add_plugin_id), - set_soundpack_id: Some(Self::set_soundpack_id), - set_flags: Some(Self::set_flags), - add_creator: Some(Self::add_creator), - set_description: Some(Self::set_description), - set_timestamps: Some(Self::set_timestamps), - add_feature: Some(Self::add_feature), - add_extra_info: Some(Self::add_extra_info), - }, - }); - - metadata_receiver.clap_preset_discovery_metadata_receiver.receiver_data = - &*metadata_receiver as *const Self as *mut c_void; - metadata_receiver - } - - /// Get a `clap_preset_discovery_metadata_receiver` vtable pointer that can be passed to the - /// `clap_preset_discovery_factory` when creating a provider. - pub fn clap_preset_discovery_metadata_receiver_ptr( - self: &Pin>, - ) -> *const clap_preset_discovery_metadata_receiver { - &self.clap_preset_discovery_metadata_receiver + }) } /// Finish the preset declaration process and return the result. This finishes any pending /// presets and returns the [`PresetFile`]. - pub fn finish(self: Pin>) -> Result> { + pub fn finish(&self) -> Result> { self.flush_preset()?; std::mem::replace(&mut *self.result.lock().unwrap(), Ok(None)) } @@ -314,17 +302,16 @@ impl MetadataReceiver { log::trace!("'{}' was called by the plugin", function_name); let state = unsafe { - if receiver.is_null() || (*receiver).receiver_data.is_null() { - fail_test!( - "'{}' was called with a null 'clap_preset_discovery_metadata_receiver' pointer", - function_name - ); - } - - &*((*receiver).receiver_data as *const Self) + Proxy::::from_vtable(receiver).unwrap_or_else(|e| { + fail_test!("{}: {}", function_name, e); + }) }; - match f(state) { + if Proxy::vtable(&state).receiver_data != CHECK_POINTER { + fail_test!("{}: plugin messed with the 'receiver_data' pointer", function_name); + } + + match f(&state) { Ok(result) => Some(result), Err(error) => { let mut guard = state.result.lock().unwrap(); diff --git a/src/plugin/preset_discovery/provider.rs b/src/plugin/preset_discovery/provider.rs index 679a60b..56ebd6d 100644 --- a/src/plugin/preset_discovery/provider.rs +++ b/src/plugin/preset_discovery/provider.rs @@ -3,13 +3,12 @@ use super::indexer::{Indexer, IndexerResults}; use super::metadata_receiver::{MetadataReceiver, PresetFile}; use super::{Location, LocationValue, PresetDiscoveryFactory, ProviderMetadata}; -use crate::plugin::util::clap_call; +use crate::plugin::util::{Proxy, clap_call}; use anyhow::{Context, Result}; use clap_sys::factory::preset_discovery::clap_preset_discovery_provider; use std::collections::{BTreeMap, HashSet}; use std::ffi::CString; use std::marker::PhantomData; -use std::pin::Pin; use std::ptr::NonNull; use walkdir::WalkDir; @@ -31,7 +30,7 @@ pub struct Provider<'a> { /// /// Since there are currently no extensions the plugin shouldn't be interacting with it anymore /// after the `init()` call, but it still needs outlive the provider. - _indexer: Pin>, + _indexer: Proxy, /// The factory this provider was created form. Only used for the lifetime. _factory: &'a PresetDiscoveryFactory<'a>, /// To honor CLAP's thread safety guidelines, this provider cannot be shared with or sent to @@ -52,7 +51,7 @@ impl<'a> Provider<'a> { clap_call! { factory=>create( factory, - indexer.clap_preset_discovery_indexer_ptr(), + Proxy::vtable(&indexer), provider_id_cstring.as_ptr() ) } @@ -142,7 +141,7 @@ impl<'a> Provider<'a> { provider, location_kind, location_ptr, - metadata_receiver.clap_preset_discovery_metadata_receiver_ptr() + Proxy::vtable(&metadata_receiver) ) } }; diff --git a/src/plugin/process.rs b/src/plugin/process.rs index a3d0db6..1721f34 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -1,8 +1,8 @@ //! Data structures and functions surrounding audio processing. use crate::plugin::instance::{PluginAudioThread, PluginStatus, ProcessStatus}; +use crate::plugin::util::Proxy; use anyhow::Result; use clap_sys::process::*; -use std::pin::Pin; mod buffer; mod events; @@ -16,8 +16,8 @@ pub struct ProcessScope<'a> { plugin: &'a PluginAudioThread<'a>, buffer: &'a mut AudioBuffers, - events_input: Pin>, - events_output: Pin>, + events_input: Proxy, + events_output: Proxy, transport: TransportState, sample_rate: f64, @@ -38,8 +38,8 @@ impl<'a> ProcessScope<'a> { Ok(ProcessScope { plugin, buffer, - events_input: EventQueue::new(), - events_output: EventQueue::new(), + events_input: InputEventQueue::new(), + events_output: OutputEventQueue::new(), transport: TransportState::dummy(), sample_rate, }) @@ -53,12 +53,13 @@ impl<'a> ProcessScope<'a> { self.buffer.samples() } - pub fn input_queue(&self) -> &EventQueue { - &self.events_input + pub fn add_events(&mut self, events: impl IntoIterator) { + self.events_input.add_events(events); } - pub fn output_queue(&self) -> &EventQueue { - &self.events_output + #[allow(unused)] + pub fn read_events(&self) -> Vec { + self.events_output.read() } pub fn transport(&mut self) -> &mut TransportState { @@ -102,20 +103,15 @@ impl<'a> ProcessScope<'a> { self.plugin.start_processing()?; } + // check that we dont overfill the input event queue + assert!( + self.events_input.last_event_time().is_none_or(|t| t < samples), + "The input event queue contains events beyond the current processing block size" + ); + // prepare output event queue for processing self.events_output.clear(); - // prepare input event queue for processing - self.events_input.sort_events(); - - // check if the input events are within the block size - if let Some(event) = self.events_input.read().last() { - assert!( - event.header().time <= samples, - "Input event timestamp larger than block size", - ); - } - // prepare output audio buffers for processing // this is used to detect uninitialized output buffers for buffer in self.buffer.iter_mut() { @@ -142,8 +138,8 @@ impl<'a> ProcessScope<'a> { audio_outputs: outputs.as_mut_ptr(), audio_inputs_count: inputs.len() as u32, audio_outputs_count: outputs.len() as u32, - in_events: self.events_input.vtable_input(), - out_events: self.events_output.vtable_output(), + in_events: Proxy::vtable(&self.events_input), + out_events: Proxy::vtable(&self.events_output), }) })?; @@ -152,7 +148,7 @@ impl<'a> ProcessScope<'a> { self.transport.advance(samples as i64, self.sample_rate()); // check output audio buffers for NaNs or infinities - check_process_call_consistency(&self.buffer[..], &original_buffers, self.output_queue(), samples)?; + check_process_call_consistency(&self.buffer[..], &original_buffers, &self.events_output.read(), samples)?; Ok(status) } @@ -189,7 +185,7 @@ const CHECK_NAN_F64: f64 = f64::from_bits(0x7FF8_1234_5678_1234); fn check_process_call_consistency( resulting_buffers: &[AudioBuffer], original_buffers: &[AudioBuffer], - output_events: &EventQueue, + output_events: &[Event], block_size: u32, ) -> Result<()> { for (buffer, before) in resulting_buffers.iter().zip(original_buffers.iter()) { @@ -249,7 +245,7 @@ fn check_process_call_consistency( // If the plugin output any events, then they should be in a monotonically increasing order let mut last_event_time = 0; - for event in output_events.read() { + for event in output_events { let event_time = event.header().time; if event_time < last_event_time { anyhow::bail!( diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index d98caae..842e7a3 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -1,20 +1,13 @@ +use crate::panic::fail_test; +use crate::plugin::util::{CHECK_POINTER, Proxy, Proxyable}; use clap_sys::events::*; -use std::pin::Pin; use std::sync::Mutex; -use crate::panic::fail_test; +#[derive(Debug)] +pub struct InputEventQueue(Mutex>); -/// An event queue that can be used as either an input queue or an output queue. This is always -/// allocated through a `Pin>` so the pointers are stable. The `VTable` type -/// argument should be either `clap_input_events` or `clap_output_events`. #[derive(Debug)] -pub struct EventQueue { - vtable_input: clap_input_events, - vtable_output: clap_output_events, - /// The actual event queue. Since we're going for correctness over performance, this uses a very - /// suboptimal memory layout by just using an `enum` instead of doing fancy bit packing. - events: Mutex>, -} +pub struct OutputEventQueue(Mutex>); /// An event sent to or from the plugin. This uses an enum to make the implementation simple and /// correct at the cost of more wasteful memory usage. @@ -38,109 +31,123 @@ pub enum Event { Unknown(clap_event_header), } -impl EventQueue { - /// Construct a new event queue. This can be used as both an input and an output queue. - pub fn new() -> Pin> { - let mut queue = Box::pin(Self { - vtable_input: clap_input_events { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - size: Some(Self::size), - get: Some(Self::get), - }, - - vtable_output: clap_output_events { - // This is set to point to this object below - ctx: std::ptr::null_mut(), - try_push: Some(Self::try_push), - }, - - // Using a mutex here is obviously a terrible idea in a real host, but we're not a real - // host - events: Mutex::new(Vec::new()), - }); - - queue.vtable_input.ctx = &*queue as *const Self as *mut _; - queue.vtable_output.ctx = &*queue as *const Self as *mut _; - queue - } +impl Proxyable for InputEventQueue { + type Vtable = clap_input_events; - pub fn clear(&self) { - self.events.lock().unwrap().clear(); + fn init(&self) -> Self::Vtable { + clap_input_events { + ctx: CHECK_POINTER, + size: Some(Self::size), + get: Some(Self::get), + } } +} - pub fn add_events(&self, extend: impl IntoIterator) { - self.events.lock().unwrap().extend(extend); - } +impl Proxyable for OutputEventQueue { + type Vtable = clap_output_events; - pub fn sort_events(&self) { - let mut events = self.events.lock().unwrap(); - events.sort_by_key(|event| event.header().time); + fn init(&self) -> Self::Vtable { + clap_output_events { + ctx: CHECK_POINTER, + try_push: Some(Self::try_push), + } } +} - pub fn is_sorted(&self) -> bool { - let events = self.events.lock().unwrap(); - events.is_sorted_by_key(|event| event.header().time) +impl InputEventQueue { + pub fn new() -> Proxy { + Proxy::new(Self(Mutex::new(Vec::new()))) } - pub fn read(&self) -> Vec { - self.events.lock().unwrap().clone() + pub fn clear(&self) { + self.0.lock().unwrap().clear(); } - /// Get the vtable pointer for input events. - pub fn vtable_input(self: &Pin>) -> *const clap_input_events { - &self.vtable_input + pub fn last_event_time(&self) -> Option { + let events = self.0.lock().unwrap(); + events.last().map(|event| event.header().time) } - /// Get the vtable pointer for output events. - pub fn vtable_output(self: &Pin>) -> *const clap_output_events { - &self.vtable_output + pub fn add_events(&self, extend: impl IntoIterator) { + let mut events = self.0.lock().unwrap(); + let is_empty = events.is_empty(); + events.extend(extend); + if !is_empty { + events.sort_by_key(|event| event.header().time); + } } unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { - unsafe { - if list.is_null() || (*list).ctx.is_null() { - fail_test!("'clap_input_events::size' was called with a null pointer"); - } - - let this = &*((*list).ctx as *const Self); - this.events.lock().unwrap().len() as u32 + let state = unsafe { + Proxy::::from_vtable(list).unwrap_or_else(|e| { + fail_test!("clap_input_events::size: {}", e); + }) + }; + + if Proxy::vtable(&state).ctx != CHECK_POINTER { + fail_test!("clap_input_events::size: plugin messed with the 'ctx' pointer"); } + + state.0.lock().unwrap().len() as u32 } unsafe extern "C" fn get(list: *const clap_input_events, index: u32) -> *const clap_event_header { - unsafe { - if list.is_null() || (*list).ctx.is_null() { - fail_test!("'clap_input_events::get' was called with a null pointer"); - } + let state = unsafe { + Proxy::::from_vtable(list).unwrap_or_else(|e| { + fail_test!("clap_input_events::size: {}", e); + }) + }; + + if Proxy::vtable(&state).ctx != CHECK_POINTER { + fail_test!("clap_input_events::size: plugin messed with the 'ctx' pointer"); + } - let this = &*((*list).ctx as *const Self); - let events = this.events.lock().unwrap(); - match events.get(index as usize) { - Some(event) => event.header(), - None => { - log::warn!( - "The plugin tried to get an event with index {index} ({} total events)", - events.len() - ); - std::ptr::null() - } + let events = state.0.lock().unwrap(); + match events.get(index as usize) { + Some(event) => event.header(), + None => { + log::warn!( + "The plugin tried to get an event with index {index} ({} total events)", + events.len() + ); + std::ptr::null() } } } +} + +impl OutputEventQueue { + pub fn new() -> Proxy { + Proxy::new(Self(Mutex::new(Vec::new()))) + } + + pub fn clear(&self) { + self.0.lock().unwrap().clear(); + } + + pub fn read(&self) -> Vec { + self.0.lock().unwrap().clone() + } unsafe extern "C" fn try_push(list: *const clap_output_events, event: *const clap_event_header) -> bool { - unsafe { - if list.is_null() || (*list).ctx.is_null() || event.is_null() { - fail_test!("'clap_output_events::try_push' was called with a null pointer"); - } + let state = unsafe { + Proxy::::from_vtable(list).unwrap_or_else(|e| { + fail_test!("clap_output_events::try_push: {}", e); + }) + }; + + if Proxy::vtable(&state).ctx != CHECK_POINTER { + fail_test!("clap_output_events::try_push: plugin messed with the 'ctx' pointer"); + } - // The monotonicity of the plugin's event insertion order is checked as part of the output - // consistency checks - let this = &*((*list).ctx as *const Self); - this.events.lock().unwrap().push(Event::from_raw(event)); - true + if event.is_null() { + fail_test!("clap_output_events::try_push: 'event' pointer is null"); } + + // The monotonicity of the plugin's event insertion order is checked as part of the output + // consistency checks + state.0.lock().unwrap().push(unsafe { Event::from_raw(event) }); + true } } diff --git a/src/plugin/util.rs b/src/plugin/util.rs index 81e67d9..a4d026e 100644 --- a/src/plugin/util.rs +++ b/src/plugin/util.rs @@ -1,7 +1,7 @@ //! Various utility functions for the plugin host. use anyhow::{Context, Result}; -use std::ffi::{CStr, CString, c_char}; +use std::ffi::{CStr, CString, c_char, c_void}; use std::sync::OnceLock; /// Call a CLAP function. This is needed because even though none of CLAP's functions are allowed to @@ -22,6 +22,10 @@ macro_rules! clap_call { pub(crate) use clap_call; +/// A pointer used for fields like `host_data` that can be checked for validity. +/// We do not use `host_data` etc. directly, instead we rely on the offsets within the owner struct (See [`crate::plugin::instance::PluginShared::wrap`] for more info) +pub const CHECK_POINTER: *mut c_void = 0xDEADCAFE as *mut c_void; + /// Similar to, [`std::any::type_name_of_val()`], but on stable Rust, and stripping away the pointer /// part. #[must_use] @@ -117,13 +121,15 @@ pub fn validator_version() -> &'static CStr { .as_c_str() } -/// Utility module for tracking CLAP object lifetimes during validation. -/// This is useful for checking that the plugin calls host-provided functions with valid pointers. -pub mod object_tracker { +pub use proxy::{Proxy, Proxyable}; + +mod proxy { use anyhow::Result; + use rustc_hash::FxHashMap; use std::any::{TypeId, type_name}; - use std::collections::HashMap; - use std::sync::RwLock; + use std::ops::Deref; + use std::pin::Pin; + use std::sync::{Arc, RwLock}; struct TrackStatus { type_id: TypeId, @@ -131,10 +137,10 @@ pub mod object_tracker { is_alive: bool, } - static OBJECTS: RwLock>> = RwLock::new(None); + static OBJECTS: RwLock>> = RwLock::new(None); /// Start tracking the given object pointer. - pub fn track(obj: *const T) { + fn track(obj: *const T) { let mut objects = OBJECTS.write().unwrap(); objects.get_or_insert_default().insert( obj.addr(), @@ -147,20 +153,16 @@ pub mod object_tracker { } /// Stop tracking the given object pointer, any subsequent use will be considered invalid. - pub fn untrack(obj: *const T) { + fn untrack(obj: *const T) { let mut objects = OBJECTS.write().unwrap(); match objects.as_mut().and_then(|x| x.get_mut(&obj.addr())) { Some(status) if TypeId::of::() == status.type_id => status.is_alive = false, - _ => panic!( - "Untrack failed: {} at {:p} was not being tracked", - type_name::(), - obj - ), + _ => unreachable!(), } } /// Check that the given object pointer is valid, of the correct type, and is still alive. - pub fn check(obj: *const T) -> Result<()> { + fn check(obj: *const T) -> Result<()> { if obj.is_null() { anyhow::bail!("null pointer to {}", type_name::()); } @@ -182,4 +184,78 @@ pub mod object_tracker { Ok(()) } + + #[repr(C)] + struct ProxyInner { + vtable: T::Vtable, + data: T, + } + + /// A type that can be proxied to the plugin through a vtable pointer. + /// See [`Proxy`] for more information. + pub trait Proxyable { + type Vtable: 'static; + + fn init(&self) -> Self::Vtable; + } + + /// An object that is accessible to the plugin through a vtable pointer. + /// Implementors of interfaces such as [`clap_host`], [`clap_istream`], and [`clap_ostream`] should be wrapped in this type before being passed to the plugin. + #[repr(transparent)] + pub struct Proxy(Pin>>); + + impl Proxy { + pub fn new(vtable: T) -> Self { + let arc = Arc::pin(ProxyInner { + vtable: vtable.init(), + data: vtable, + }); + + track(&arc.vtable); + Self(arc) + } + + pub fn vtable(this: &Self) -> &T::Vtable { + &this.0.vtable + } + + pub unsafe fn from_vtable(vtable: *const T::Vtable) -> Result { + check(vtable)?; + + unsafe { + let inner = vtable.cast::>(); + Arc::increment_strong_count(inner); + Ok(Proxy(Pin::new_unchecked(Arc::from_raw(inner)))) + } + } + } + + impl Clone for Proxy { + fn clone(&self) -> Self { + Proxy(self.0.clone()) + } + } + + impl Deref for Proxy { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0.data + } + } + + impl std::fmt::Debug for Proxy + where + T: std::fmt::Debug, + { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Proxy").field(&self.0.data).finish() + } + } + + impl Drop for ProxyInner { + fn drop(&mut self) { + untrack(&self.vtable); + } + } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 5339984..dadf51f 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -195,7 +195,7 @@ impl<'a> TestCase<'a> for PluginTestCase { ), PluginTestCase::ParamFuzzSampleAccurate => String::from( "Sets parameter values in a sample-accurate fashion while processing audio, generating them at fixed \ - intervals (1, 100, 1000 samples). The plugin passes the test if it doesn't produce any infinite or \ + intervals (10, 100, 1000 samples). The plugin passes the test if it doesn't produce any infinite or \ NaN values, and doesn't crash.", ), PluginTestCase::ParamFuzzModulation => String::from( diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 4fdee2f..2b88662 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -5,11 +5,9 @@ use crate::plugin::ext::configurable_audio_ports::ConfigurableAudioPorts; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, ProcessScope}; -use crate::plugin::util::{cstr_ptr_to_mandatory_string, cstr_ptr_to_string}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng, random_layout_requests}; use anyhow::{Context, Result}; -use clap_sys::ext::audio_ports::clap_audio_port_info; const BUFFER_SIZE: u32 = 512; @@ -161,48 +159,46 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) config_audio_ports_config.id, ); - for is_input in [true, false] { - let count = if is_input { - config_audio_ports_config.input_port_count - } else { - config_audio_ports_config.output_port_count - }; - - for index in 0..count { - let info_apci = audio_ports_config_info - .get_raw_port_info(config_audio_ports_config.id, is_input, index) - .with_context(|| { - format!( - "Could not get info for {} port {} of configuration '{}' ({}) from \ - 'audio-ports-config-info'", - if is_input { "input" } else { "output" }, - index, - config_audio_ports_config.name, - config_audio_ports_config.id, - ) - })?; - - let info_ap = audio_ports.get_raw_port_info(is_input, index).with_context(|| { + for index in 0..config_audio_ports_config.input_port_count { + let extra_info = audio_ports_config_info + .get(config_audio_ports_config.id, true, index) + .with_context(|| { format!( - "Could not get info for {} port {} of configuration '{}' ({}) from 'audio-ports'", - if is_input { "input" } else { "output" }, - index, - config_audio_ports_config.name, - config_audio_ports_config.id, + "Could not get info for input port {} of configuration '{}' ({}) from \ + 'audio-ports-config-info'", + index, config_audio_ports_config.name, config_audio_ports_config.id, ) })?; - check_mismatch_audio_port_info(&info_apci, &info_ap).with_context(|| { + anyhow::ensure!( + extra_info == config_audio_ports.inputs[index as usize], + "Mismatch between info queried via 'audio-ports-config-info' and 'audio-ports' for input port {} \ + of configuration '{}' ({})", + index, + config_audio_ports_config.name, + config_audio_ports_config.id, + ) + } + + for index in 0..config_audio_ports_config.output_port_count { + let extra_info = audio_ports_config_info + .get(config_audio_ports_config.id, false, index) + .with_context(|| { format!( - "Mismatch between info queried via 'audio-ports-config-info' and 'audio-ports' for {} \ - port {} of configuration '{}' ({})", - if is_input { "input" } else { "output" }, - index, - config_audio_ports_config.name, - config_audio_ports_config.id, + "Could not get info for output port {} of configuration '{}' ({}) from \ + 'audio-ports-config-info'", + index, config_audio_ports_config.name, config_audio_ports_config.id, ) })?; - } + + anyhow::ensure!( + extra_info == config_audio_ports.outputs[index as usize], + "Mismatch between info queried via 'audio-ports-config-info' and 'audio-ports' for output port {} \ + of configuration '{}' ({})", + index, + config_audio_ports_config.name, + config_audio_ports_config.id, + ) } } @@ -214,9 +210,7 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) for _ in 0..5 { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -315,9 +309,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: for _ in 0..5 { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -386,49 +378,3 @@ pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &s Ok(TestStatus::Success { details: None }) } - -fn check_mismatch_audio_port_info(info_left: &clap_audio_port_info, info_right: &clap_audio_port_info) -> Result<()> { - if info_left.id != info_right.id { - anyhow::bail!("ID mismatch: {} vs {}", info_left.id, info_right.id); - } - - if info_left.channel_count != info_right.channel_count { - anyhow::bail!( - "Channel count mismatch: {} vs {}", - info_left.channel_count, - info_right.channel_count - ); - } - - if info_left.flags != info_right.flags { - anyhow::bail!("Flags mismatch"); - } - - let (name_left, name_right) = unsafe { - ( - cstr_ptr_to_mandatory_string(info_left.name.as_ptr())?, - cstr_ptr_to_mandatory_string(info_right.name.as_ptr())?, - ) - }; - - if name_left != name_right { - anyhow::bail!("Name mismatch: {:?} vs {:?}", name_left, name_right); - } - - let (port_type_left, port_type_right) = unsafe { - ( - cstr_ptr_to_string(info_left.port_type)?, - cstr_ptr_to_string(info_right.port_type)?, - ) - }; - - if port_type_left != port_type_right { - anyhow::bail!( - "Port type mismatch: {:?} vs {:?}", - port_type_left.as_deref().unwrap_or(""), - port_type_right.as_deref().unwrap_or("") - ); - } - - Ok(()) -} diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index 724f033..cfb2a9c 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -3,7 +3,7 @@ use super::PluginTestCase; use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::note_ports::{NotePortConfig, NotePorts}; -use crate::plugin::ext::params::{ParamInfo, Params}; +use crate::plugin::ext::params::{Param, ParamInfo, Params}; use crate::plugin::library::PluginLibrary; use crate::plugin::process::{AudioBuffers, Event, ProcessScope}; use crate::tests::rng::{NoteGenerator, ParamFuzzer, new_prng}; @@ -226,13 +226,11 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b let run_result = plugin.on_audio_thread(|plugin| -> Result<()> { let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - process.input_queue().add_events(current_events.clone().unwrap()); + process.add_events(current_events.clone().unwrap()); for _ in 0..FUZZ_RUNS_PER_PERMUTATION { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -282,7 +280,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b /// The test for `ProcessingTest::ParamFuzzSampleAccurate`. pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) -> Result { - const INTERVALS: &[u32] = &[1000, 100, 1]; + const INTERVALS: &[u32] = &[1000, 100, 10]; let mut prng = new_prng(); let plugin = library @@ -325,16 +323,15 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for &interval in INTERVALS { + let num_steps = (interval * 4).div_ceil(BUFFER_SIZE); + plugin.on_audio_thread(|plugin| -> Result<()> { let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - - let num_steps = interval.div_ceil(BUFFER_SIZE); let mut current_sample = 0; for _ in 0..num_steps { while current_sample < BUFFER_SIZE { let events: Vec = param_fuzzer.randomize_params_at(&mut prng, current_sample).collect(); - - process.input_queue().add_events(events.clone()); + process.add_events(events.clone()); current_events = Some(events); current_sample += interval; } @@ -344,9 +341,7 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) // Audio and MIDI/note events are randomized in accordance to what the plugin // supports process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -400,12 +395,8 @@ pub fn test_param_fuzz_modulation(library: &PluginLibrary, plugin_id: &str) -> R let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(param_fuzzer.generate_events(&mut prng, process.max_block_size())); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, process.max_block_size())); + process.add_events(param_fuzzer.generate_events(&mut prng, process.max_block_size())); + process.add_events(note_rng.generate_events(&mut prng, process.max_block_size())); Ok(()) })?; @@ -465,7 +456,7 @@ pub fn test_param_set_wrong_namespace(library: &PluginLibrary, plugin_id: &str) let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().fill_white_noise(&mut prng); - process.input_queue().add_events(random_param_set_events); + process.add_events(random_param_set_events); process.run() })?; @@ -519,7 +510,7 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re .get(param_id) .with_context(|| format!("Could not get value for parameter {param_id}"))?; - if !param_compare_approx(default_value, param_info.default) { + if !param_compare_approx(¶m_info, default_value, param_info.default) { anyhow::bail!( "The default value for parameter {param_id} ('{}') is {}, but the actual parameter value after \ initialization is {}.", @@ -535,7 +526,16 @@ pub fn test_param_default_values(library: &PluginLibrary, plugin_id: &str) -> Re Ok(TestStatus::Success { details: None }) } -pub fn param_compare_approx(actual: f64, expected: f64) -> bool { - const EPSILON: f64 = 1e-5; - (actual - expected).abs() <= EPSILON +pub fn param_compare_approx(param: &Param, actual: f64, expected: f64) -> bool { + if param.stepped() { + let actual = actual.round() as i64; + let expected = expected.round() as i64; + + actual == expected + } else { + let actual = (actual - param.range.start()) / (param.range.end() - param.range.start()); + let expected = (expected - param.range.start()) / (param.range.end() - param.range.start()); + + (actual - expected).abs() <= 1e-4 // 0.01% of the range + } } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index f01b020..725ff7b 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -113,9 +113,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl for _ in 0..5 { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -184,9 +182,7 @@ pub fn test_process_note_out_of_place( for _ in 0..5 { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -238,9 +234,7 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st for _ in 0..5 { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; } @@ -291,9 +285,7 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str for _ in 0..num_iters { process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, buffer_size)); + process.add_events(note_rng.generate_events(&mut prng, buffer_size)); process.run()?; } @@ -345,9 +337,7 @@ pub fn test_process_random_block_sizes(library: &PluginLibrary, plugin_id: &str) }; process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, buffer_size)); + process.add_events(note_rng.generate_events(&mut prng, buffer_size)); process .run_with_block_size(buffer_size) .with_context(|| format!("Error while processing with buffer size of {}", buffer_size))?; @@ -391,9 +381,7 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: // first run, "control" run process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; let output_control = process @@ -406,9 +394,7 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: // second run, deactivate and reactivate the plugin, see if the output changes process.restart(); process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; let output_reactivated = process @@ -421,9 +407,7 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: // third run, reset the plugin, see if the output matches the control run process.reset(); process.audio_buffers().fill_white_noise(&mut new_prng()); - process - .input_queue() - .add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); process.run()?; let output_reset = process @@ -528,16 +512,14 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str // block 2: randomize inputs, see if the plugin tracks constant channels process.audio_buffers().fill_white_noise(&mut prng); - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.run()?; check_buffers(process.audio_buffers()).context("Block 1")?; // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay process.audio_buffers().fill_silence(); - process.input_queue().add_events(note_rng.stop_all_voices(0)); + process.add_events(note_rng.stop_all_voices(0)); for _ in 3..=40 { process.run()?; check_buffers(process.audio_buffers())?; @@ -599,12 +581,10 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st let is_quiet = (0..5).contains(&i) || (10..20).contains(&i) || (30..).contains(&i); if is_quiet { - process.input_queue().add_events(note_rng.stop_all_voices(0)); + process.add_events(note_rng.stop_all_voices(0)); process.audio_buffers().fill_silence(); } else { - process - .input_queue() - .add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); process.audio_buffers().fill_white_noise(&mut prng); } diff --git a/src/tests/plugin/state.rs b/src/tests/plugin/state.rs index d4c1d2e..05230d6 100644 --- a/src/tests/plugin/state.rs +++ b/src/tests/plugin/state.rs @@ -11,7 +11,7 @@ use crate::plugin::ext::audio_ports::{AudioPortConfig, AudioPorts}; use crate::plugin::ext::params::Params; use crate::plugin::ext::state::State; use crate::plugin::library::PluginLibrary; -use crate::plugin::process::{AudioBuffers, Event, EventQueue, ProcessScope}; +use crate::plugin::process::{AudioBuffers, Event, InputEventQueue, OutputEventQueue, ProcessScope}; use crate::tests::plugin::params::param_compare_approx; use crate::tests::rng::{ParamFuzzer, new_prng}; use crate::tests::{TestCase, TestStatus}; @@ -174,10 +174,12 @@ pub fn test_state_reproducibility_basic( let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().fill_white_noise(&mut prng); - process.input_queue().add_events(random_param_set_events); + process.add_events(random_param_set_events); process.run() })?; + plugin.poll_callback(|_| Ok(()))?; + // We'll check that the plugin has these sames values after reloading the state. These // values are rounded to the tenth decimal to provide some leeway in the serialization and // deserializatoin process. @@ -255,6 +257,8 @@ pub fn test_state_reproducibility_basic( ); } + plugin.poll_callback(|_| Ok(()))?; + // Now for the moment of truth let actual_state = state.save()?; @@ -331,8 +335,8 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let param_fuzzer = ParamFuzzer::new(¶m_infos); let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); - let input_events = EventQueue::new(); - let output_events = EventQueue::new(); + let input_events = InputEventQueue::new(); + let output_events = OutputEventQueue::new(); input_events.add_events(random_param_set_events.clone()); params.flush(&input_events, &output_events); @@ -432,10 +436,12 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().fill_white_noise(&mut prng); - process.input_queue().add_events(new_random_param_set_events); + process.add_events(new_random_param_set_events); process.run() })?; + plugin.poll_callback(|_| Ok(()))?; + let actual_param_values: BTreeMap = expected_param_values .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) @@ -455,6 +461,8 @@ pub fn test_state_reproducibility_flush(library: &PluginLibrary, plugin_id: &str ); } + plugin.poll_callback(|_| Ok(()))?; + let actual_state = state.save()?; plugin.poll_callback(|_| Ok(()))?; @@ -521,15 +529,19 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> let param_fuzzer = ParamFuzzer::new(¶m_infos); let random_param_set_events: Vec<_> = param_fuzzer.randomize_params_at(&mut prng, 0).collect(); + plugin.poll_callback(|_| Ok(()))?; + plugin.on_audio_thread(|plugin| { let mut buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut buffers)?; process.audio_buffers().fill_white_noise(&mut prng); - process.input_queue().add_events(random_param_set_events); + process.add_events(random_param_set_events); process.run() })?; + plugin.poll_callback(|_| Ok(()))?; + let expected_param_values: BTreeMap = param_infos .keys() .map(|param_id| params.get(*param_id).map(|value| (*param_id, value))) @@ -583,6 +595,7 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> // This is a buffered load that only loads 17 bytes at a time. Why 17? Because. const BUFFERED_LOAD_MAX_BYTES: usize = 17; state.load_buffered(&expected_state, BUFFERED_LOAD_MAX_BYTES)?; + plugin.poll_callback(|_| Ok(()))?; let actual_param_values: BTreeMap = expected_param_values @@ -604,6 +617,8 @@ pub fn test_state_buffered_streams(library: &PluginLibrary, plugin_id: &str) -> ); } + plugin.poll_callback(|_| Ok(()))?; + // Because we're mean, we'll use a different prime number for the saving const BUFFERED_SAVE_MAX_BYTES: usize = 23; let actual_state = state.save_buffered(BUFFERED_SAVE_MAX_BYTES)?; @@ -645,12 +660,13 @@ fn generate_param_diff( let diff = actual .iter() .filter_map(|(¶m_id, &actual_value)| { + let info = ¶m_infos[¶m_id]; let expected_value = expected[¶m_id]; - if param_compare_approx(actual_value, expected_value) { + + if param_compare_approx(info, actual_value, expected_value) { return None; } - let param_name = ¶m_infos[¶m_id].name; let string_actual = params .value_to_text(param_id, actual_value) .ok() @@ -664,7 +680,7 @@ fn generate_param_diff( Some(format!( "{}, {:?}, {:?}, {:.4}, {:?}, {:.4}", - param_id, param_name, string_actual, actual_value, string_expected, expected_value, + param_id, info.name, string_actual, actual_value, string_expected, expected_value, )) }) .collect::>(); diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index 83f82e1..22839a9 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -38,9 +38,7 @@ pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result Result Date: Tue, 3 Feb 2026 17:17:22 +0400 Subject: [PATCH 053/114] tracing experiment --- Cargo.lock | 485 +++++++++++++++--- Cargo.toml | 8 +- src/commands/validate.rs | 6 +- src/index.rs | 4 +- src/main.rs | 59 ++- src/panic.rs | 4 +- src/plugin/ext/ambisonic.rs | 2 + src/plugin/ext/audio_ports.rs | 56 +- src/plugin/ext/audio_ports_activation.rs | 7 + src/plugin/ext/audio_ports_config.rs | 1 + src/plugin/ext/latency.rs | 1 + src/plugin/ext/note_ports.rs | 63 ++- src/plugin/ext/params.rs | 13 +- src/plugin/ext/preset_load.rs | 5 +- src/plugin/ext/state.rs | 16 +- src/plugin/ext/surround.rs | 2 + src/plugin/ext/tail.rs | 4 +- src/plugin/ext/thread_pool.rs | 2 + src/plugin/ext/voice_info.rs | 8 +- src/plugin/instance.rs | 3 + src/plugin/instance/audio_thread.rs | 17 +- src/plugin/instance/main_thread.rs | 32 +- src/plugin/instance/shared.rs | 120 +++-- src/plugin/library.rs | 10 +- src/plugin/preset_discovery/indexer.rs | 38 +- .../preset_discovery/metadata_receiver.rs | 2 +- src/plugin/process.rs | 28 +- src/plugin/process/events.rs | 13 +- src/tests/plugin.rs | 2 + src/tests/plugin/layout.rs | 24 +- src/tests/plugin/processing.rs | 4 - src/tests/plugin_library.rs | 2 + src/tests/plugin_library/preset_discovery.rs | 2 +- src/validator.rs | 76 ++- 34 files changed, 822 insertions(+), 297 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9c95f2..c198449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,12 +3,12 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.0.2" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "memchr", + "libc", ] [[package]] @@ -62,9 +62,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" @@ -78,12 +84,47 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clack-common" version = "0.1.0" @@ -155,21 +196,22 @@ dependencies = [ "crossbeam", "either", "libloading", - "log", "midi-consts", - "rand", + "rand 0.9.2", "rand_pcg", "rayon", - "regex", + "regex-lite", "rustc-hash", "serde", "serde_json", - "simplelog", "strum", "strum_macros", "tempfile", "textwrap", "time", + "tracing", + "tracing-perfetto", + "tracing-subscriber", "wait-timeout", "walkdir", ] @@ -324,6 +366,23 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -354,6 +413,30 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -376,12 +459,37 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.178" @@ -434,6 +542,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -441,12 +558,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] -name = "num_threads" -version = "0.1.6" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "libc", + "autocfg", ] [[package]] @@ -455,6 +572,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "powerfmt" version = "0.2.0" @@ -476,6 +599,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.42" @@ -491,14 +637,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -508,7 +675,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -517,7 +693,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -526,7 +702,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" dependencies = [ - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -559,33 +735,10 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.9.1" +name = "regex-lite" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "rustc-hash" @@ -633,6 +786,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.15" @@ -692,16 +851,26 @@ dependencies = [ ] [[package]] -name = "simplelog" -version = "0.12.1" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "log", - "termcolor", - "time", + "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "smawk" version = "0.3.2" @@ -756,15 +925,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.2.6" @@ -797,6 +957,25 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread-id" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.36" @@ -804,10 +983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", - "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -830,6 +1006,79 @@ dependencies = [ "time-core", ] +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-perfetto" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf599f51530a7211f5aa92c84abdfc1137362d471f0473a4b1be8d625167fb0d" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "prost", + "rand 0.8.5", + "thread-id", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" version = "1.0.11" @@ -854,6 +1103,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -873,6 +1128,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -882,6 +1143,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -913,12 +1219,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 12ad7d3..3dbda99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,15 +25,13 @@ clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6 colored = "3.0.0" crossbeam = "0.8.4" libloading = "0.9.0" -log = "0.4" midi-consts = "0.1.0" rand = "0.9.2" rand_pcg = "0.9.0" rayon = "1.6.1" -regex = "1.6" +regex-lite = "0.1.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -simplelog = "0.12" strum = "0.27.2" strum_macros = "0.27.2" tempfile = "3.3" @@ -43,6 +41,10 @@ walkdir = "2.3" wait-timeout = "0.2.1" rustc-hash = "2.1.1" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" +tracing-perfetto = "0.1.5" + [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" diff --git a/src/commands/validate.rs b/src/commands/validate.rs index 9ae9a48..1d3c09d 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -132,10 +132,10 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result "PASSED".green(), - TestStatus::Crashed { .. } => "CRASHED".red().bold(), - TestStatus::Failed { .. } => "FAILED".red(), - TestStatus::Skipped { .. } => "SKIPPED".yellow(), + TestStatus::Skipped { .. } => "SKIPPED".dimmed(), TestStatus::Warning { .. } => "WARNING".yellow(), + TestStatus::Failed { .. } => "FAILED".red(), + TestStatus::Crashed { .. } => "CRASHED".red().bold(), }; let test_result = match test.status.details() { Some(reason) => format!(" {status_text}: {reason}"), diff --git a/src/index.rs b/src/index.rs index 9a8de75..9b5d4bd 100644 --- a/src/index.rs +++ b/src/index.rs @@ -32,7 +32,7 @@ pub fn index() -> Index { let directories = match clap_directories() { Ok(directories) => directories, Err(err) => { - log::error!("Could not find the CLAP plugin locations: {err:#}"); + tracing::error!("Could not find the CLAP plugin locations: {err:#}"); return index; } }; @@ -54,7 +54,7 @@ pub fn index() -> Index { Ok(metadata) => { index.0.insert(clap_plugin_path.into_path(), metadata); } - Err(err) => log::error!("{err:#}"), + Err(err) => tracing::error!("{err:#}"), } } } diff --git a/src/main.rs b/src/main.rs index 2adf678..aaf5936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,9 @@ use clap::{Parser, Subcommand, ValueEnum}; use std::process::ExitCode; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; mod commands; mod index; @@ -54,25 +58,40 @@ enum Command { fn main() -> ExitCode { let cli = Cli::parse(); - // For now logging everything to the terminal is fine. In the future it may be useful to have - // CLI options for things like the verbosity level. - simplelog::TermLogger::init( - match cli.verbosity { - Verbosity::Quiet => simplelog::LevelFilter::Off, - Verbosity::Error => simplelog::LevelFilter::Error, - Verbosity::Warn => simplelog::LevelFilter::Warn, - Verbosity::Info => simplelog::LevelFilter::Info, - Verbosity::Debug => simplelog::LevelFilter::Debug, - Verbosity::Trace => simplelog::LevelFilter::Trace, - }, - simplelog::ConfigBuilder::new() - .set_thread_mode(simplelog::ThreadLogMode::Both) - .set_location_level(simplelog::LevelFilter::Debug) - .build(), - simplelog::TerminalMode::Stderr, - simplelog::ColorChoice::Auto, - ) - .expect("Could not initialize logger"); + let subscriber_fmt = tracing_subscriber::fmt::Layer::new() + .without_time() + .pretty() + .with_span_events(FmtSpan::ACTIVE) + .with_thread_names(true) + .with_target(false); + + let subscriber_perfetto = tracing_perfetto::PerfettoLayer::new(std::sync::Mutex::new( + std::fs::File::create("/tmp/test.pftrace").unwrap(), + )) + .with_debug_annotations(true); + + tracing_subscriber::registry() + .with(subscriber_perfetto) + .with(subscriber_fmt) + .init(); + + // simplelog::TermLogger::init( + // match cli.verbosity { + // Verbosity::Quiet => simplelog::LevelFilter::Off, + // Verbosity::Error => simplelog::LevelFilter::Error, + // Verbosity::Warn => simplelog::LevelFilter::Warn, + // Verbosity::Info => simplelog::LevelFilter::Info, + // Verbosity::Debug => simplelog::LevelFilter::Debug, + // Verbosity::Trace => simplelog::LevelFilter::Trace, + // }, + // simplelog::ConfigBuilder::new() + // .set_thread_mode(simplelog::ThreadLogMode::Both) + // .set_location_level(simplelog::LevelFilter::Debug) + // .build(), + // simplelog::TerminalMode::Stderr, + // simplelog::ColorChoice::Auto, + // ) + // .expect("Could not initialize logger"); // Install the panic hook to log panics instead of printing them to stderr. panic::install_panic_hook(); @@ -91,7 +110,7 @@ fn main() -> ExitCode { match result { Ok(exit_code) => exit_code, Err(err) => { - log::error!("{err:?}"); + tracing::error!("{err:?}"); ExitCode::FAILURE } } diff --git a/src/panic.rs b/src/panic.rs index 9bb60c3..a7b9ecf 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -18,7 +18,7 @@ pub fn install_panic_hook() { match info.location() { Some(location) => { - log::error!( + tracing::error!( target: "panic", "thread '{}' panicked at '{}': {}:{}{}", thread, message, @@ -27,7 +27,7 @@ pub fn install_panic_hook() { backtrace ); } - None => log::error!( + None => tracing::error!( target: "panic", "thread '{}' panicked at '{}'{:?}", thread, diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index 2fd76ba..349dd10 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -26,6 +26,7 @@ impl<'a> Extension for Ambisonic<'a> { } impl<'a> Ambisonic<'a> { + #[tracing::instrument(name = "clap_plugin_ambisonic::is_config_supported", level = 1, skip(self))] pub fn is_config_supported(&self, config: &clap_ambisonic_config) -> bool { let ambisonic = self.ambisonic.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -34,6 +35,7 @@ impl<'a> Ambisonic<'a> { } } + #[tracing::instrument(name = "clap_plugin_ambisonic::get_config", level = 1, skip(self))] pub fn get_config(&self, is_input: bool, port_index: u32) -> Option { let ambisonic = self.ambisonic.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/audio_ports.rs b/src/plugin/ext/audio_ports.rs index b366099..91cdc54 100644 --- a/src/plugin/ext/audio_ports.rs +++ b/src/plugin/ext/audio_ports.rs @@ -74,33 +74,12 @@ impl AudioPorts<'_> { /// Get the audio port configuration for this plugin. This automatically performs a number of /// consistency checks on the plugin's audio port configuration. pub fn config(&self) -> Result { - fn get_raw_port_info(this: &AudioPorts, is_input: bool, port_index: u32) -> Option { - let audio_ports = this.audio_ports.as_ptr(); - let plugin = this.plugin.as_ptr(); - - unsafe { - let mut info = clap_audio_port_info { ..zeroed() }; - if !clap_call! { audio_ports=>get(plugin, port_index, is_input, &mut info) } { - return None; - } - - Some(info) - } - } - let mut config = AudioPortConfig::default(); - - let audio_ports = self.audio_ports.as_ptr(); - let plugin = self.plugin.as_ptr(); - let (num_inputs, num_outputs) = unsafe { - ( - clap_call! { audio_ports=>count(plugin, true) }, - clap_call! { audio_ports=>count(plugin, false) }, - ) - }; + let num_inputs = self.get_raw_port_count(true); + let num_outputs = self.get_raw_port_count(false); for index in 0..num_inputs { - let info = match get_raw_port_info(self, true, index) { + let info = match self.get_raw_port_info(true, index) { Some(info) => info, None => { anyhow::bail!( @@ -117,7 +96,7 @@ impl AudioPorts<'_> { } for index in 0..num_outputs { - let info = match get_raw_port_info(self, false, index) { + let info = match self.get_raw_port_info(false, index) { Some(info) => info, None => { anyhow::bail!( @@ -173,6 +152,31 @@ impl AudioPorts<'_> { Ok(config) } + + #[tracing::instrument(name = "clap_plugin_audio_ports::count", level = 1, skip(self))] + fn get_raw_port_count(&self, is_input: bool) -> u32 { + let audio_ports = self.audio_ports.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + clap_call! { audio_ports=>count(plugin, is_input) } + } + } + + #[tracing::instrument(name = "clap_plugin_audio_ports::get", level = 1, skip(self))] + fn get_raw_port_info(&self, is_input: bool, port_index: u32) -> Option { + let audio_ports = self.audio_ports.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut info = clap_audio_port_info { ..zeroed() }; + if !clap_call! { audio_ports=>get(plugin, port_index, is_input, &mut info) } { + return None; + } + + Some(info) + } + } } pub fn check_audio_port_info_valid( @@ -319,7 +323,7 @@ pub fn check_audio_port_type_consistent( Ok(()) } else { - log::warn!("Unknown audio port type '{port_type:?}'"); + tracing::warn!("Unknown audio port type '{port_type:?}'"); Ok(()) } } diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index fcdf0ba..00630fb 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -28,6 +28,11 @@ impl<'a> Extension for AudioPortsActivation<'a> { impl<'a> AudioPortsActivation<'a> { /// TODO: extra test where we do this while processing #[allow(unused)] + #[tracing::instrument( + name = "clap_plugin_audio_ports_activation::can_activate_while_processing", + level = 1, + skip(self) + )] pub fn can_activate_while_processing(&self) -> bool { let audio_ports_activation = self.audio_ports_activation.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -37,6 +42,8 @@ impl<'a> AudioPortsActivation<'a> { } /// Activates or deactivates audio ports while inactive. + #[allow(unused)] + #[tracing::instrument(name = "clap_plugin_audio_ports_activation::set_active", level = 1, skip(self))] pub fn set_active(&mut self, is_input: bool, port_index: u32, is_active: bool, sample_size: u32) -> bool { self.plugin.status().assert_inactive(); diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 555d87c..1769734 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -133,6 +133,7 @@ impl AudioPortsConfig<'_> { .collect() } + #[tracing::instrument(name = "clap_plugin_audio_ports_config::select", level = 1, skip(self))] pub fn select(&self, config_id: clap_id) -> Result<()> { let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/latency.rs b/src/plugin/ext/latency.rs index 08b67d6..a43f294 100644 --- a/src/plugin/ext/latency.rs +++ b/src/plugin/ext/latency.rs @@ -27,6 +27,7 @@ impl<'a> Extension for Latency<'a> { impl<'a> Latency<'a> { #[allow(unused)] + #[tracing::instrument(name = "clap_plugin_latency::get", level = 1, skip(self))] pub fn get(&self) -> u32 { self.plugin.status().assert_is_not(PluginStatus::Deactivated); diff --git a/src/plugin/ext/note_ports.rs b/src/plugin/ext/note_ports.rs index b9ff26c..90f6e84 100644 --- a/src/plugin/ext/note_ports.rs +++ b/src/plugin/ext/note_ports.rs @@ -54,30 +54,15 @@ impl NotePorts<'_> { pub fn config(&self) -> Result { let mut config = NotePortConfig::default(); - let note_ports = self.note_ports.as_ptr(); - let plugin = self.plugin.as_ptr(); - let (num_inputs, num_outputs) = unsafe { - ( - clap_call! { note_ports=>count(plugin, true) }, - clap_call! { note_ports=>count(plugin, false) }, - ) - }; + let num_inputs = self.get_raw_port_count(true); + let num_outputs = self.get_raw_port_count(false); // We don't need the port's stable IDs, but we'll still verify that they're unique let mut input_stable_indices: HashSet = HashSet::new(); let mut output_stable_indices: HashSet = HashSet::new(); for index in 0..num_inputs { - let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe { - clap_call! { note_ports=>get(plugin, index, true, &mut info) } - }; - - if !success { - anyhow::bail!( - "Plugin returned false when querying input note port {index} ({num_inputs} total input ports)." - ); - } + let info = self.get_raw_port_info(true, index)?; if !input_stable_indices.insert(info.id) { anyhow::bail!("The stable ID of input note port {index} ({}) is a duplicate.", info.id); @@ -90,16 +75,7 @@ impl NotePorts<'_> { } for index in 0..num_outputs { - let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; - let success = unsafe { - clap_call! { note_ports=>get(plugin, index, false, &mut info) } - }; - - if !success { - anyhow::bail!( - "Plugin returned false when querying output note port {index} ({num_outputs} total output ports)." - ); - } + let info = self.get_raw_port_info(false, index)?; if !output_stable_indices.insert(info.id) { anyhow::bail!( @@ -116,6 +92,37 @@ impl NotePorts<'_> { Ok(config) } + + #[tracing::instrument(name = "clap_plugin_note_ports::count", level = 1, skip(self))] + fn get_raw_port_count(&self, is_input: bool) -> u32 { + let note_ports = self.note_ports.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { note_ports=>count(plugin, is_input) } + } + } + + #[tracing::instrument(name = "clap_plugin_note_ports::get", level = 1, skip(self))] + fn get_raw_port_info(&self, is_input: bool, port_index: u32) -> Result { + let note_ports = self.note_ports.as_ptr(); + let plugin = self.plugin.as_ptr(); + + let mut info: clap_note_port_info = unsafe { std::mem::zeroed() }; + let success = unsafe { + clap_call! { note_ports=>get(plugin, port_index, is_input, &mut info) } + }; + + if !success { + anyhow::bail!( + "Plugin returned false when querying {} note port {port_index} ({} total {} ports).", + if is_input { "input" } else { "output" }, + self.get_raw_port_count(is_input), + if is_input { "input" } else { "output" } + ); + } + + Ok(info) + } } impl NotePort { diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index 2abb3ca..a115c40 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -1,7 +1,7 @@ //! Abstractions for interacting with the `params` extension. use super::Extension; -use crate::plugin::instance::{Plugin, PluginStatus}; +use crate::plugin::instance::Plugin; use crate::plugin::process::{InputEventQueue, OutputEventQueue}; use crate::plugin::util::{self, Proxy, c_char_slice_to_string, clap_call}; use anyhow::{Context, Result}; @@ -54,12 +54,8 @@ unsafe impl Send for Param {} unsafe impl Sync for Param {} impl Params<'_> { - /// Used by the status assertion macros. - fn status(&self) -> PluginStatus { - self.plugin.status() - } - /// Get a parameter's value. + #[tracing::instrument(name = "clap_plugin_params::get_value", level = 1, skip(self))] pub fn get(&self, param_id: clap_id) -> Result { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -78,6 +74,7 @@ impl Params<'_> { /// Convert a parameter value's to a string. Returns `Ok(None)` if the plugin doesn't support /// this, or an error if the returned string did not contain any null bytes or if it isn't /// invalid UTF-8. + #[tracing::instrument(name = "clap_plugin_params::value_to_text", level = 1, skip(self))] pub fn value_to_text(&self, param_id: clap_id, value: f64) -> Result> { let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -107,6 +104,7 @@ impl Params<'_> { /// Convert a string representation for a parameter to a value. Returns an `Ok(None)` if the /// plugin doesn't support this, or an error if the string contained internal null bytes. + #[tracing::instrument(name = "clap_plugin_params::text_to_value", level = 1, skip(self))] pub fn text_to_value(&self, param_id: clap_id, text: &str) -> Result> { let text_cstring = CString::new(text)?; @@ -321,10 +319,11 @@ impl Params<'_> { } /// Perform a parameter flush. + #[tracing::instrument(name = "clap_plugin_params::flush", level = 1, skip_all)] pub fn flush(&self, input_events: &Proxy, output_events: &Proxy) { // This may only be called on the audio thread when the plugin is active. This object is the // main thread interface for the parameters extension. - self.status().assert_inactive(); + self.plugin.status().assert_inactive(); let params = self.params.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/preset_load.rs b/src/plugin/ext/preset_load.rs index 60bee05..3b3e635 100644 --- a/src/plugin/ext/preset_load.rs +++ b/src/plugin/ext/preset_load.rs @@ -31,13 +31,12 @@ impl<'a> Extension for PresetLoad<'a> { } impl PresetLoad<'_> { - /// Try to load a preet based on a location and an optional load key. This information can be + /// Try to load a preset based on a location and an optional load key. This information can be /// obtained through the preset discovery factory /// ([`Library::preset_discovery_factory()`][[crate::plugin::library::Library::preset_discovery_factory()]]). /// Load keys are only used for container presets, otherwise they're `None`. The semantics are /// similar to loading state. - #[allow(clippy::wrong_self_convention)] - pub fn from_location(&self, location: &LocationValue, load_key: Option<&str>) -> Result<()> { + pub fn load_from_location(&self, location: &LocationValue, load_key: Option<&str>) -> Result<()> { let (location_kind, location_ptr) = location.to_raw(); let load_key_cstring = load_key .map(|load_key| CString::new(load_key).context("Load key contained internal null bytes")) diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index 8d2f98f..f4fb973 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -72,24 +72,27 @@ impl<'a> Extension for State<'a> { impl State<'_> { /// Retrieve the plugin's state. Returns an error if the plugin returned `false`. + #[tracing::instrument(name = "clap_plugin_state::save", level = 1, skip(self))] pub fn save(&self) -> Result> { let stream = OutputStream::new(None); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { + let _span = tracing::trace_span!("clap_plugin_state::save").entered(); clap_call! { state=>save(plugin, Proxy::vtable(&stream)) } }; if result { Ok(stream.take()) } else { - anyhow::bail!("'clap_plugin_state::save()' returned false."); + anyhow::bail!("'clap_plugin_state::save()' returned false"); } } /// Retrieve the plugin's state while limiting the number of bytes the plugin can write at a /// time. Returns an error if the plugin returned `false`. + #[tracing::instrument(name = "clap_plugin_state::save", level = 1, skip(self))] pub fn save_buffered(&self, max_bytes: usize) -> Result> { let stream = OutputStream::new(Some(max_bytes)); @@ -104,12 +107,13 @@ impl State<'_> { } else { anyhow::bail!( "'clap_plugin_state::save()' returned false when only allowing the plugin to write {max_bytes} bytes \ - at a time." + at a time" ); } } /// Restore previously stored state. Returns an error if the plugin returned `false`. + #[tracing::instrument(name = "clap_plugin_state::load", level = 1, skip_all)] pub fn load(&self, state: &[u8]) -> Result<()> { let stream = InputStream::new(state, None); @@ -122,18 +126,20 @@ impl State<'_> { if result { Ok(()) } else { - anyhow::bail!("'clap_plugin_state::load()' returned false."); + anyhow::bail!("'clap_plugin_state::load()' returned false"); } } /// Restore previously stored state while limiting the number of bytes the plugin can read at a /// time. Returns an error if the plugin returned `false`. + #[tracing::instrument(name = "clap_plugin_state::load", level = 1, skip_all)] pub fn load_buffered(&self, state: &[u8], max_bytes: usize) -> Result<()> { let stream = InputStream::new(state, Some(max_bytes)); let state = self.state.as_ptr(); let plugin = self.plugin.as_ptr(); let result = unsafe { + let _span = tracing::trace_span!("clap_plugin_state::load").entered(); clap_call! { state=>load(plugin, Proxy::vtable(&stream)) } }; @@ -142,7 +148,7 @@ impl State<'_> { } else { anyhow::bail!( "'clap_plugin_state::load()' returned false when only allowing the plugin to read {max_bytes} bytes \ - at a time." + at a time" ); } } @@ -181,6 +187,7 @@ impl<'a> InputStream<'a> { }) } + #[tracing::instrument(name = "clap_istream::read", level = 1, skip(stream))] unsafe extern "C" fn read(stream: *const clap_istream, buffer: *mut c_void, size: u64) -> i64 { unsafe { let state = Proxy::::from_vtable(stream).unwrap_or_else(|e| { @@ -228,6 +235,7 @@ impl OutputStream { std::mem::take(&mut *self.write_buffer.lock().unwrap()) } + #[tracing::instrument(name = "clap_ostream::write", level = 1, skip(stream))] unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { unsafe { let state = Proxy::::from_vtable(stream).unwrap_or_else(|e| { diff --git a/src/plugin/ext/surround.rs b/src/plugin/ext/surround.rs index 8c3988c..a6717a6 100644 --- a/src/plugin/ext/surround.rs +++ b/src/plugin/ext/surround.rs @@ -25,6 +25,7 @@ impl<'a> Extension for Surround<'a> { } impl<'a> Surround<'a> { + #[tracing::instrument(name = "clap_plugin_surround::is_channel_mask_supported", level = 1, skip(self))] pub fn is_channel_mask_supported(&self, channel_mask: u64) -> bool { let surround = self.surround.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -39,6 +40,7 @@ impl<'a> Surround<'a> { } } + #[tracing::instrument(name = "clap_plugin_surround::get_channel_map", level = 1, skip(self))] pub fn get_channel_map(&self, is_input: bool, port_index: u32, channel_count: u32) -> Vec { let surround = self.surround.as_ptr(); let plugin = self.plugin.as_ptr(); diff --git a/src/plugin/ext/tail.rs b/src/plugin/ext/tail.rs index 7b3bcda..d1af996 100644 --- a/src/plugin/ext/tail.rs +++ b/src/plugin/ext/tail.rs @@ -5,7 +5,6 @@ use clap_sys::ext::tail::{CLAP_EXT_TAIL, clap_plugin_tail}; use std::ffi::CStr; use std::ptr::NonNull; -#[allow(unused)] pub struct Tail<'a> { plugin: &'a PluginAudioThread<'a>, tail: NonNull, @@ -26,10 +25,11 @@ impl<'a> Extension for Tail<'a> { } impl<'a> Tail<'a> { - #[allow(unused)] + #[tracing::instrument(name = "clap_plugin_tail::get", level = 1, skip(self))] pub fn get(&self) -> u32 { let tail = self.tail.as_ptr(); let plugin = self.plugin.as_ptr(); + unsafe { clap_call! { tail=>get(plugin) } } diff --git a/src/plugin/ext/thread_pool.rs b/src/plugin/ext/thread_pool.rs index 0a2cd30..67b8d7b 100644 --- a/src/plugin/ext/thread_pool.rs +++ b/src/plugin/ext/thread_pool.rs @@ -28,9 +28,11 @@ impl<'a> Extension for ThreadPool<'a> { } impl<'a> ThreadPool<'a> { + #[tracing::instrument(name = "clap_plugin_thread_pool::exec", level = 1, skip(self))] pub fn exec(&self, task: u32) { let thread_pool = self.tail.as_ptr(); let plugin = self.plugin.clap_plugin; + unsafe { clap_call! { thread_pool=>exec(plugin, task) } } diff --git a/src/plugin/ext/voice_info.rs b/src/plugin/ext/voice_info.rs index 8cfcc11..1bc5f43 100644 --- a/src/plugin/ext/voice_info.rs +++ b/src/plugin/ext/voice_info.rs @@ -28,14 +28,18 @@ impl<'a> Extension for VoiceInfo<'a> { impl<'a> VoiceInfo<'a> { #[allow(unused)] + #[tracing::instrument(name = "clap_plugin_voice_info::get", level = 1, skip(self))] pub fn get(&self) -> Option { let voice_info = self.voice_info.as_ptr(); let plugin = self.plugin.as_ptr(); unsafe { let mut result = clap_voice_info { ..zeroed() }; - let success = clap_call! { voice_info=>get(plugin, &mut result) }; - if success { Some(result) } else { None } + if clap_call! { voice_info=>get(plugin, &mut result) } { + Some(result) + } else { + None + } } } } diff --git a/src/plugin/instance.rs b/src/plugin/instance.rs index ae50118..55d53fb 100644 --- a/src/plugin/instance.rs +++ b/src/plugin/instance.rs @@ -25,9 +25,12 @@ pub enum CallbackEvent { AudioPortsRescanNames, AudioPortsRescanAll, + NotePortsRescanNames, NotePortsRescanAll, + AudioPortsConfigRescan, + /// clap_plugin_latency::changed() LatencyChanged, diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 5919d77..8966beb 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -79,6 +79,7 @@ impl<'a> PluginAudioThread<'a> { /// for the task to complete and return its result. /// /// TODO: this could be optimized and the 'static requirement dropped. + #[tracing::instrument(name = "PluginAudioThread::on_main_thread", level = 1, skip(self, callback))] pub fn on_main_thread T + Send, T: Send + 'static>(&self, callback: F) -> T { struct Context { sender: SyncSender>>, @@ -117,13 +118,13 @@ impl<'a> PluginAudioThread<'a> { pub fn start_processing(&self) -> Result<()> { self.status().assert_is(PluginStatus::Activated); - let plugin = self.as_ptr(); let result = unsafe { - clap_call! { plugin=>start_processing(plugin) } + let _span = tracing::trace_span!("clap_plugin::start_processing").entered(); + clap_call! { self.as_ptr()=>start_processing(self.as_ptr()) } }; if result { - self.shared.status.store(PluginStatus::Processing); + self.shared.set_status(PluginStatus::Processing); Ok(()) } else { anyhow::bail!("'clap_plugin::start_processing()' returned false.") @@ -164,9 +165,9 @@ impl<'a> PluginAudioThread<'a> { pub fn reset(&self) { self.status().assert_active(); - let plugin = self.as_ptr(); unsafe { - clap_call! { plugin=>reset(plugin) } + let _span = tracing::trace_span!("clap_plugin::reset").entered(); + clap_call! { self.as_ptr()=>reset(self.as_ptr()) } }; } @@ -176,11 +177,11 @@ impl<'a> PluginAudioThread<'a> { pub fn stop_processing(&self) { self.status().assert_is(PluginStatus::Processing); - let plugin = self.as_ptr(); unsafe { - clap_call! { plugin=>stop_processing(plugin) } + let _span = tracing::trace_span!("clap_plugin::stop_processing").entered(); + clap_call! { self.as_ptr()=>stop_processing(self.as_ptr()) } }; - self.shared.status.store(PluginStatus::Activated); + self.shared.set_status(PluginStatus::Activated); } } diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index c7df29f..12a08c9 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -42,12 +42,14 @@ pub struct Plugin<'lib> { /// [`on_audio_thread()`][Self::on_audio_thread()] method spawns an audio thread that is able to call /// the plugin's audio thread functions. pub(super) _thread: PhantomData<*const ()>, + + pub(super) _span: tracing::span::EnteredSpan, } impl Drop for Plugin<'_> { fn drop(&mut self) { if let Some(error) = self.shared.callback_error.lock().unwrap().take() { - log::warn!( + tracing::warn!( "The validator's host has detected a callback error but this error has not been used as part of the \ test result. This could be a clap-validator bug. The error message is: {error}" ) @@ -64,6 +66,7 @@ impl Drop for Plugin<'_> { let plugin = self.as_ptr(); unsafe { + let _span = tracing::trace_span!("clap_plugin::destroy").entered(); clap_call! { plugin=>destroy(plugin) } } } @@ -121,6 +124,7 @@ impl<'lib> Plugin<'lib> { /// /// If whatever happens on the audio thread caused main-thread callback requests to be emited, /// then those will be handled concurrently. + #[tracing::instrument(name = "Plugin::on_audio_thread", level = 1, skip(self, f))] pub fn on_audio_thread T + Send>(&self, f: F) -> T { let result = crossbeam::scope(|s| { if self.shared.audio_thread_id.load().is_some() { @@ -130,7 +134,7 @@ impl<'lib> Plugin<'lib> { let shared = self.shared.clone(); let thread = s .builder() - .name("audio-thread".into()) + .name("audio".into()) .spawn(move |_| f(PluginAudioThread::new(shared))) .unwrap(); @@ -157,6 +161,7 @@ impl<'lib> Plugin<'lib> { let plugin = self.as_ptr(); let result = unsafe { + let _span = tracing::trace_span!("clap_plugin::init").entered(); clap_call! { plugin=>init(plugin) } }; @@ -167,7 +172,7 @@ impl<'lib> Plugin<'lib> { "clap_plugin::on_main_thread is null" ); - self.shared.status.store(PluginStatus::Deactivated); + self.shared.set_status(PluginStatus::Deactivated); Ok(()) } else { anyhow::bail!("'clap_plugin::init()' returned false.") @@ -185,18 +190,19 @@ impl<'lib> Plugin<'lib> { assert!(max_buffer_size >= min_buffer_size); // we need to track the `Activating` state to validate that we call clap_host_latency::changed only within the activation call. - self.shared.status.store(PluginStatus::Activating); + self.shared.set_status(PluginStatus::Activating); - let plugin = self.as_ptr(); let result = unsafe { - clap_call! { plugin=>activate(plugin, sample_rate, min_buffer_size, max_buffer_size) } + let _span = + tracing::trace_span!("clap_plugin::activate", sample_rate, min_buffer_size, max_buffer_size).entered(); + clap_call! { self.as_ptr()=>activate(self.as_ptr(), sample_rate, min_buffer_size, max_buffer_size) } }; if result { - self.shared.status.store(PluginStatus::Activated); + self.shared.set_status(PluginStatus::Activated); Ok(()) } else { - self.shared.status.store(PluginStatus::Deactivated); + self.shared.set_status(PluginStatus::Deactivated); anyhow::bail!("'clap_plugin::activate()' returned false.") } } @@ -207,19 +213,19 @@ impl<'lib> Plugin<'lib> { pub fn deactivate(&self) { self.status().assert_is(PluginStatus::Activated); - let plugin = self.as_ptr(); unsafe { - clap_call! { plugin=>deactivate(plugin) } + let _span = tracing::trace_span!("clap_plugin::deactivate").entered(); + clap_call! { self.as_ptr()=>deactivate(self.as_ptr()) } } - self.shared.status.store(PluginStatus::Deactivated); + self.shared.set_status(PluginStatus::Deactivated); } fn poll_callback_unchecked(&self) { if self.shared.requested_callback.swap(false) { - let plugin = self.as_ptr(); unsafe { - clap_call! { plugin=>on_main_thread(plugin) } + let _span = tracing::trace_span!("clap_plugin::on_main_thread").entered(); + clap_call! { self.as_ptr()=>on_main_thread(self.as_ptr()) } }; } } diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index 8f9d2e4..cd7128a 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -1,6 +1,7 @@ use crate::panic::fail_test; use crate::plugin::ext::Extension; use crate::plugin::ext::audio_ports::AudioPorts; +use crate::plugin::ext::audio_ports_config::AudioPortsConfig; use crate::plugin::ext::latency::Latency; use crate::plugin::ext::note_ports::NotePorts; use crate::plugin::ext::params::Params; @@ -14,6 +15,7 @@ use crate::plugin::preset_discovery::LocationValue; use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable, clap_call, validator_version}; use anyhow::{Context, Result}; use clap_sys::ext::audio_ports::*; +use clap_sys::ext::audio_ports_config::{CLAP_EXT_AUDIO_PORTS_CONFIG, clap_host_audio_ports_config}; use clap_sys::ext::latency::*; use clap_sys::ext::note_ports::*; use clap_sys::ext::params::*; @@ -36,6 +38,7 @@ use std::ptr::NonNull; use std::sync::Mutex; use std::sync::mpsc::{Sender, channel}; use std::thread::ThreadId; +use tracing::{Span, instrument}; /// Plugin instance state that is shared between the main thread, audio thread and any external unmanaged threads. /// This struct also acts as the `clap_host` implementation for the plugin instance. @@ -45,7 +48,7 @@ pub struct PluginShared { pub callback_error: Mutex>, /// The plugin's current state in terms of activation and processing status. - pub status: AtomicCell, + status: AtomicCell, /// The plugin instance's main thread. Used for the main thread checks. pub main_thread_id: ThreadId, @@ -99,6 +102,8 @@ impl PluginShared { /// The `factory` object must be valid. /// The caller must ensure that this is called from the OS main thread. pub unsafe fn create_plugin<'a>(factory: *const clap_plugin_factory, plugin_id: &CStr) -> Result> { + let span = tracing::debug_span!("Plugin", plugin_id = %plugin_id.to_string_lossy()); + let (callback_sender, callback_receiver) = channel(); let (task_sender, task_receiver) = channel(); @@ -138,6 +143,7 @@ impl PluginShared { _library: std::marker::PhantomData, _thread: std::marker::PhantomData, + _span: span.entered(), }) } @@ -146,10 +152,14 @@ impl PluginShared { self.status().assert_is_not(PluginStatus::Uninitialized); for id in T::IDS { + let span = tracing::trace_span!("clap_plugin::get_extension", extension_id = %id.to_string_lossy(), found = tracing::field::Empty).entered(); + let extension_ptr = unsafe { clap_call! { self.clap_plugin=>get_extension(self.clap_plugin, id.as_ptr()) } }; + span.record("found", !extension_ptr.is_null()); + if !extension_ptr.is_null() { return NonNull::new(extension_ptr as *mut T::Struct); } @@ -168,9 +178,14 @@ impl PluginShared { self.status.load() } + pub fn set_status(&self, status: PluginStatus) { + let old_status = self.status.swap(status); + tracing::trace!(from = ?old_status, to = ?status, "State transition"); + } + #[track_caller] - fn wrap(host: *const clap_host, function_name: &str, f: impl FnOnce(&Self) -> Result) -> Option { - log::trace!("'{}' was called by the plugin", function_name); + fn wrap(host: *const clap_host, f: impl FnOnce(&Self) -> Result) -> Option { + let function_name = Span::current().metadata().map_or("", |m| m.name()); let state = unsafe { Proxy::::from_vtable(host).unwrap_or_else(|e| { @@ -185,6 +200,8 @@ impl PluginShared { match f(&state) { Ok(result) => Some(result), Err(error) => { + tracing::error!("{:#}", error); + let mut guard = state.callback_error.lock().unwrap(); if guard.is_none() { *guard = Some(error.context(function_name.to_string())); @@ -310,10 +327,20 @@ impl PluginShared { changed: Some(Self::ext_voice_info_changed), }; + const EXT_AUDIO_PORTS_CONFIG: clap_host_audio_ports_config = clap_host_audio_ports_config { + rescan: Some(Self::ext_audio_ports_config_rescan), + }; + + #[instrument( + name = "clap_host::get_extension", + level = 1, + skip_all, + fields(extension_id = tracing::field::Empty, found = tracing::field::Empty) + )] unsafe extern "C" fn clap_get_extension(host: *const clap_host, extension_id: *const c_char) -> *const c_void { // Right now there's no way to have the host only expose certain extensions. We can always // add that when test cases need it. - Self::wrap(host, "clap_host::get_extension", |_| { + Self::wrap(host, |_| { if extension_id.is_null() { anyhow::bail!("Null extension ID"); } @@ -339,48 +366,58 @@ impl PluginShared { &Self::EXT_TAIL as *const _ as *const c_void } else if extension_id_cstr == CLAP_EXT_VOICE_INFO { &Self::EXT_VOICE_INFO as *const _ as *const c_void + } else if extension_id_cstr == CLAP_EXT_AUDIO_PORTS_CONFIG { + &Self::EXT_AUDIO_PORTS_CONFIG as *const _ as *const c_void } else { std::ptr::null() }; + Span::current().record("extension_id", extension_id_cstr.to_string_lossy().as_ref()); + Span::current().record("found", !extension_ptr.is_null()); + Ok(extension_ptr) }) .unwrap_or_default() } + #[instrument(name = "clap_host::request_restart", level = 1, skip(host))] unsafe extern "C" fn clap_request_restart(host: *const clap_host) { - Self::wrap(host, "clap_host::request_restart", |this| { + Self::wrap(host, |this| { this.requested_restart.store(true); Ok(()) }); } + #[instrument(name = "clap_host::request_process", level = 1, skip(host))] unsafe extern "C" fn clap_request_process(host: *const clap_host) { - Self::wrap(host, "clap_host::request_process", |this| { + Self::wrap(host, |this| { this.callback_sender.send(CallbackEvent::RequestProcess).unwrap(); Ok(()) }); } + #[instrument(name = "clap_host::request_callback", level = 1, skip(host))] unsafe extern "C" fn clap_request_callback(host: *const clap_host) { - Self::wrap(host, "clap_host::request_callback", |this| { + Self::wrap(host, |this| { this.requested_callback.store(true); this.task_sender.send(MainThreadTask::CallbackRequest).unwrap(); Ok(()) }); } - unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, _flag: u32) -> bool { - Self::wrap(host, "clap_host_audio_ports::is_rescan_flag_supported", |this| { + #[instrument(name = "clap_host_audio_ports::is_rescan_flag_supported", level = 1, skip(host))] + unsafe extern "C" fn ext_audio_ports_is_rescan_flag_supported(host: *const clap_host, flag: u32) -> bool { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; - Ok(true) + Ok(false) }) .unwrap_or(false) } + #[instrument(name = "clap_host_audio_ports::rescan", level = 1, skip(host))] unsafe extern "C" fn ext_audio_ports_rescan(host: *const clap_host, flags: u32) { - Self::wrap(host, "clap_host_audio_ports::rescan", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -401,8 +438,9 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_note_ports::supported_dialects", level = 1, skip(host))] unsafe extern "C" fn ext_note_ports_supported_dialects(host: *const clap_host) -> clap_note_dialect { - Self::wrap(host, "clap_host_note_ports::supported_dialects", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; Ok(CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_MIDI_MPE) @@ -410,8 +448,9 @@ impl PluginShared { .unwrap_or(0) } + #[instrument(name = "clap_host_note_ports::rescan", level = 1, skip(host))] unsafe extern "C" fn ext_note_ports_rescan(host: *const clap_host, flags: u32) { - Self::wrap(host, "clap_host_note_ports::rescan", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -432,6 +471,7 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_preset_load::on_error", level = 1, skip(host))] unsafe extern "C" fn ext_preset_load_on_error( host: *const clap_host, location_kind: clap_preset_discovery_location_kind, @@ -440,7 +480,7 @@ impl PluginShared { os_error: i32, msg: *const c_char, ) { - Self::wrap(host, "clap_host_preset_load::on_error", |this| -> Result<()> { + Self::wrap(host, |this| -> Result<()> { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -465,13 +505,14 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_preset_load::loaded", level = 1, skip(host))] unsafe extern "C" fn ext_preset_load_loaded( host: *const clap_host, location_kind: clap_preset_discovery_location_kind, location: *const c_char, load_key: *const c_char, ) { - Self::wrap(host, "clap_host_preset_load::loaded", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -480,13 +521,14 @@ impl PluginShared { let _load_key = unsafe { util::cstr_ptr_to_optional_string(load_key) } .context("'Called with an invalid load_key parameter")?; - log::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); + tracing::debug!("TODO: Handle 'clap_host_preset_load::loaded()'"); Ok(()) }); } + #[instrument(name = "clap_host_params::rescan", level = 1, skip(host))] unsafe extern "C" fn ext_params_rescan(host: *const clap_host, flags: clap_param_rescan_flags) { - Self::wrap(host, "clap_host_params::rescan", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -515,17 +557,19 @@ impl PluginShared { }); } - unsafe extern "C" fn ext_params_clear(host: *const clap_host, _param_id: clap_id, _flags: clap_param_clear_flags) { - Self::wrap(host, "clap_host_params::clear", |this| { + #[instrument(name = "clap_host_params::clear", level = 1, skip(host))] + unsafe extern "C" fn ext_params_clear(host: *const clap_host, param_id: clap_id, flags: clap_param_clear_flags) { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; - log::debug!("TODO: Handle 'clap_host_params::clear()'"); + tracing::debug!("TODO: Handle 'clap_host_params::clear()'"); Ok(()) }); } + #[instrument(name = "clap_host_params::request_flush", level = 1, skip(host))] unsafe extern "C" fn ext_params_request_flush(host: *const clap_host) { - Self::wrap(host, "clap_host_params::request_flush", |this| { + Self::wrap(host, |this| { this.assert_not_audio_thread()?; this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::RequestFlush).unwrap(); @@ -533,8 +577,9 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_state::mark_dirty", level = 1, skip(host))] unsafe extern "C" fn ext_state_mark_dirty(host: *const clap_host) { - Self::wrap(host, "clap_host_state::mark_dirty", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::StateMarkDirty).unwrap(); @@ -542,22 +587,22 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_thread_check::is_main_thread", level = 1, skip(host))] unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { - Self::wrap(host, "clap_host_thread_check::is_main_thread", |this| { - Ok(this.main_thread_id == std::thread::current().id()) - }) - .unwrap_or(false) + Self::wrap(host, |this| Ok(this.main_thread_id == std::thread::current().id())).unwrap_or(false) } + #[instrument(name = "clap_host_thread_check::is_audio_thread", level = 1, skip(host))] unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { - Self::wrap(host, "clap_host_thread_check::is_audio_thread", |this| { + Self::wrap(host, |this| { Ok(this.audio_thread_id.load() == Some(std::thread::current().id())) }) .unwrap_or(false) } + #[instrument(name = "clap_host_latency::changed", level = 1, skip(host))] unsafe extern "C" fn ext_latency_changed(host: *const clap_host) { - Self::wrap(host, "clap_host_latency::changed", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; @@ -572,8 +617,9 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_tail::changed", level = 1, skip(host))] unsafe extern "C" fn ext_tail_changed(host: *const clap_host) { - Self::wrap(host, "clap_host_tail::changed", |this| { + Self::wrap(host, |this| { this.assert_audio_thread()?; this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::TailChanged).unwrap(); @@ -581,8 +627,9 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_voice_info::changed", level = 1, skip(host))] unsafe extern "C" fn ext_voice_info_changed(host: *const clap_host) { - Self::wrap(host, "clap_host_voice_info::changed", |this| { + Self::wrap(host, |this| { this.assert_main_thread()?; this.assert_has_extension::()?; this.callback_sender.send(CallbackEvent::VoiceInfoChanged).unwrap(); @@ -590,8 +637,19 @@ impl PluginShared { }); } + #[instrument(name = "clap_host_audio_ports_config::rescan", level = 1, skip(host))] + unsafe extern "C" fn ext_audio_ports_config_rescan(host: *const clap_host) { + Self::wrap(host, |this| { + this.assert_main_thread()?; + this.assert_has_extension::()?; + this.callback_sender.send(CallbackEvent::AudioPortsConfigRescan).unwrap(); + Ok(()) + }); + } + + #[instrument(name = "clap_host_thread_pool::request_exec", level = 1, skip(host))] unsafe extern "C" fn ext_thread_pool_request_exec(host: *const clap_host, num_tasks: u32) -> bool { - Self::wrap(host, "clap_host_thread_pool::request_exec", |this| { + Self::wrap(host, |this| { this.assert_audio_thread()?; this.assert_has_extension::()?; diff --git a/src/plugin/library.rs b/src/plugin/library.rs index b90102f..fb06c20 100644 --- a/src/plugin/library.rs +++ b/src/plugin/library.rs @@ -33,6 +33,8 @@ pub struct PluginLibrary { /// To honor CLAP's thread safety guidelines, the thread this object was created from is /// designated the 'main thread', and this object cannot be shared with other threads. _thread: PhantomData<*const ()>, + + _span: tracing::span::EnteredSpan, } /// Metadata for a CLAP plugin library, which may contain multiple plugins. @@ -115,11 +117,6 @@ impl PluginLibrary { path: impl AsRef, load: impl FnOnce(&Path) -> Result, ) -> Result { - // assert!( - // IS_OS_MAIN_THREAD.with(|cell| cell.get()), - // "PluginLibrary must be loaded from the OS main thread" - // ); - // NOTE: We'll always make sure `path` is either relative to the current directory or // absolute. Otherwise the system libraries may be searched instead which would lead // to unexpected behavior. Joining an absolute path to a relative directory gets you @@ -128,6 +125,8 @@ impl PluginLibrary { .unwrap_or_else(|_| PathBuf::from(".")) .join(path); + let span = tracing::debug_span!("PluginLibrary", library_path = %path.display()).entered(); + // This is the path passed to `clap_entry::init()`. On macOS this should point to the // bundle, not the DSO. let path_cstring = CString::new(path.as_os_str().to_str().context("Path contains invalid UTF-8")?) @@ -169,6 +168,7 @@ impl PluginLibrary { plugin_path: path, library, _thread: PhantomData, + _span: span, }) } diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 5614386..266701b 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -358,12 +358,8 @@ impl Indexer { } #[track_caller] - fn wrap( - indexer: *const clap_preset_discovery_indexer, - function_name: &str, - f: impl FnOnce(&Self) -> Result, - ) -> Option { - log::trace!("'{}' was called by the plugin", function_name); + fn wrap(indexer: *const clap_preset_discovery_indexer, f: impl FnOnce(&Self) -> Result) -> Option { + let function_name = tracing::Span::current().metadata().map_or("", |m| m.name()); let state = unsafe { Proxy::::from_vtable(indexer).unwrap_or_else(|e| { @@ -378,6 +374,8 @@ impl Indexer { match f(&state) { Ok(result) => Some(result), Err(error) => { + tracing::error!("{:#}", error); + let mut guard = state.result.lock().unwrap(); if guard.is_ok() { *guard = Err(error.context(function_name.to_string())); @@ -405,11 +403,16 @@ impl Indexer { Ok(()) } + #[tracing::instrument( + name = "clap_preset_discovery_indexer::declare_filetype", + level = 1, + skip(indexer, filetype) + )] unsafe extern "C" fn declare_filetype( indexer: *const clap_preset_discovery_indexer, filetype: *const clap_preset_discovery_filetype, ) -> bool { - Self::wrap(indexer, "clap_preset_discovery_indexer::declare_filetype", |this| { + Self::wrap(indexer, |this| { this.assert_same_thread()?; let mut results = this.result.lock().unwrap(); @@ -425,11 +428,16 @@ impl Indexer { .unwrap_or(false) } + #[tracing::instrument( + name = "clap_preset_discovery_indexer::declare_location", + level = 1, + skip(indexer, location) + )] unsafe extern "C" fn declare_location( indexer: *const clap_preset_discovery_indexer, location: *const clap_preset_discovery_location, ) -> bool { - Self::wrap(indexer, "clap_preset_discovery_indexer::declare_location", |this| { + Self::wrap(indexer, |this| { this.assert_same_thread()?; let mut results = this.result.lock().unwrap(); @@ -444,11 +452,16 @@ impl Indexer { .unwrap_or(false) } + #[tracing::instrument( + name = "clap_preset_discovery_indexer::declare_soundpack", + level = 1, + skip(indexer, soundpack) + )] unsafe extern "C" fn declare_soundpack( indexer: *const clap_preset_discovery_indexer, soundpack: *const clap_preset_discovery_soundpack, ) -> bool { - Self::wrap(indexer, "clap_preset_discovery_indexer::declare_soundpack", |this| { + Self::wrap(indexer, |this| { this.assert_same_thread()?; let mut results = this.result.lock().unwrap(); @@ -465,11 +478,16 @@ impl Indexer { .unwrap_or(false) } + #[tracing::instrument( + name = "clap_preset_discovery_indexer::get_extension", + level = 1, + skip(indexer, extension_id) + )] unsafe extern "C" fn get_extension( indexer: *const clap_preset_discovery_indexer, extension_id: *const c_char, ) -> *const c_void { - Self::wrap(indexer, "clap_preset_discovery_indexer::get_extension", |_| { + Self::wrap(indexer, |_| { if extension_id.is_null() { anyhow::bail!("Null extension ID"); } diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index 30ad2df..57290b3 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -299,7 +299,7 @@ impl MetadataReceiver { function_name: &str, f: impl FnOnce(&Self) -> Result, ) -> Option { - log::trace!("'{}' was called by the plugin", function_name); + tracing::trace!(target: "callback", "{}", function_name); let state = unsafe { Proxy::::from_vtable(receiver).unwrap_or_else(|e| { diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 1721f34..9e5fcdb 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -10,6 +10,7 @@ mod transport; pub use buffer::*; pub use events::*; +use tracing::span::EnteredSpan; pub use transport::*; pub struct ProcessScope<'a> { @@ -21,6 +22,9 @@ pub struct ProcessScope<'a> { transport: TransportState, sample_rate: f64, + + span_active: Option, + span_processing: Option, } impl<'a> ProcessScope<'a> { @@ -42,6 +46,8 @@ impl<'a> ProcessScope<'a> { events_output: OutputEventQueue::new(), transport: TransportState::dummy(), sample_rate, + span_active: None, + span_processing: None, }) } @@ -72,6 +78,7 @@ impl<'a> ProcessScope<'a> { pub fn reset(&mut self) { if self.plugin.status() >= PluginStatus::Activated { + tracing::debug!("Resetting plugin"); self.plugin.reset(); } } @@ -85,6 +92,7 @@ impl<'a> ProcessScope<'a> { // check for requested restart if self.plugin.shared().requested_restart.load() { + tracing::debug!("Plugin has requested a restart"); self.restart(); } @@ -94,13 +102,25 @@ impl<'a> ProcessScope<'a> { let sample_rate = self.sample_rate; let buffer_size = self.buffer.samples(); + self.plugin .on_main_thread(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; + + self.span_active = Some( + tracing::debug_span! { + "Plugin::Active", + sample_rate=%sample_rate, + min_buffer_size=1, + max_buffer_size=%buffer_size + } + .entered(), + ); } // start processing if needed if self.plugin.status() == PluginStatus::Activated { self.plugin.start_processing()?; + self.span_processing = Some(tracing::debug_span!("Plugin::Processing").entered()); } // check that we dont overfill the input event queue @@ -125,6 +145,8 @@ impl<'a> ProcessScope<'a> { // run processing let status = self.buffer.process(|inputs, outputs| { + let _span = tracing::debug_span!("Plugin::Process", buffer_size = samples).entered(); + let transport = self.transport.as_clap_transport(0); self.plugin.process(&clap_process { steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), @@ -156,12 +178,12 @@ impl<'a> ProcessScope<'a> { pub fn restart(&mut self) { if self.plugin.status() == PluginStatus::Processing { self.plugin.stop_processing(); + self.span_processing.take(); } if self.plugin.status() == PluginStatus::Activated { - self.plugin.on_main_thread(|plugin| { - plugin.deactivate(); - }); + self.plugin.on_main_thread(|plugin| plugin.deactivate()); + self.span_active.take(); } } } diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index 842e7a3..107bd21 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -60,7 +60,8 @@ impl InputEventQueue { } pub fn clear(&self) { - self.0.lock().unwrap().clear(); + let mut events = self.0.lock().unwrap(); + events.clear(); } pub fn last_event_time(&self) -> Option { @@ -77,6 +78,7 @@ impl InputEventQueue { } } + #[tracing::instrument(name = "clap_input_events::size", level = 1, skip(list))] unsafe extern "C" fn size(list: *const clap_input_events) -> u32 { let state = unsafe { Proxy::::from_vtable(list).unwrap_or_else(|e| { @@ -88,9 +90,11 @@ impl InputEventQueue { fail_test!("clap_input_events::size: plugin messed with the 'ctx' pointer"); } - state.0.lock().unwrap().len() as u32 + let events = state.0.lock().unwrap(); + events.len() as u32 } + #[tracing::instrument(name = "clap_input_events::get", level = 1, skip(list))] unsafe extern "C" fn get(list: *const clap_input_events, index: u32) -> *const clap_event_header { let state = unsafe { Proxy::::from_vtable(list).unwrap_or_else(|e| { @@ -106,8 +110,8 @@ impl InputEventQueue { match events.get(index as usize) { Some(event) => event.header(), None => { - log::warn!( - "The plugin tried to get an event with index {index} ({} total events)", + tracing::warn!( + "The plugin tried to get an out of bounds event with index {index} ({} total events)", events.len() ); std::ptr::null() @@ -129,6 +133,7 @@ impl OutputEventQueue { self.0.lock().unwrap().clone() } + #[tracing::instrument(name = "clap_output_events::try_push", level = 1, skip(list, event))] unsafe extern "C" fn try_push(list: *const clap_output_events, event: *const clap_event_header) -> bool { let state = unsafe { Proxy::::from_vtable(list).unwrap_or_else(|e| { diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index dadf51f..369faa8 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -259,6 +259,8 @@ impl<'a> TestCase<'a> for PluginTestCase { } fn run(&self, (library_path, plugin_id): Self::TestArgs) -> Result { + let _span = tracing::debug_span!("PluginTestCase::run", test_case = %self, plugin_id = %plugin_id, library_path = %library_path.display()).entered(); + // SAFETY: This is called on the main thread. let library = &PluginLibrary::load(library_path) .with_context(|| format!("Could not load '{}'", library_path.display()))?; diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 2b88662..80f5798 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -231,6 +231,9 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) /// The test for `PluginTestCase::LayoutConfigurableAudioPorts`. pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: &str) -> Result { + const MAX_TOTAL_CHECKS: u32 = 200; + const MAX_PASSED_CHECKS: u32 = 20; + let mut prng = new_prng(); let plugin = library .create_plugin(plugin_id) @@ -273,7 +276,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: let mut checks_total = 0; let mut checks_passed = 0; - while checks_total < 200 && checks_passed < 20 { + while checks_total < MAX_TOTAL_CHECKS && checks_passed < MAX_PASSED_CHECKS { let requests = random_layout_requests(&config_audio_ports, &mut prng); let can_apply = configurable_audio_ports.can_apply_configuration(requests.iter().copied()); @@ -327,8 +330,9 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: if checks_passed == 0 { return Ok(TestStatus::Warning { - details: Some(String::from( - "Tried 200 random audio port layouts, but none were accepted.", + details: Some(format!( + "Tried {} random audio port layouts, but none were accepted.", + checks_total )), }); } @@ -358,6 +362,13 @@ pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &s } }; + let note_ports_config = match plugin.get_extension::() { + Some(note_ports) => note_ports + .config() + .context("Error while querying 'note-ports' IO configuration")?, + None => NotePortConfig::default(), + }; + let audio_ports_activation = match plugin.get_extension::() { Some(extension) => extension, None => { @@ -369,12 +380,5 @@ pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &s } }; - let note_ports = match plugin.get_extension::() { - Some(note_ports) => note_ports - .config() - .context("Error while querying 'note-ports' IO configuration")?, - None => NotePortConfig::default(), - }; - Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 725ff7b..6376692 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -225,8 +225,6 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for &sample_rate in SAMPLE_RATES { - log::trace!("Testing processing with sample rate: {:.2}hz", sample_rate); - plugin .on_audio_thread(|plugin| -> Result<()> { let mut note_rng = NoteGenerator::new(¬e_ports_config); @@ -274,8 +272,6 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str .unwrap_or_default(); for &buffer_size in BLOCK_SIZES { - log::trace!("Testing processing with max buffer size: {}", buffer_size); - plugin .on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size); diff --git a/src/tests/plugin_library.rs b/src/tests/plugin_library.rs index 554c038..11cbec2 100644 --- a/src/tests/plugin_library.rs +++ b/src/tests/plugin_library.rs @@ -70,6 +70,8 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { } fn run(&self, library_path: Self::TestArgs) -> Result { + let _span = tracing::debug_span!("PluginLibraryTestCase::run", test_case = %self, library_path = %library_path.display()).entered(); + match self { PluginLibraryTestCase::PresetDiscoveryCrawl => preset_discovery::test_crawl(library_path, false), PluginLibraryTestCase::PresetDiscoveryDescriptorConsistency => { diff --git a/src/tests/plugin_library/preset_discovery.rs b/src/tests/plugin_library/preset_discovery.rs index 4bf30ad..e6cea0a 100644 --- a/src/tests/plugin_library/preset_discovery.rs +++ b/src/tests/plugin_library/preset_discovery.rs @@ -137,7 +137,7 @@ pub fn test_crawl(library_path: &Path, load_presets: bool) -> Result // be loaded at any point, even when the plugin is processing audio. Test // this. let load_result = preset_load - .from_location(&preset.location, preset.load_key.as_deref()) + .load_from_location(&preset.location, preset.load_key.as_deref()) .with_context(|| { format!( "Could not load the preset '{}' for plugin '{}'", diff --git a/src/validator.rs b/src/validator.rs index 39e9fb4..35056a9 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -10,7 +10,7 @@ use crate::util::{self, IteratorExt}; use anyhow::{Context, Result}; use clap::ValueEnum; use clap_sys::version::clap_version_is_compatible; -use regex::{Regex, RegexBuilder}; +use regex_lite::Regex; use serde::Serialize; use std::collections::BTreeMap; use std::ffi::OsStr; @@ -58,15 +58,11 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result>>()?, ); @@ -103,7 +99,7 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result Result<()> { /// The filter function for determining whether or not a test should be run based on the validator's /// settings settings. -fn test_filter<'a, T: TestCase<'a>>(test: &T, settings: &ValidatorSettings, test_filter_re: &Option) -> bool { +fn test_filter<'a, T: TestCase<'a>>(test: &T, settings: &ValidatorSettings, test_filter: Option<&Regex>) -> bool { let test_name = test.to_string(); - match (&test_filter_re, settings.invert_filter) { - (Some(test_filter_re), false) if !test_filter_re.is_match(&test_name) => false, - (Some(test_filter_re), true) if test_filter_re.is_match(&test_name) => false, + match (test_filter, settings.invert_filter) { + (Some(test_filter), false) if !test_filter.is_match(&test_name) => false, + (Some(test_filter), true) if test_filter.is_match(&test_name) => false, _ => true, } } @@ -282,7 +278,7 @@ fn run_test_out_of_process<'a, T: TestCase<'a>>( command.stderr(Stdio::null()); } - let exit_status = command + let status = command .spawn() .context("Could not call clap-validator for out-of-process validation")? // The docs make it seem like this can only fail if the process isn't running, but if @@ -290,33 +286,29 @@ fn run_test_out_of_process<'a, T: TestCase<'a>>( .wait_timeout(WAIT_TIMEOUT) .context("Error while waiting on clap-validator to finish running the test")?; - match exit_status { - None => { - return Ok(TestStatus::Crashed { - details: format!("Timed out after {} seconds", WAIT_TIMEOUT.as_secs()), - }); + match status { + None => Ok(TestStatus::Crashed { + details: format!("Timed out after {} seconds", WAIT_TIMEOUT.as_secs()), + }), + + Some(status) if !status.success() => Ok(TestStatus::Crashed { + details: status.to_string(), + }), + + _ => { + // At this point, the child process _should_ have written its output to `output_file_path`, + // and we can just parse it from there + let result = serde_json::from_str(&fs::read_to_string(&output_file_path).with_context(|| { + format!( + "Could not read the child process output from '{}'", + output_file_path.display() + ) + })?) + .context("Could not parse the child process output to JSON")?; + + Ok(result) } - - Some(status) if !status.success() => { - return Ok(TestStatus::Crashed { - details: status.to_string(), - }); - } - - _ => {} } - - // At this point, the child process _should_ have written its output to `output_file_path`, - // and we can just parse it from there - let result = serde_json::from_str(&fs::read_to_string(&output_file_path).with_context(|| { - format!( - "Could not read the child process output from '{}'", - output_file_path.display() - ) - })?) - .context("Could not parse the child process output to JSON")?; - - Ok(result) } fn run_test_in_process(test: impl FnOnce() -> Result) -> TestStatus { From b897694545e92c825424f9867811d8ed33f57c29 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 4 Feb 2026 01:05:21 +0400 Subject: [PATCH 054/114] finished the tracing experiment; new logger using tracing --- Cargo.lock | 386 +----------------- Cargo.toml | 8 +- src/commands/list.rs | 2 +- src/commands/validate.rs | 11 +- src/debug.rs | 7 + src/debug/log.rs | 82 ++++ src/{ => debug}/panic.rs | 2 +- src/debug/trace.rs | 132 ++++++ src/main.rs | 79 ++-- src/plugin/ext/audio_ports_config.rs | 54 ++- src/plugin/ext/params.rs | 41 +- src/plugin/ext/state.rs | 2 +- src/plugin/instance/audio_thread.rs | 85 +++- src/plugin/instance/main_thread.rs | 13 +- src/plugin/instance/shared.rs | 8 +- src/plugin/preset_discovery/indexer.rs | 2 +- .../preset_discovery/metadata_receiver.rs | 2 +- src/plugin/process.rs | 45 +- src/plugin/process/events.rs | 34 +- src/plugin/util.rs | 2 +- src/tests/plugin.rs | 7 +- src/tests/plugin/layout.rs | 4 +- src/tests/plugin/params.rs | 19 +- src/tests/plugin/processing.rs | 90 ++-- src/tests/plugin/transport.rs | 8 +- src/tests/plugin_library.rs | 6 +- src/tests/rng.rs | 7 + src/validator.rs | 26 +- 28 files changed, 593 insertions(+), 571 deletions(-) create mode 100644 src/debug.rs create mode 100644 src/debug/log.rs rename src/{ => debug}/panic.rs (97%) create mode 100644 src/debug/trace.rs diff --git a/Cargo.lock b/Cargo.lock index c198449..6a6db11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.3.2" @@ -66,12 +57,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "bitflags" version = "1.3.2" @@ -84,47 +69,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "clack-common" version = "0.1.0" @@ -191,13 +141,12 @@ dependencies = [ "anyhow", "clap", "clap-sys 0.5.0 (git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156)", - "colored", "core-foundation", "crossbeam", "either", "libloading", "midi-consts", - "rand 0.9.2", + "rand", "rand_pcg", "rayon", "regex-lite", @@ -210,10 +159,10 @@ dependencies = [ "textwrap", "time", "tracing", - "tracing-perfetto", "tracing-subscriber", "wait-timeout", "walkdir", + "yansi", ] [[package]] @@ -253,15 +202,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "colored" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -366,23 +306,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "getrandom" version = "0.3.4" @@ -413,30 +336,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -459,31 +358,12 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -524,12 +404,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - [[package]] name = "memchr" version = "2.5.0" @@ -542,30 +416,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -599,29 +455,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "quote" version = "1.0.42" @@ -637,35 +470,14 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -675,16 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -693,7 +496,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.4", + "getrandom", ] [[package]] @@ -702,7 +505,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" dependencies = [ - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -786,12 +589,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "ryu" version = "1.0.15" @@ -859,18 +656,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - [[package]] name = "smawk" version = "0.3.2" @@ -957,16 +742,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "thread-id" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "thread_local" version = "1.1.9" @@ -1035,34 +810,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-perfetto" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf599f51530a7211f5aa92c84abdfc1137362d471f0473a4b1be8d625167fb0d" -dependencies = [ - "anyhow", - "bytes", - "chrono", - "prost", - "rand 0.8.5", - "thread-id", - "tracing", - "tracing-subscriber", ] [[package]] @@ -1071,12 +818,9 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ - "nu-ansi-term", "sharded-slab", - "smallvec", "thread_local", "tracing-core", - "tracing-log", ] [[package]] @@ -1103,12 +847,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "wait-timeout" version = "0.2.1" @@ -1128,12 +866,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1143,51 +875,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1219,65 +906,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -1432,3 +1066,9 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 3dbda99..0cc0caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ either = "1.9.0" clap = { version = "4.1.8", features = ["derive", "wrap_help"] } # For CLAP 1.2.2 support clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } -colored = "3.0.0" crossbeam = "0.8.4" libloading = "0.9.0" midi-consts = "0.1.0" @@ -40,10 +39,9 @@ time = { version = "0.3", features = ["serde"]} walkdir = "2.3" wait-timeout = "0.2.1" rustc-hash = "2.1.1" - -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-perfetto = "0.1.5" +tracing = { version = "0.1.44", features = ["max_level_off"] } +tracing-subscriber = { version = "0.3.18", default-features = false, features = ["registry"] } +yansi = "1.0.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" diff --git a/src/commands/list.rs b/src/commands/list.rs index eae0e53..ca3aa50 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -5,9 +5,9 @@ use crate::index::PresetIndexResult; use crate::plugin::preset_discovery::PresetFile; use anyhow::{Context, Result}; use clap::Subcommand; -use colored::Colorize; use std::path::{Path, PathBuf}; use std::process::ExitCode; +use yansi::Paint; /// Commands for listing tests and data realted to the installed plugins. #[derive(Subcommand)] diff --git a/src/commands/validate.rs b/src/commands/validate.rs index 1d3c09d..9ed2025 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -5,9 +5,9 @@ use crate::tests::{TestResult, TestStatus}; use crate::{Verbosity, validator}; use anyhow::{Context, Result}; use clap::Args; -use colored::Colorize; use std::path::PathBuf; use std::process::ExitCode; +use yansi::Paint; /// Options for the validator. #[derive(Debug, Args)] @@ -54,6 +54,12 @@ pub struct ValidatorSettings { /// --in-process option is used. Can be useful for keeping plugin output in the correct order. #[arg(long, conflicts_with = "in_process")] pub no_parallel: bool, + /// When running the validation in-process, emit a JSON trace file that can be viewed with + /// Chrome's tracing viewer or . + /// + /// This has a non-negligible performance impact. + #[arg(long, requires = "in_process")] + pub trace: bool, } /// Options for running a single test. This is used for the out-of-process testing method. This @@ -132,11 +138,12 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result "PASSED".green(), - TestStatus::Skipped { .. } => "SKIPPED".dimmed(), + TestStatus::Skipped { .. } => "SKIPPED".dim(), TestStatus::Warning { .. } => "WARNING".yellow(), TestStatus::Failed { .. } => "FAILED".red(), TestStatus::Crashed { .. } => "CRASHED".red().bold(), }; + let test_result = match test.status.details() { Some(reason) => format!(" {status_text}: {reason}"), None => format!(" {status_text}"), diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..b0ce5f7 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,7 @@ +mod log; +mod panic; +mod trace; + +pub use log::*; +pub use panic::*; +pub use trace::*; diff --git a/src/debug/log.rs b/src/debug/log.rs new file mode 100644 index 0000000..2cf601e --- /dev/null +++ b/src/debug/log.rs @@ -0,0 +1,82 @@ +//! A tracing layer that logs events to standard output in compact human readable format. + +use std::fmt::{Debug, Write}; +use std::time::Instant; +use tracing::field::Field; +use yansi::Paint; + +pub struct LogStderrLayer { + _inner: std::marker::PhantomData, + start: Instant, +} + +impl LogStderrLayer { + pub fn new() -> Self { + Self { + _inner: std::marker::PhantomData, + start: Instant::now(), + } + } +} + +impl tracing_subscriber::Layer for LogStderrLayer +where + S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) { + thread_local! { + static BUFFER: std::cell::RefCell = std::cell::RefCell::new(String::with_capacity(256)); + } + + let elapsed = self.start.elapsed(); + let prefix = match *event.metadata().level() { + tracing::Level::ERROR => "ERROR".red().bold(), + tracing::Level::WARN => " WARN".yellow().bold(), + tracing::Level::INFO => " INFO".cyan().bold(), + tracing::Level::DEBUG => "DEBUG".white().bold(), + tracing::Level::TRACE => "TRACE".dim().bold(), + }; + + BUFFER.with_borrow_mut(|buffer| { + buffer.clear(); + write!(buffer, "{}{}", elapsed.as_millis().dim(), "ms".dim()).ok(); + write!(buffer, " {}: ", prefix).ok(); + event.record(&mut WriteMessage(buffer)); + event.record(&mut WriteFields(buffer)); + writeln!(buffer).ok(); + eprint!("{}", buffer); + }); + } +} + +struct WriteMessage<'a>(&'a mut String); + +struct WriteFields<'a>(&'a mut String); + +impl<'a> tracing::field::Visit for WriteMessage<'a> { + fn record_debug(&mut self, field: &Field, value: &dyn Debug) { + if field.name() == "message" { + write!(self.0, "{:?}", value).unwrap(); + } + } + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "message" { + write!(self.0, "{}", value).unwrap(); + } + } +} + +impl<'a> tracing::field::Visit for WriteFields<'a> { + fn record_debug(&mut self, field: &Field, value: &dyn Debug) { + if field.name() != "message" { + write!(self.0, " {:?}", value.italic().dim()).unwrap(); + } + } + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() != "message" { + write!(self.0, " {}", value.italic().dim()).unwrap(); + } + } +} diff --git a/src/panic.rs b/src/debug/panic.rs similarity index 97% rename from src/panic.rs rename to src/debug/panic.rs index a7b9ecf..68caeb2 100644 --- a/src/panic.rs +++ b/src/debug/panic.rs @@ -60,7 +60,7 @@ pub struct TestFailure(pub String); /// Prefer regular error handling where possible. macro_rules! fail_test { ($($arg:tt)*) => { - std::panic::panic_any($crate::panic::TestFailure(format!($($arg)*))) + std::panic::panic_any($crate::debug::TestFailure(format!($($arg)*))) }; } diff --git a/src/debug/trace.rs b/src/debug/trace.rs new file mode 100644 index 0000000..e4b0928 --- /dev/null +++ b/src/debug/trace.rs @@ -0,0 +1,132 @@ +//! A tracing layer that outputs Chrome JSON trace files. + +use std::collections::BTreeMap; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::marker::PhantomData; +use std::path::Path; +use std::sync::Mutex; +use std::time::Instant; +use tracing::Subscriber; +use tracing::span::{Attributes, Id, Record}; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; +use tracing_subscriber::registry::LookupSpan; + +pub struct ChromeJsonLayer { + start: Instant, + writer: Mutex>, + _inner: PhantomData, +} + +impl ChromeJsonLayer { + pub fn new(path: impl AsRef) -> Self { + let mut file = BufWriter::new(File::create(path).unwrap()); + file.write_all(b"[\n").unwrap(); + + Self { + start: Instant::now(), + writer: Mutex::new(file), + _inner: PhantomData, + } + } + + fn emit(&self, event: Trace) { + let mut writer = self.writer.lock().unwrap(); + serde_json::to_writer(&mut *writer, &event).unwrap(); + writer.write_all(b",\n").unwrap(); + writer.flush().unwrap(); + } +} + +impl Layer for ChromeJsonLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_new_span(&self, attrs: &Attributes<'_>, _id: &Id, ctx: Context<'_, S>) { + let time = self.start.elapsed().as_micros(); + + let mut data = TraceArgs::default(); + attrs.record(&mut data); + + self.emit(Trace { + name: attrs.metadata().name(), + cat: std::thread::current().name().unwrap_or("?"), + ts: time, + id: 1, + pid: 1, + ph: "b", + args: &data, + }); + + ctx.span(_id).unwrap().extensions_mut().insert(data); + } + + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + let time = self.start.elapsed().as_micros(); + + let mut args = TraceArgs::default(); + event.record(&mut args); + + self.emit(Trace { + name: args.values.get("message").map(|s| s.as_str()).unwrap_or("?"), + cat: std::thread::current().name().unwrap_or("?"), + ts: time, + id: 1, + pid: 1, + ph: "n", + args: &args, + }); + } + + fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) { + let span = ctx.span(id).unwrap(); + if let Some(args) = span.extensions_mut().get_mut::() { + values.record(args); + } + } + + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let time = self.start.elapsed().as_micros(); + let span = ctx.span(&id).unwrap(); + + if let Some(args) = span.extensions().get::() { + self.emit(Trace { + name: span.name(), + cat: std::thread::current().name().unwrap_or("?"), + ts: time, + id: 1, + pid: 1, + ph: "e", + args, + }); + } + } +} + +#[derive(serde::Serialize)] +struct Trace<'a> { + name: &'a str, + cat: &'a str, + ts: u128, + id: u64, + pid: u64, + ph: &'a str, + args: &'a TraceArgs, +} + +#[derive(serde::Serialize, Default)] +#[serde(transparent)] +struct TraceArgs { + values: BTreeMap<&'static str, String>, +} + +impl tracing::field::Visit for TraceArgs { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + self.values.insert(field.name(), format!("{value:?}")); + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + self.values.insert(field.name(), value.to_string()); + } +} diff --git a/src/main.rs b/src/main.rs index aaf5936..beec563 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ use clap::{Parser, Subcommand, ValueEnum}; use std::process::ExitCode; use tracing::level_filters::LevelFilter; -use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::Layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; +use yansi::Paint; mod commands; +mod debug; mod index; -mod panic; mod plugin; mod tests; mod util; @@ -58,43 +59,35 @@ enum Command { fn main() -> ExitCode { let cli = Cli::parse(); - let subscriber_fmt = tracing_subscriber::fmt::Layer::new() - .without_time() - .pretty() - .with_span_events(FmtSpan::ACTIVE) - .with_thread_names(true) - .with_target(false); + // Before doing anything, we need to make sure any temporary artifact files from the previous + // run are cleaned up. These are used for things like state dumps when one of the state tests + // fail. This is allowed to fail since the directory may not exist and even if it does and we + // cannot remove it, then that may not be a problem. + let _ = std::fs::remove_dir_all(util::validator_temp_dir()); + let _ = std::fs::create_dir_all(util::validator_temp_dir()); - let subscriber_perfetto = tracing_perfetto::PerfettoLayer::new(std::sync::Mutex::new( - std::fs::File::create("/tmp/test.pftrace").unwrap(), - )) - .with_debug_annotations(true); + let trace_path = util::validator_temp_dir().join("trace.json"); + let trace_enabled = match &cli.command { + Command::Validate(settings) => settings.trace, + _ => false, + }; + + let log_level = match cli.verbosity { + Verbosity::Quiet => LevelFilter::OFF, + Verbosity::Error => LevelFilter::ERROR, + Verbosity::Warn => LevelFilter::WARN, + Verbosity::Info => LevelFilter::INFO, + Verbosity::Debug => LevelFilter::DEBUG, + Verbosity::Trace => LevelFilter::TRACE, + }; tracing_subscriber::registry() - .with(subscriber_perfetto) - .with(subscriber_fmt) + .with(debug::LogStderrLayer::new().with_filter(log_level)) + .with(trace_enabled.then(|| debug::ChromeJsonLayer::new(&trace_path))) .init(); - // simplelog::TermLogger::init( - // match cli.verbosity { - // Verbosity::Quiet => simplelog::LevelFilter::Off, - // Verbosity::Error => simplelog::LevelFilter::Error, - // Verbosity::Warn => simplelog::LevelFilter::Warn, - // Verbosity::Info => simplelog::LevelFilter::Info, - // Verbosity::Debug => simplelog::LevelFilter::Debug, - // Verbosity::Trace => simplelog::LevelFilter::Trace, - // }, - // simplelog::ConfigBuilder::new() - // .set_thread_mode(simplelog::ThreadLogMode::Both) - // .set_location_level(simplelog::LevelFilter::Debug) - // .build(), - // simplelog::TerminalMode::Stderr, - // simplelog::ColorChoice::Auto, - // ) - // .expect("Could not initialize logger"); - // Install the panic hook to log panics instead of printing them to stderr. - panic::install_panic_hook(); + debug::install_panic_hook(); // Mark the main thread as such for plugin instance creation checks. unsafe { @@ -107,11 +100,25 @@ fn main() -> ExitCode { Command::List(command) => commands::list::list(&command), }; - match result { - Ok(exit_code) => exit_code, + let status = match &result { + Ok(code) => *code, Err(err) => { - tracing::error!("{err:?}"); + tracing::error!("{err:#}"); ExitCode::FAILURE } + }; + + if trace_enabled { + eprintln!( + "{}", + format!( + "Trace written to '{}'. Use 'https://ui.perfetto.dev/ to view it.", + trace_path.display() + ) + .dim() + .italic() + ); } + + status } diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 1769734..45d9156 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -71,19 +71,9 @@ impl AudioPortsConfig<'_> { let ext_ambisonic = self.plugin.get_extension::(); let ext_surround = self.plugin.get_extension::(); - let audio_ports_config = self.audio_ports_config.as_ptr(); - let plugin = self.plugin.as_ptr(); - let count = unsafe { - clap_call! { audio_ports_config=>count(plugin) } - }; - - (0..count) + (0..self.get_raw_config_count()) .map(|i| unsafe { - let mut info = clap_audio_ports_config { ..zeroed() }; - let result = clap_call! { audio_ports_config=>get(plugin, i, &mut info) }; - if !result { - anyhow::bail!("audio_ports_config::get({}) returned false", i); - } + let info = self.get_raw_config_info(i)?; if info.has_main_input { let port_type = if info.main_input_port_type.is_null() { @@ -137,20 +127,49 @@ impl AudioPortsConfig<'_> { pub fn select(&self, config_id: clap_id) -> Result<()> { let audio_ports_config = self.audio_ports_config.as_ptr(); let plugin = self.plugin.as_ptr(); - let result = unsafe { - clap_call! { audio_ports_config=>select(plugin, config_id) } - }; - if !result { - anyhow::bail!("audio_ports_config::select() returned false"); + unsafe { + if !clap_call! { audio_ports_config=>select(plugin, config_id) } { + anyhow::bail!("audio_ports_config::select() returned false"); + } } Ok(()) } + + #[tracing::instrument(name = "clap_plugin_audio_ports_config::count", level = 1, skip(self))] + fn get_raw_config_count(&self) -> u32 { + let audio_ports_config = self.audio_ports_config.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + clap_call! { audio_ports_config=>count(plugin) } + } + } + + #[tracing::instrument(name = "clap_plugin_audio_ports_config::get", level = 1, skip(self))] + fn get_raw_config_info(&self, index: u32) -> Result { + let audio_ports_config = self.audio_ports_config.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut info = clap_audio_ports_config { ..zeroed() }; + if !clap_call! { audio_ports_config=>get(plugin, index, &mut info) } { + anyhow::bail!( + "audio_ports_config::get({}) returned false ({} total configs)", + index, + self.get_raw_config_count() + ); + } + + Ok(info) + } + } } impl AudioPortsConfigInfo<'_> { /// Get the current selected audio ports configuration ID. + #[tracing::instrument(name = "clap_plugin_audio_ports_config_info::current_config", level = 1, skip(self))] pub fn current(&self) -> clap_id { let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -161,6 +180,7 @@ impl AudioPortsConfigInfo<'_> { } /// Get information about an audio port for a configuration. + #[tracing::instrument(name = "clap_plugin_audio_ports_config_info::get", level = 1, skip(self))] pub fn get(&self, config_id: clap_id, is_input: bool, port_index: u32) -> Result { let info = unsafe { let audio_ports_config_info = self.audio_ports_config_info.as_ptr(); diff --git a/src/plugin/ext/params.rs b/src/plugin/ext/params.rs index a115c40..6b2824e 100644 --- a/src/plugin/ext/params.rs +++ b/src/plugin/ext/params.rs @@ -131,24 +131,12 @@ impl Params<'_> { /// BTreeMap to ensure the order is consistent between runs. pub fn info(&self) -> Result { let mut result = BTreeMap::new(); - - let params = self.params.as_ptr(); - let plugin = self.plugin.as_ptr(); - let num_params = unsafe { - clap_call! { params=>count(plugin) } - }; + let num_params = self.get_raw_param_count(); // Right now this is only used to make sure the plugin doesn't have multiple bypass parameters let mut bypass_parameter_id = None; for i in 0..num_params { - let mut info: clap_param_info = unsafe { std::mem::zeroed() }; - let success = unsafe { - clap_call! { params=>get_info(plugin, i, &mut info) } - }; - - if !success { - anyhow::bail!("Plugin returned false when querying parameter {i} ({num_params} total parameters)."); - } + let info = self.get_raw_param_info(i)?; if info.id == CLAP_INVALID_ID { anyhow::bail!("The stable ID for parameter {i} is `CLAP_INVALID_ID`."); @@ -337,6 +325,31 @@ impl Params<'_> { }; } } + + #[tracing::instrument(name = "clap_plugin_params::count", level = 1, skip(self))] + fn get_raw_param_count(&self) -> u32 { + let params = self.params.as_ptr(); + let plugin = self.plugin.as_ptr(); + unsafe { + clap_call! { params=>count(plugin) } + } + } + + #[tracing::instrument(name = "clap_plugin_params::get_info", level = 1, skip(self))] + fn get_raw_param_info(&self, index: u32) -> Result { + let params = self.params.as_ptr(); + let plugin = self.plugin.as_ptr(); + + unsafe { + let mut info = clap_param_info { ..std::mem::zeroed() }; + if !clap_call! { params=>get_info(plugin, index, &mut info) } { + let num_params = self.get_raw_param_count(); + anyhow::bail!("Plugin returned false when querying parameter {index} ({num_params} total parameters)."); + } + + Ok(info) + } + } } impl Param { diff --git a/src/plugin/ext/state.rs b/src/plugin/ext/state.rs index f4fb973..044f0c6 100644 --- a/src/plugin/ext/state.rs +++ b/src/plugin/ext/state.rs @@ -1,7 +1,7 @@ //! Abstractions for interacting with the `state` extension. use super::Extension; -use crate::panic::fail_test; +use crate::debug::fail_test; use crate::plugin::instance::Plugin; use crate::plugin::util::{CHECK_POINTER, Proxy, Proxyable, clap_call}; use anyhow::Result; diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index 8966beb..e5ed2c5 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -3,8 +3,11 @@ use super::{Plugin, PluginStatus}; use crate::plugin::ext::Extension; use crate::plugin::instance::{CallbackEvent, MainThreadTask, PluginShared}; +use crate::plugin::process::{InputEventQueue, OutputEventQueue}; use crate::plugin::util::{Proxy, clap_call}; use anyhow::Result; +use clap_sys::audio_buffer::clap_audio_buffer; +use clap_sys::events::clap_event_transport; use clap_sys::plugin::clap_plugin; use clap_sys::process::*; use std::any::Any; @@ -12,6 +15,7 @@ use std::marker::PhantomData; use std::mem::MaybeUninit; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::mpsc::SyncSender; +use std::fmt::Debug; /// An audio thread equivalent to [`Plugin`]. This version only allows audio thread functions to be /// called. It can be constructed using [`Plugin::on_audio_thread()`]. @@ -25,11 +29,13 @@ pub struct PluginAudioThread<'a> { /// To honor CLAP's thread safety guidelines, the thread this object was created from is /// designated the 'audio thread', and this object cannot be shared with other threads. _send_sync_marker: PhantomData<*const ()>, + + _span: tracing::span::EnteredSpan, } /// The equivalent of `clap_process_status`, minus the `CLAP_PROCESS_ERROR` value as this is already /// treated as an error by `PluginAudioThread::process()`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum ProcessStatus { Continue, ContinueIfNotQuiet, @@ -37,6 +43,28 @@ pub enum ProcessStatus { Sleep, } +#[derive(Debug)] +pub struct ProcessInfo<'a> { + pub frames_count: u32, + pub steady_time: Option, + pub transport: Option<&'a clap_event_transport>, + pub audio_inputs: &'a [clap_audio_buffer], + pub audio_outputs: &'a mut [clap_audio_buffer], + pub input_events: &'a Proxy, + pub output_events: &'a Proxy, +} + +impl Debug for ProcessStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProcessStatus::Continue => write!(f, "CLAP_PROCESS_CONTINUE"), + ProcessStatus::ContinueIfNotQuiet => write!(f, "CLAP_PROCESS_CONTINUE_IF_NOT_QUIET"), + ProcessStatus::Tail => write!(f, "CLAP_PROCESS_TAIL"), + ProcessStatus::Sleep => write!(f, "CLAP_PROCESS_SLEEP"), + } + } +} + impl Drop for PluginAudioThread<'_> { fn drop(&mut self) { self.shared.audio_thread_id.store(None); @@ -46,11 +74,18 @@ impl Drop for PluginAudioThread<'_> { impl<'a> PluginAudioThread<'a> { pub(super) fn new(shared: Proxy) -> PluginAudioThread<'a> { + let span = tracing::info_span!( + "AudioThread", + plugin_id = %shared.plugin_id.to_string_lossy() + ).entered(); + shared.audio_thread_id.store(Some(std::thread::current().id())); + PluginAudioThread { shared, _plugin_marker: PhantomData, _send_sync_marker: PhantomData, + _span: span, } } @@ -115,11 +150,11 @@ impl<'a> PluginAudioThread<'a> { /// Prepare for audio processing. Returns an error if the plugin returned `false`. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. + #[tracing::instrument(name = "clap_plugin::start_processing", level = 1, skip(self))] pub fn start_processing(&self) -> Result<()> { self.status().assert_is(PluginStatus::Activated); let result = unsafe { - let _span = tracing::trace_span!("clap_plugin::start_processing").entered(); clap_call! { self.as_ptr()=>start_processing(self.as_ptr()) } }; @@ -135,38 +170,62 @@ impl<'a> PluginAudioThread<'a> { /// status code, then this will return an error. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. - pub fn process(&self, process_data: &clap_process) -> Result { + #[tracing::instrument( + name = "clap_plugin::process", + level = 1, + skip_all, + fields( + frames_count = process.frames_count, + steady_time = process.steady_time.map(|t| t as i64).unwrap_or(-1), + transport = ?process.transport, + result = tracing::field::Empty, + ) + )] + pub fn process(&self, process: ProcessInfo) -> Result { self.status().assert_is(PluginStatus::Processing); self.shared.is_currently_in_process_call.store(true); - let plugin = self.as_ptr(); let result = unsafe { - clap_call! { plugin=>process(plugin, process_data) } + clap_call! { self.as_ptr()=>process(self.as_ptr(), &clap_process { + frames_count: process.frames_count, + steady_time: process.steady_time.map(|t| t as i64).unwrap_or(-1), + transport: process.transport.map_or(std::ptr::null(), |t| t as *const clap_event_transport), + audio_inputs: process.audio_inputs.as_ptr(), + audio_outputs: process.audio_outputs.as_mut_ptr(), + audio_inputs_count: process.audio_inputs.len() as u32, + audio_outputs_count: process.audio_outputs.len() as u32, + in_events: Proxy::vtable(process.input_events), + out_events: Proxy::vtable(process.output_events), + }) } }; self.shared.is_currently_in_process_call.store(false); - match result { + let result = match result { CLAP_PROCESS_ERROR => { anyhow::bail!("The plugin returned 'CLAP_PROCESS_ERROR' from 'clap_plugin::process()'.") } - CLAP_PROCESS_CONTINUE => Ok(ProcessStatus::Continue), - CLAP_PROCESS_CONTINUE_IF_NOT_QUIET => Ok(ProcessStatus::ContinueIfNotQuiet), - CLAP_PROCESS_TAIL => Ok(ProcessStatus::Tail), - CLAP_PROCESS_SLEEP => Ok(ProcessStatus::Sleep), + CLAP_PROCESS_CONTINUE => ProcessStatus::Continue, + CLAP_PROCESS_CONTINUE_IF_NOT_QUIET => ProcessStatus::ContinueIfNotQuiet, + CLAP_PROCESS_TAIL => ProcessStatus::Tail, + CLAP_PROCESS_SLEEP => ProcessStatus::Sleep, result => anyhow::bail!( "The plugin returned an unknown 'clap_process_status' value {result} from 'clap_plugin::process()'." ), - } + }; + + tracing::Span::current().record("result", tracing::field::debug(&result)); + + Ok(result) } /// Reset the internal state of the plugin. + #[tracing::instrument(name = "clap_plugin::reset", level = 1, skip(self))] pub fn reset(&self) { self.status().assert_active(); unsafe { - let _span = tracing::trace_span!("clap_plugin::reset").entered(); clap_call! { self.as_ptr()=>reset(self.as_ptr()) } }; } @@ -174,11 +233,11 @@ impl<'a> PluginAudioThread<'a> { /// Stop processing audio. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. + #[tracing::instrument(name = "clap_plugin::stop_processing", level = 1, skip(self))] pub fn stop_processing(&self) { self.status().assert_is(PluginStatus::Processing); unsafe { - let _span = tracing::trace_span!("clap_plugin::stop_processing").entered(); clap_call! { self.as_ptr()=>stop_processing(self.as_ptr()) } }; diff --git a/src/plugin/instance/main_thread.rs b/src/plugin/instance/main_thread.rs index 12a08c9..dbaaf8b 100644 --- a/src/plugin/instance/main_thread.rs +++ b/src/plugin/instance/main_thread.rs @@ -43,6 +43,7 @@ pub struct Plugin<'lib> { /// the plugin's audio thread functions. pub(super) _thread: PhantomData<*const ()>, + /// Tracing span entered when this plugin instance was created pub(super) _span: tracing::span::EnteredSpan, } @@ -156,19 +157,18 @@ impl<'lib> Plugin<'lib> { } /// Initialize the plugin. This needs to be called before doing anything else. + #[tracing::instrument(name = "clap_plugin::init", level = 1, skip(self))] pub fn init(&self) -> Result<()> { self.status().assert_is(PluginStatus::Uninitialized); - let plugin = self.as_ptr(); let result = unsafe { - let _span = tracing::trace_span!("clap_plugin::init").entered(); - clap_call! { plugin=>init(plugin) } + clap_call! { self.as_ptr()=>init(self.as_ptr()) } }; if result { // If the plugin never calls `request_callback`, the validator won't catch this anyhow::ensure!( - unsafe { (*plugin).on_main_thread.is_some() }, + unsafe { (*self.as_ptr()).on_main_thread.is_some() }, "clap_plugin::on_main_thread is null" ); @@ -182,6 +182,7 @@ impl<'lib> Plugin<'lib> { /// Activate the plugin. Returns an error if the plugin returned `false`. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. + #[tracing::instrument(name = "clap_plugin::activate", level = 1, skip(self))] pub fn activate(&self, sample_rate: f64, min_buffer_size: u32, max_buffer_size: u32) -> Result<()> { self.status().assert_is(PluginStatus::Deactivated); @@ -193,8 +194,6 @@ impl<'lib> Plugin<'lib> { self.shared.set_status(PluginStatus::Activating); let result = unsafe { - let _span = - tracing::trace_span!("clap_plugin::activate", sample_rate, min_buffer_size, max_buffer_size).entered(); clap_call! { self.as_ptr()=>activate(self.as_ptr(), sample_rate, min_buffer_size, max_buffer_size) } }; @@ -210,11 +209,11 @@ impl<'lib> Plugin<'lib> { /// Deactivate the plugin. See /// [plugin.h](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h) for the /// preconditions. + #[tracing::instrument(name = "Plugin::deactivate", level = 1, skip(self))] pub fn deactivate(&self) { self.status().assert_is(PluginStatus::Activated); unsafe { - let _span = tracing::trace_span!("clap_plugin::deactivate").entered(); clap_call! { self.as_ptr()=>deactivate(self.as_ptr()) } } diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index cd7128a..644e554 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -1,4 +1,4 @@ -use crate::panic::fail_test; +use crate::debug::fail_test; use crate::plugin::ext::Extension; use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::audio_ports_config::AudioPortsConfig; @@ -33,7 +33,7 @@ use clap_sys::plugin::clap_plugin; use clap_sys::version::CLAP_VERSION; use crossbeam::atomic::AtomicCell; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use std::ffi::{CStr, c_char, c_void}; +use std::ffi::{CStr, CString, c_char, c_void}; use std::ptr::NonNull; use std::sync::Mutex; use std::sync::mpsc::{Sender, channel}; @@ -50,6 +50,9 @@ pub struct PluginShared { /// The plugin's current state in terms of activation and processing status. status: AtomicCell, + /// The plugin's unique identifier. + pub plugin_id: CString, + /// The plugin instance's main thread. Used for the main thread checks. pub main_thread_id: ThreadId, @@ -113,6 +116,7 @@ impl PluginShared { callback_error: Mutex::new(None), status: AtomicCell::new(PluginStatus::Uninitialized), + plugin_id: plugin_id.to_owned(), main_thread_id: std::thread::current().id(), audio_thread_id: AtomicCell::new(None), requested_callback: AtomicCell::new(false), diff --git a/src/plugin/preset_discovery/indexer.rs b/src/plugin/preset_discovery/indexer.rs index 266701b..266aff9 100644 --- a/src/plugin/preset_discovery/indexer.rs +++ b/src/plugin/preset_discovery/indexer.rs @@ -1,7 +1,7 @@ //! The indexer abstraction for a CLAP plugin's preset discovery factory. During initialization the //! plugin fills this object with its supported locations, file types, and sound packs. -use crate::panic::fail_test; +use crate::debug::fail_test; use crate::plugin::preset_discovery::parse_timestamp; use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable, validator_version}; use anyhow::{Context, Result}; diff --git a/src/plugin/preset_discovery/metadata_receiver.rs b/src/plugin/preset_discovery/metadata_receiver.rs index 57290b3..4b42d77 100644 --- a/src/plugin/preset_discovery/metadata_receiver.rs +++ b/src/plugin/preset_discovery/metadata_receiver.rs @@ -3,7 +3,7 @@ //! one or more presets to. use super::{Flags, LocationValue}; -use crate::panic::fail_test; +use crate::debug::fail_test; use crate::plugin::preset_discovery::parse_timestamp; use crate::plugin::util::{self, CHECK_POINTER, Proxy, Proxyable}; use anyhow::{Context, Result}; diff --git a/src/plugin/process.rs b/src/plugin/process.rs index 9e5fcdb..46b933c 100644 --- a/src/plugin/process.rs +++ b/src/plugin/process.rs @@ -1,8 +1,7 @@ //! Data structures and functions surrounding audio processing. -use crate::plugin::instance::{PluginAudioThread, PluginStatus, ProcessStatus}; +use crate::plugin::instance::{PluginAudioThread, PluginStatus, ProcessInfo, ProcessStatus}; use crate::plugin::util::Proxy; use anyhow::Result; -use clap_sys::process::*; mod buffer; mod events; @@ -10,7 +9,6 @@ mod transport; pub use buffer::*; pub use events::*; -use tracing::span::EnteredSpan; pub use transport::*; pub struct ProcessScope<'a> { @@ -22,9 +20,6 @@ pub struct ProcessScope<'a> { transport: TransportState, sample_rate: f64, - - span_active: Option, - span_processing: Option, } impl<'a> ProcessScope<'a> { @@ -46,8 +41,6 @@ impl<'a> ProcessScope<'a> { events_output: OutputEventQueue::new(), transport: TransportState::dummy(), sample_rate, - span_active: None, - span_processing: None, }) } @@ -78,7 +71,6 @@ impl<'a> ProcessScope<'a> { pub fn reset(&mut self) { if self.plugin.status() >= PluginStatus::Activated { - tracing::debug!("Resetting plugin"); self.plugin.reset(); } } @@ -105,22 +97,11 @@ impl<'a> ProcessScope<'a> { self.plugin .on_main_thread(move |plugin| plugin.activate(sample_rate, 1, buffer_size))?; - - self.span_active = Some( - tracing::debug_span! { - "Plugin::Active", - sample_rate=%sample_rate, - min_buffer_size=1, - max_buffer_size=%buffer_size - } - .entered(), - ); } // start processing if needed if self.plugin.status() == PluginStatus::Activated { self.plugin.start_processing()?; - self.span_processing = Some(tracing::debug_span!("Plugin::Processing").entered()); } // check that we dont overfill the input event queue @@ -145,23 +126,15 @@ impl<'a> ProcessScope<'a> { // run processing let status = self.buffer.process(|inputs, outputs| { - let _span = tracing::debug_span!("Plugin::Process", buffer_size = samples).entered(); - let transport = self.transport.as_clap_transport(0); - self.plugin.process(&clap_process { - steady_time: self.transport.sample_pos.map_or(-1, |f| f as i64), + self.plugin.process(ProcessInfo { frames_count: samples, - transport: if self.transport.is_freerun { - std::ptr::null() - } else { - &transport as *const _ - }, - audio_inputs: inputs.as_ptr(), - audio_outputs: outputs.as_mut_ptr(), - audio_inputs_count: inputs.len() as u32, - audio_outputs_count: outputs.len() as u32, - in_events: Proxy::vtable(&self.events_input), - out_events: Proxy::vtable(&self.events_output), + steady_time: self.transport.sample_pos, + audio_inputs: inputs, + audio_outputs: outputs, + input_events: &self.events_input, + output_events: &self.events_output, + transport: (!self.transport.is_freerun).then_some(&transport), }) })?; @@ -178,12 +151,10 @@ impl<'a> ProcessScope<'a> { pub fn restart(&mut self) { if self.plugin.status() == PluginStatus::Processing { self.plugin.stop_processing(); - self.span_processing.take(); } if self.plugin.status() == PluginStatus::Activated { self.plugin.on_main_thread(|plugin| plugin.deactivate()); - self.span_active.take(); } } } diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index 107bd21..c1310fa 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -1,7 +1,9 @@ -use crate::panic::fail_test; +use crate::debug::fail_test; use crate::plugin::util::{CHECK_POINTER, Proxy, Proxyable}; use clap_sys::events::*; +use std::fmt::Debug; use std::sync::Mutex; +use tracing::Span; #[derive(Debug)] pub struct InputEventQueue(Mutex>); @@ -11,7 +13,7 @@ pub struct OutputEventQueue(Mutex>); /// An event sent to or from the plugin. This uses an enum to make the implementation simple and /// correct at the cost of more wasteful memory usage. -#[derive(Debug, Clone)] +#[derive(Clone)] #[repr(C, align(8))] pub enum Event { /// `CLAP_EVENT_NOTE_ON`, `CLAP_EVENT_NOTE_OFF`, `CLAP_EVENT_NOTE_CHOKE`, or `CLAP_EVENT_NOTE_END`. @@ -94,7 +96,7 @@ impl InputEventQueue { events.len() as u32 } - #[tracing::instrument(name = "clap_input_events::get", level = 1, skip(list))] + #[tracing::instrument(name = "clap_input_events::get", level = 1, skip(list), fields(event = tracing::field::Empty))] unsafe extern "C" fn get(list: *const clap_input_events, index: u32) -> *const clap_event_header { let state = unsafe { Proxy::::from_vtable(list).unwrap_or_else(|e| { @@ -108,7 +110,10 @@ impl InputEventQueue { let events = state.0.lock().unwrap(); match events.get(index as usize) { - Some(event) => event.header(), + Some(event) => { + Span::current().record("event", tracing::field::debug(&event)); + event.header() + } None => { tracing::warn!( "The plugin tried to get an out of bounds event with index {index} ({} total events)", @@ -133,7 +138,7 @@ impl OutputEventQueue { self.0.lock().unwrap().clone() } - #[tracing::instrument(name = "clap_output_events::try_push", level = 1, skip(list, event))] + #[tracing::instrument(name = "clap_output_events::try_push", level = 1, skip_all, fields(event = tracing::field::Empty))] unsafe extern "C" fn try_push(list: *const clap_output_events, event: *const clap_event_header) -> bool { let state = unsafe { Proxy::::from_vtable(list).unwrap_or_else(|e| { @@ -149,9 +154,12 @@ impl OutputEventQueue { fail_test!("clap_output_events::try_push: 'event' pointer is null"); } + let event = unsafe { Event::from_raw(event) }; + // The monotonicity of the plugin's event insertion order is checked as part of the output // consistency checks - state.0.lock().unwrap().push(unsafe { Event::from_raw(event) }); + Span::current().record("event", tracing::field::debug(&event)); + state.0.lock().unwrap().push(event); true } } @@ -198,3 +206,17 @@ impl Event { } } } + +impl Debug for Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Event::Note(event) => event.fmt(f), + Event::NoteExpression(event) => event.fmt(f), + Event::ParamValue(event) => event.fmt(f), + Event::ParamMod(event) => event.fmt(f), + Event::Midi(event) => event.fmt(f), + Event::Transport(event) => event.fmt(f), + Event::Unknown(header) => header.fmt(f), + } + } +} diff --git a/src/plugin/util.rs b/src/plugin/util.rs index a4d026e..639472d 100644 --- a/src/plugin/util.rs +++ b/src/plugin/util.rs @@ -15,7 +15,7 @@ macro_rules! clap_call { { $obj_ptr:expr=>$function_name:ident($($args:expr),* $(, )?) } => { match (*$obj_ptr).$function_name { Some(function_ptr) => function_ptr($($args),*), - None => $crate::panic::fail_test!("'{}::{}' is a null pointer, but this is not allowed", $crate::plugin::util::type_name_of_ptr($obj_ptr), stringify!($function_name)), + None => $crate::debug::fail_test!("'{}::{}' is a null pointer, but this is not allowed", $crate::plugin::util::type_name_of_ptr($obj_ptr), stringify!($function_name)), } } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 369faa8..5271b87 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -258,9 +258,12 @@ impl<'a> TestCase<'a> for PluginTestCase { } } + #[tracing::instrument(name = "PluginTestCase::run", level = "debug", skip_all, fields( + test_case = %self, + plugin_id = %plugin_id, + library_path = %library_path.display() + ))] fn run(&self, (library_path, plugin_id): Self::TestArgs) -> Result { - let _span = tracing::debug_span!("PluginTestCase::run", test_case = %self, plugin_id = %plugin_id, library_path = %library_path.display()).entered(); - // SAFETY: This is called on the main thread. let library = &PluginLibrary::load(library_path) .with_context(|| format!("Could not load '{}'", library_path.display()))?; diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 80f5798..8bb825c 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -205,7 +205,7 @@ pub fn test_layout_audio_ports_config(library: &PluginLibrary, plugin_id: &str) plugin .on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE)?; - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { @@ -307,7 +307,7 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: plugin .on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_in_place_f32(&config_audio_ports, BUFFER_SIZE)?; - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; for _ in 0..5 { diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index cfb2a9c..d44a07f 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -78,18 +78,21 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul // We keep track of how many parameters support these conversions. A plugin // should support either conversion either for all of its parameters, or for // none of them. - let expected_conversions = param_infos.len() * 101; + + let conversions_per_param = 4000usize.div_ceil(param_infos.len()).min(100); + let expected_conversions = param_infos.len() * conversions_per_param; let mut num_supported_value_to_text = 0; let mut num_supported_text_to_value = 0; let mut failed_value_to_text_calls: Vec<(String, f64)> = Vec::new(); let mut failed_text_to_value_calls: Vec<(String, String)> = Vec::new(); + 'param_loop: for (param_id, param_info) in param_infos { let param_name = ¶m_info.name; - 'value_loop: for i in 0..=100 { - let starting_value = - param_info.range.start() + (param_info.range.end() - param_info.range.start()) * (i as f64 / 100.0); + 'value_loop: for i in 0..conversions_per_param { + let starting_value = param_info.range.start() + + (param_info.range.end() - param_info.range.start()) * (i as f64 / (conversions_per_param - 1) as f64); // If the plugin rounds string representations then `value` may very // will not roundtrip correctly, so we'll start at the string @@ -212,7 +215,7 @@ pub fn test_param_fuzz_basic(library: &PluginLibrary, plugin_id: &str, snap_to_b param_fuzzer = param_fuzzer.snap_to_bounds(); } - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); // We'll keep track of the current and the previous set of parameter value so we can write them // to a file if the test fails @@ -307,22 +310,24 @@ pub fn test_param_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: &str) .transpose() .context("Could not fetch the plugin's audio port config")? .unwrap_or_default(); + let note_ports_config = note_ports .map(|ports| ports.config()) .transpose() .context("Could not fetch the plugin's note port config")? .unwrap_or_default(); + let param_infos = params.info().context("Could not fetch the plugin's parameters")?; // For each set of runs we'll generate new parameter values, and if the plugin supports notes // we'll also generate note events. let param_fuzzer = ParamFuzzer::new(¶m_infos); - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut current_events: Option> = None; - let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for &interval in INTERVALS { + let _span = tracing::debug_span!("WithInterval", interval).entered(); let num_steps = (interval * 4).div_ceil(BUFFER_SIZE); plugin.on_audio_thread(|plugin| -> Result<()> { diff --git a/src/tests/plugin/processing.rs b/src/tests/plugin/processing.rs index 6376692..5f967c5 100644 --- a/src/tests/plugin/processing.rs +++ b/src/tests/plugin/processing.rs @@ -101,7 +101,7 @@ pub fn test_process_audio_double(library: &PluginLibrary, plugin_id: &str, in_pl }); } - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-4..=64); let mut audio_buffers = if in_place { AudioBuffers::new_in_place_f64(&audio_ports_config, BUFFER_SIZE)? } else { @@ -225,6 +225,8 @@ pub fn test_process_varying_sample_rates(library: &PluginLibrary, plugin_id: &st let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); for &sample_rate in SAMPLE_RATES { + let _span = tracing::debug_span!("WithSampleRate", sample_rate).entered(); + plugin .on_audio_thread(|plugin| -> Result<()> { let mut note_rng = NoteGenerator::new(¬e_ports_config); @@ -272,6 +274,8 @@ pub fn test_process_varying_block_sizes(library: &PluginLibrary, plugin_id: &str .unwrap_or_default(); for &buffer_size in BLOCK_SIZES { + let _span = tracing::debug_span!("WithBlockSize", buffer_size).entered(); + plugin .on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, buffer_size); @@ -372,13 +376,15 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: let result = plugin.on_audio_thread(|plugin| -> Result { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-4..=64); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; - // first run, "control" run - process.audio_buffers().fill_white_noise(&mut new_prng()); - process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + // first run, the "control" + tracing::debug_span!("RunControl").in_scope(|| { + process.audio_buffers().fill_white_noise(&mut new_prng()); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run() + })?; let output_control = process .audio_buffers() @@ -387,11 +393,16 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: .cloned() .collect::>(); - // second run, deactivate and reactivate the plugin, see if the output changes process.restart(); - process.audio_buffers().fill_white_noise(&mut new_prng()); - process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + + // second run, deactivate and reactivate the plugin, see if the output changes + tracing::debug_span!("RunReactivate", comment = "Check if output changes after reactivation").in_scope( + || { + process.audio_buffers().fill_white_noise(&mut new_prng()); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run() + }, + )?; let output_reactivated = process .audio_buffers() @@ -400,11 +411,16 @@ pub fn test_process_audio_reset_determinism(library: &PluginLibrary, plugin_id: .cloned() .collect::>(); - // third run, reset the plugin, see if the output matches the control run process.reset(); - process.audio_buffers().fill_white_noise(&mut new_prng()); - process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); - process.run()?; + + // third run, reset the plugin, see if the output matches the control run + tracing::debug_span!("RunReset", comment = "Check if output changes after clap_plugin::reset").in_scope( + || { + process.audio_buffers().fill_white_noise(&mut new_prng()); + process.add_events(note_rng.generate_events(&mut new_prng(), BUFFER_SIZE)); + process.run() + }, + )?; let output_reset = process .audio_buffers() @@ -499,29 +515,45 @@ pub fn test_process_sleep_constant_mask(library: &PluginLibrary, plugin_id: &str plugin.on_audio_thread(|plugin| -> Result<()> { let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-4..=64); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; // block 1: silent inputs, see what the plugin does - process.run()?; - check_buffers(process.audio_buffers()).context("Block 0")?; + tracing::debug_span!( + "BlockPrerollSilent", + comment = "A block of silence before the initial sound, to check if the plugin marks output as constant \ + with no tail" + ) + .in_scope(|| { + process.run()?; + check_buffers(process.audio_buffers()).context("Block preroll silent") + })?; // block 2: randomize inputs, see if the plugin tracks constant channels - process.audio_buffers().fill_white_noise(&mut prng); - process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); - process.run()?; - check_buffers(process.audio_buffers()).context("Block 1")?; + tracing::debug_span!( + "BlockRandomInput", + comment = "A block filled with white noise, to check if the plugin correctly handles non-constant input \ + (and does not mark output as constant)" + ) + .in_scope(|| { + process.audio_buffers().fill_white_noise(&mut prng); + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + check_buffers(process.audio_buffers()).context("Block random input") + })?; // block 3-40: silent inputs again, see if the plugin updates the constant mask accordingly // 40 blocks to give the output tail to fully decay to silence if there is any reverb/delay - process.audio_buffers().fill_silence(); - process.add_events(note_rng.stop_all_voices(0)); - for _ in 3..=40 { - process.run()?; - check_buffers(process.audio_buffers())?; - } + tracing::debug_span!("BlockTailSilent", comment = "A tail of silent blocks").in_scope(|| { + process.audio_buffers().fill_silence(); + process.add_events(note_rng.stop_all_voices(0)); + for _ in 3..=40 { + process.run()?; + check_buffers(process.audio_buffers())?; + } - Ok(()) + Ok(()) + }) })?; plugin.poll_callback(|_| Ok(()))?; @@ -567,7 +599,7 @@ pub fn test_process_sleep_process_status(library: &PluginLibrary, plugin_id: &st let tail = plugin.get_extension::(); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-4..=64); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; let mut is_sleeping = false; diff --git a/src/tests/plugin/transport.rs b/src/tests/plugin/transport.rs index 22839a9..1dad289 100644 --- a/src/tests/plugin/transport.rs +++ b/src/tests/plugin/transport.rs @@ -32,7 +32,7 @@ pub fn test_transport_null(library: &PluginLibrary, plugin_id: &str) -> Result Result<()> { - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; @@ -76,7 +76,7 @@ pub fn test_transport_fuzz(library: &PluginLibrary, plugin_id: &str) -> Result Result<()> { let mut transport_fuzz = TransportFuzzer::new(); - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; @@ -122,9 +122,11 @@ pub fn test_transport_fuzz_sample_accurate(library: &PluginLibrary, plugin_id: & }; for &interval in INTERVALS { + let _span = tracing::debug_span!("WithInterval", interval).entered(); + plugin .on_audio_thread(|plugin| -> Result<()> { - let mut note_rng = NoteGenerator::new(¬e_ports_config); + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); let mut audio_buffers = AudioBuffers::new_out_of_place_f32(&audio_ports_config, BUFFER_SIZE); let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; diff --git a/src/tests/plugin_library.rs b/src/tests/plugin_library.rs index 11cbec2..e269edb 100644 --- a/src/tests/plugin_library.rs +++ b/src/tests/plugin_library.rs @@ -69,9 +69,11 @@ impl<'a> TestCase<'a> for PluginLibraryTestCase { } } + #[tracing::instrument(name = "PluginLibraryTestCase::run", level = "debug", skip_all, fields( + test_case = %self, + library_path = %library_path.display() + ))] fn run(&self, library_path: Self::TestArgs) -> Result { - let _span = tracing::debug_span!("PluginLibraryTestCase::run", test_case = %self, library_path = %library_path.display()).entered(); - match self { PluginLibraryTestCase::PresetDiscoveryCrawl => preset_discovery::test_crawl(library_path, false), PluginLibraryTestCase::PresetDiscoveryDescriptorConsistency => { diff --git a/src/tests/rng.rs b/src/tests/rng.rs index 83eda6f..798c9ea 100644 --- a/src/tests/rng.rs +++ b/src/tests/rng.rs @@ -164,6 +164,13 @@ impl<'a> NoteGenerator<'a> { } } + /// Set the range for the next event's timing relative to the previous event. This will be + /// clamped to 0 when generating events. + pub fn with_sample_offset_range(mut self, range: RangeInclusive) -> Self { + self.sample_offset_range = range; + self + } + /// Set the parameter info to generate random polyphonic automation and modulation events for. pub fn with_params(mut self, params: &'a ParamInfo) -> Self { self.params = Some(params); diff --git a/src/validator.rs b/src/validator.rs index 35056a9..d35e45f 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -3,10 +3,10 @@ use crate::Verbosity; use crate::commands::validate::{SingleTestSettings, ValidatorSettings}; -use crate::panic::panic_message; +use crate::debug::panic_message; use crate::plugin::library::{PluginLibrary, PluginMetadata}; use crate::tests::{PluginLibraryTestCase, PluginTestCase, SerializedTest, TestCase, TestResult, TestStatus}; -use crate::util::{self, IteratorExt}; +use crate::util::IteratorExt; use anyhow::{Context, Result}; use clap::ValueEnum; use clap_sys::version::clap_version_is_compatible; @@ -53,12 +53,6 @@ pub struct ValidationTally { /// Run the validator using the specified settings. Returns an error if any of the plugin paths /// could not loaded, or if the plugin ID filter did not match any plugins. pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { - // Before doing anything, we need to make sure any temporary artifact files from the previous - // run are cleaned up. These are used for things like state dumps when one of the state tests - // fail. This is allowed to fail since the directory may not exist and even if it does and we - // cannot remove it, then that may not be a problem. - let _ = std::fs::remove_dir_all(util::validator_temp_dir()); - let test_regex = settings .test_filter .as_ref() @@ -229,6 +223,22 @@ fn run_test<'a, T: TestCase<'a>>( run_test_out_of_process(test, args, verbosity, settings.hide_output)? }; + match &status { + TestStatus::Success { details } => { + tracing::info!(test = %test, details=details, "Test completed") + } + TestStatus::Warning { details } => { + tracing::warn!(test = %test, details=details, "Test completed with a warning") + } + TestStatus::Failed { details } => { + tracing::error!(test = %test, details=details, "Test failed") + } + TestStatus::Crashed { details } => { + tracing::error!(test = %test, details=details, "Test crashed") + } + _ => {} + } + Ok(TestResult { name: test.to_string(), description: test.description(), From 5f57833b777564724ae797e3edbc2d5c3fad6bf5 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 4 Feb 2026 16:26:08 +0400 Subject: [PATCH 055/114] initial audio-ports-activation test; move from tracing-subscriber Layer to tracing-core Subscriber for better perf --- Cargo.lock | 44 +--- Cargo.toml | 4 +- src/debug/log.rs | 76 ++++-- src/debug/trace.rs | 288 ++++++++++++++++++----- src/main.rs | 60 ++--- src/plugin/ext/audio_ports_activation.rs | 12 +- src/plugin/instance/shared.rs | 4 +- src/tests/plugin/layout.rs | 95 ++++++++ src/tests/plugin/params.rs | 4 +- tests/clack-synth/src/lib.rs | 10 + 10 files changed, 438 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a6db11..837a802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ "textwrap", "time", "tracing", - "tracing-subscriber", + "tracing-core", "wait-timeout", "walkdir", "yansi", @@ -364,12 +364,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.178" @@ -647,15 +641,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "smawk" version = "0.3.2" @@ -742,15 +727,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.36" @@ -810,17 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", + "valuable", ] [[package]] @@ -847,6 +813,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 0cc0caa..9385c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ time = { version = "0.3", features = ["serde"]} walkdir = "2.3" wait-timeout = "0.2.1" rustc-hash = "2.1.1" -tracing = { version = "0.1.44", features = ["max_level_off"] } -tracing-subscriber = { version = "0.3.18", default-features = false, features = ["registry"] } +tracing = "0.1.44" +tracing-core = "0.1.36" yansi = "1.0.1" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src/debug/log.rs b/src/debug/log.rs index 2cf601e..7f38fde 100644 --- a/src/debug/log.rs +++ b/src/debug/log.rs @@ -1,54 +1,86 @@ -//! A tracing layer that logs events to standard output in compact human readable format. +//! A tracing layer that logs events to standard output in a compact human readable format. +use std::cell::RefCell; use std::fmt::{Debug, Write}; use std::time::Instant; use tracing::field::Field; +use tracing::level_filters::LevelFilter; +use tracing::{Level, Subscriber, span}; use yansi::Paint; -pub struct LogStderrLayer { - _inner: std::marker::PhantomData, +pub struct LogStderrSubscriber { + level: LevelFilter, start: Instant, } -impl LogStderrLayer { - pub fn new() -> Self { +impl LogStderrSubscriber { + pub fn new(level: LevelFilter) -> Self { Self { - _inner: std::marker::PhantomData, + level, start: Instant::now(), } } -} -impl tracing_subscriber::Layer for LogStderrLayer -where - S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, -{ - fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) { + fn write(&self, level: Level, content: impl FnOnce(&mut String)) { thread_local! { - static BUFFER: std::cell::RefCell = std::cell::RefCell::new(String::with_capacity(256)); + static BUFFER: RefCell = const { RefCell::new(String::new()) } } let elapsed = self.start.elapsed(); - let prefix = match *event.metadata().level() { - tracing::Level::ERROR => "ERROR".red().bold(), - tracing::Level::WARN => " WARN".yellow().bold(), - tracing::Level::INFO => " INFO".cyan().bold(), - tracing::Level::DEBUG => "DEBUG".white().bold(), - tracing::Level::TRACE => "TRACE".dim().bold(), + let prefix = match level { + Level::ERROR => "ERROR".red().bold(), + Level::WARN => " WARN".yellow(), + Level::INFO => " INFO".green(), + Level::DEBUG => "DEBUG".blue(), + Level::TRACE => "TRACE".white(), }; BUFFER.with_borrow_mut(|buffer| { buffer.clear(); - write!(buffer, "{}{}", elapsed.as_millis().dim(), "ms".dim()).ok(); + write!(buffer, "{:>5}{}", elapsed.as_millis().dim(), "ms".dim()).ok(); write!(buffer, " {}: ", prefix).ok(); - event.record(&mut WriteMessage(buffer)); - event.record(&mut WriteFields(buffer)); + content(buffer); writeln!(buffer).ok(); eprint!("{}", buffer); }); } } +// why subscriber directly and not a layer? +// the initial layer implementation was taking 25% of total runtime doing practically nothing +impl Subscriber for LogStderrSubscriber { + fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool { + metadata.level() <= &self.level && metadata.is_event() + } + + fn max_level_hint(&self) -> Option { + Some(self.level) + } + + fn new_span(&self, _: &span::Attributes<'_>) -> span::Id { + span::Id::from_u64(1) + } + + fn current_span(&self) -> tracing_core::span::Current { + tracing_core::span::Current::none() + } + + fn record(&self, _: &span::Id, _: &span::Record<'_>) {} + + fn record_follows_from(&self, _: &span::Id, _: &span::Id) {} + + fn enter(&self, _: &span::Id) {} + + fn exit(&self, _: &span::Id) {} + + fn event(&self, event: &tracing::Event<'_>) { + self.write(*event.metadata().level(), |buffer| { + event.record(&mut WriteMessage(buffer)); + event.record(&mut WriteFields(buffer)); + }); + } +} + struct WriteMessage<'a>(&'a mut String); struct WriteFields<'a>(&'a mut String); diff --git a/src/debug/trace.rs b/src/debug/trace.rs index e4b0928..cddd114 100644 --- a/src/debug/trace.rs +++ b/src/debug/trace.rs @@ -1,111 +1,268 @@ //! A tracing layer that outputs Chrome JSON trace files. +use std::cell::RefCell; use std::collections::BTreeMap; use std::fs::File; -use std::io::{BufWriter, Write}; -use std::marker::PhantomData; +use std::io::Write; use std::path::Path; use std::sync::Mutex; +use std::sync::atomic::AtomicU64; use std::time::Instant; -use tracing::Subscriber; -use tracing::span::{Attributes, Id, Record}; -use tracing_subscriber::Layer; -use tracing_subscriber::layer::Context; -use tracing_subscriber::registry::LookupSpan; +use tracing_core::span::{Attributes, Current, Id, Record}; +use tracing_core::{Metadata, Subscriber}; -pub struct ChromeJsonLayer { +static NEXT_SPAN_ID: AtomicU64 = AtomicU64::new(1); + +thread_local! { + static THREAD_DATA: RefCell = RefCell::new(ThreadData::new()); +} + +pub struct ChromeJsonSubscriber { start: Instant, - writer: Mutex>, - _inner: PhantomData, + writer: Mutex>, } -impl ChromeJsonLayer { +impl ChromeJsonSubscriber { pub fn new(path: impl AsRef) -> Self { - let mut file = BufWriter::new(File::create(path).unwrap()); - file.write_all(b"[\n").unwrap(); + let file = File::create(path).and_then(|mut f| { + f.write_all(b"[\n")?; + Ok(f) + }); Self { start: Instant::now(), writer: Mutex::new(file), - _inner: PhantomData, } } - fn emit(&self, event: Trace) { - let mut writer = self.writer.lock().unwrap(); - serde_json::to_writer(&mut *writer, &event).unwrap(); - writer.write_all(b",\n").unwrap(); - writer.flush().unwrap(); + pub fn check_error(&self) -> anyhow::Result<()> { + match &*self.writer.lock().unwrap() { + Ok(_) => Ok(()), + Err(e) => anyhow::bail!("{}", e), + } } -} -impl Layer for ChromeJsonLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ - fn on_new_span(&self, attrs: &Attributes<'_>, _id: &Id, ctx: Context<'_, S>) { - let time = self.start.elapsed().as_micros(); + fn emit(&self, event: TraceEvent) { + thread_local! { + static BUFFER: RefCell> = RefCell::new(Vec::with_capacity(256)); + } - let mut data = TraceArgs::default(); - attrs.record(&mut data); - - self.emit(Trace { - name: attrs.metadata().name(), - cat: std::thread::current().name().unwrap_or("?"), - ts: time, - id: 1, - pid: 1, - ph: "b", - args: &data, + BUFFER.with_borrow_mut(|buffer| { + buffer.clear(); + serde_json::to_writer(&mut *buffer, &event).unwrap(); + buffer.extend_from_slice(b",\n"); + + let mut writer = self.writer.lock().unwrap(); + if let Ok(file) = &mut *writer + && let Err(e) = file.write_all(buffer).and_then(|_| file.flush()) + { + *writer = Err(e); + } }); + } +} - ctx.span(_id).unwrap().extensions_mut().insert(data); +impl Subscriber for ChromeJsonSubscriber { + fn enabled(&self, _: &tracing::Metadata<'_>) -> bool { + true } - fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { - let time = self.start.elapsed().as_micros(); + fn new_span(&self, span: &Attributes<'_>) -> Id { + THREAD_DATA.with_borrow_mut(|thread| { + let id = Id::from_u64(NEXT_SPAN_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)); + let mut args = thread.new_args(); - let mut args = TraceArgs::default(); - event.record(&mut args); - - self.emit(Trace { - name: args.values.get("message").map(|s| s.as_str()).unwrap_or("?"), - cat: std::thread::current().name().unwrap_or("?"), - ts: time, - id: 1, - pid: 1, - ph: "n", - args: &args, + span.record(&mut args); + thread.active_spans.push(ThreadSpan { + id: id.clone(), + meta: span.metadata(), + args, + uses: 1, + }); + + id + }) + } + + fn clone_span(&self, id: &Id) -> Id { + THREAD_DATA.with_borrow_mut(|thread| { + if let Some(span) = thread.span_mut(id) { + span.uses += 1; + } + + id.clone() + }) + } + + fn try_close(&self, id: Id) -> bool { + THREAD_DATA.with_borrow_mut(|thread| { + if let Some(span) = thread.span_mut(&id) { + span.uses -= 1; + if span.uses == 0 { + let span = thread.remove_span(&id); + if let Some(span) = span { + thread.free_args(span.args); + return true; + } + } + } + + false + }) + } + + fn record(&self, id: &Id, values: &Record<'_>) { + THREAD_DATA.with_borrow_mut(|thread| { + if let Some(span) = thread.span_mut(id) { + values.record(&mut span.args); + } }); } - fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) { - let span = ctx.span(id).unwrap(); - if let Some(args) = span.extensions_mut().get_mut::() { - values.record(args); - } + fn record_follows_from(&self, _: &Id, _: &Id) {} + + fn current_span(&self) -> Current { + THREAD_DATA.with_borrow(|thread| { + if let Some(span) = thread.active_spans.last() { + Current::new(span.id.clone(), span.meta) + } else { + Current::none() + } + }) } - fn on_close(&self, id: Id, ctx: Context<'_, S>) { + fn event(&self, event: &tracing::Event<'_>) { let time = self.start.elapsed().as_micros(); - let span = ctx.span(&id).unwrap(); - if let Some(args) = span.extensions().get::() { - self.emit(Trace { - name: span.name(), - cat: std::thread::current().name().unwrap_or("?"), + THREAD_DATA.with_borrow_mut(|thread| { + let mut args = thread.new_args(); + event.record(&mut args); + + self.emit(TraceEvent { + name: args.values.get("message").map(|s| s.as_str()).unwrap_or("?"), + cat: &thread.thread, + args: &args, ts: time, id: 1, pid: 1, - ph: "e", - args, + ph: "n", }); + + thread.reuse_args.push(args); + }); + } + + fn enter(&self, span: &Id) { + let time = self.start.elapsed().as_micros(); + + THREAD_DATA.with_borrow_mut(|thread| { + if let Some(span) = thread.span(span) { + self.emit(TraceEvent { + name: span.meta.name(), + cat: &thread.thread, + ts: time, + id: 1, + pid: 1, + ph: "b", + args: &span.args, + }); + } + }); + } + + fn exit(&self, span: &Id) { + let time = self.start.elapsed().as_micros(); + + THREAD_DATA.with_borrow_mut(|thread| { + if let Some(span) = thread.span(span) { + self.emit(TraceEvent { + name: span.meta.name(), + cat: &thread.thread, + ts: time, + id: 1, + pid: 1, + ph: "e", + args: &span.args, + }); + } + }); + } +} + +/// Per-span tracer data +struct ThreadSpan { + /// The span's (per-program unique) ID + id: Id, + + /// Associated metadata + meta: &'static Metadata<'static>, + + /// Recorded dynamic arguments + args: TraceArgs, + + /// Reference count for the span, span is closed when this reaches 0 + uses: u32, +} + +/// Per-thread tracer data +struct ThreadData { + /// Current thread display name + thread: String, + + /// A "stack" of currently active spans on this thread, in the order they were entered + /// In most cases (FIFO) this is extremely fast, but this also allows for out-of-order/overlapping spans at the cost of O(n) lookups + active_spans: Vec, + + /// A list of `TraceArgs` for reuse, to minimize allocations. + reuse_args: Vec, +} + +impl ThreadData { + pub fn new() -> Self { + Self { + thread: std::thread::current() + .name() + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("{:?}", std::thread::current().id())), + active_spans: Vec::new(), + reuse_args: Vec::new(), } } + + /// Find a span by its ID + fn span(&self, id: &Id) -> Option<&ThreadSpan> { + self.active_spans.iter().find(|span| &span.id == id) + } + + /// Find a span by its ID + fn span_mut(&mut self, id: &Id) -> Option<&mut ThreadSpan> { + self.active_spans.iter_mut().rfind(|span| &span.id == id) + } + + /// Remove a span by its ID, returning it if found + fn remove_span(&mut self, id: &Id) -> Option { + self.active_spans + .iter() + .rposition(|span| &span.id == id) + .map(|idx| self.active_spans.remove(idx)) + } + + /// Constructs a new `TraceArgs`, reusing one from the pool if available. + fn new_args(&mut self) -> TraceArgs { + let mut args = self.reuse_args.pop().unwrap_or_default(); + args.values.clear(); + args + } + + /// Returns a `TraceArgs` to the pool for reuse. + fn free_args(&mut self, args: TraceArgs) { + self.reuse_args.push(args); + } } +/// An event that is written to the file #[derive(serde::Serialize)] -struct Trace<'a> { +struct TraceEvent<'a> { name: &'a str, cat: &'a str, ts: u128, @@ -115,6 +272,7 @@ struct Trace<'a> { args: &'a TraceArgs, } +/// A helper object used to store and record event/span attribute data #[derive(serde::Serialize, Default)] #[serde(transparent)] struct TraceArgs { diff --git a/src/main.rs b/src/main.rs index beec563..b9dc974 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use std::process::ExitCode; +use std::sync::Arc; use tracing::level_filters::LevelFilter; -use tracing_subscriber::Layer; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; use yansi::Paint; mod commands; @@ -67,24 +65,25 @@ fn main() -> ExitCode { let _ = std::fs::create_dir_all(util::validator_temp_dir()); let trace_path = util::validator_temp_dir().join("trace.json"); - let trace_enabled = match &cli.command { - Command::Validate(settings) => settings.trace, - _ => false, + let trace_writer = if matches!(&cli.command, Command::Validate(settings) if settings.trace) { + Some(Arc::new(debug::ChromeJsonSubscriber::new(&trace_path))) + } else { + None }; - let log_level = match cli.verbosity { - Verbosity::Quiet => LevelFilter::OFF, - Verbosity::Error => LevelFilter::ERROR, - Verbosity::Warn => LevelFilter::WARN, - Verbosity::Info => LevelFilter::INFO, - Verbosity::Debug => LevelFilter::DEBUG, - Verbosity::Trace => LevelFilter::TRACE, - }; - - tracing_subscriber::registry() - .with(debug::LogStderrLayer::new().with_filter(log_level)) - .with(trace_enabled.then(|| debug::ChromeJsonLayer::new(&trace_path))) - .init(); + if let Some(trace) = trace_writer.clone() { + tracing::subscriber::set_global_default(trace).unwrap(); + } else { + tracing::subscriber::set_global_default(debug::LogStderrSubscriber::new(match cli.verbosity { + Verbosity::Quiet => LevelFilter::OFF, + Verbosity::Error => LevelFilter::ERROR, + Verbosity::Warn => LevelFilter::WARN, + Verbosity::Info => LevelFilter::INFO, + Verbosity::Debug => LevelFilter::DEBUG, + Verbosity::Trace => LevelFilter::TRACE, + })) + .unwrap(); + } // Install the panic hook to log panics instead of printing them to stderr. debug::install_panic_hook(); @@ -108,16 +107,19 @@ fn main() -> ExitCode { } }; - if trace_enabled { - eprintln!( - "{}", - format!( - "Trace written to '{}'. Use 'https://ui.perfetto.dev/ to view it.", - trace_path.display() - ) - .dim() - .italic() - ); + if let Some(trace) = trace_writer { + match trace.check_error() { + Ok(()) => eprintln!( + "{}", + format!( + "Trace written to '{}'. Use 'https://ui.perfetto.dev/ to view it.", + trace_path.display() + ) + .dim() + .italic() + ), + Err(e) => eprintln!("{}: {}", "Failed to write trace".red().italic(), e), + } } status diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index 00630fb..b431579 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -1,6 +1,7 @@ use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; use crate::plugin::util::clap_call; +use anyhow::Result; use clap_sys::ext::audio_ports_activation::*; use std::ffi::CStr; use std::ptr::NonNull; @@ -44,13 +45,20 @@ impl<'a> AudioPortsActivation<'a> { /// Activates or deactivates audio ports while inactive. #[allow(unused)] #[tracing::instrument(name = "clap_plugin_audio_ports_activation::set_active", level = 1, skip(self))] - pub fn set_active(&mut self, is_input: bool, port_index: u32, is_active: bool, sample_size: u32) -> bool { + pub fn set_active(&self, is_input: bool, port_index: u32, is_active: bool, sample_size: u32) -> Result<()> { self.plugin.status().assert_inactive(); let audio_ports_activation = self.audio_ports_activation.as_ptr(); let plugin = self.plugin.as_ptr(); + unsafe { - clap_call! { audio_ports_activation=>set_active(plugin, is_input, port_index, is_active, sample_size) } + let success = + clap_call! { audio_ports_activation=>set_active(plugin, is_input, port_index, is_active, sample_size) }; + if success { + Ok(()) + } else { + anyhow::bail!("clap_plugin_audio_ports_activation::set_active returned false") + } } } } diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index 644e554..6287d10 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -591,12 +591,12 @@ impl PluginShared { }); } - #[instrument(name = "clap_host_thread_check::is_main_thread", level = 1, skip(host))] + // #[instrument(name = "clap_host_thread_check::is_main_thread", level = 1, skip(host))] unsafe extern "C" fn ext_thread_check_is_main_thread(host: *const clap_host) -> bool { Self::wrap(host, |this| Ok(this.main_thread_id == std::thread::current().id())).unwrap_or(false) } - #[instrument(name = "clap_host_thread_check::is_audio_thread", level = 1, skip(host))] + // #[instrument(name = "clap_host_thread_check::is_audio_thread", level = 1, skip(host))] unsafe extern "C" fn ext_thread_check_is_audio_thread(host: *const clap_host) -> bool { Self::wrap(host, |this| { Ok(this.audio_thread_id.load() == Some(std::thread::current().id())) diff --git a/src/tests/plugin/layout.rs b/src/tests/plugin/layout.rs index 8bb825c..c2a6988 100644 --- a/src/tests/plugin/layout.rs +++ b/src/tests/plugin/layout.rs @@ -8,6 +8,7 @@ use crate::plugin::process::{AudioBuffers, ProcessScope}; use crate::tests::TestStatus; use crate::tests::rng::{NoteGenerator, new_prng, random_layout_requests}; use anyhow::{Context, Result}; +use rand::Rng; const BUFFER_SIZE: u32 = 512; @@ -341,6 +342,8 @@ pub fn test_layout_configurable_audio_ports(library: &PluginLibrary, plugin_id: } /// The test for `PluginTestCase::LayoutAudioPortsActivation`. +/// TODO: fix deactivated output ports false positive +/// TODO: audio ports activation invalidation test (audio-ports-config extension and port rescan) pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &str) -> Result { let mut prng = new_prng(); @@ -380,5 +383,97 @@ pub fn test_layout_audio_ports_activation(library: &PluginLibrary, plugin_id: &s } }; + let full_input_mask = 1u64 + .unbounded_shl(audio_ports_config.inputs.len() as u32) + .wrapping_sub(1); + let full_output_mask = 1u64 + .unbounded_shl(audio_ports_config.outputs.len() as u32) + .wrapping_sub(1); + + let mut next_input_mask = full_input_mask; + let mut next_output_mask = full_output_mask; + + // 32 different attempts + for _ in 0..32 { + let prev_input_mask = std::mem::replace(&mut next_input_mask, prng.random()); + let prev_output_mask = std::mem::replace(&mut next_output_mask, prng.random()); + + next_input_mask &= full_input_mask; + next_output_mask &= full_output_mask; + + let _span = tracing::debug_span!( + "WithAudioPortsActivation", + input_mask = format_args!("0b{:b}", next_input_mask), + output_mask = format_args!("0b{:b}", next_output_mask), + ) + .entered(); + + for input in 0..audio_ports_config.inputs.len() { + let currently_active = (prev_input_mask & (1 << input)) != 0; + let should_be_active = (next_input_mask & (1 << input)) != 0; + + if currently_active != should_be_active { + audio_ports_activation + .set_active(true, input as u32, should_be_active, 32) + .with_context(|| { + format!( + "Could not make input port {} {}", + input, + if should_be_active { "active" } else { "inactive" } + ) + })?; + } + } + + for output in 0..audio_ports_config.outputs.len() { + let currently_active = (prev_output_mask & (1 << output)) != 0; + let should_be_active = (next_output_mask & (1 << output)) != 0; + + if currently_active != should_be_active { + audio_ports_activation + .set_active(false, output as u32, should_be_active, 32) + .with_context(|| { + format!( + "Could not make output port {} {}", + output, + if should_be_active { "active" } else { "inactive" } + ) + })?; + } + } + + plugin + .on_audio_thread(|plugin| -> Result<()> { + let mut audio_buffers = AudioBuffers::new_in_place_f32(&audio_ports_config, BUFFER_SIZE)?; + let mut note_rng = NoteGenerator::new(¬e_ports_config).with_sample_offset_range(-1..=128); + let mut process = ProcessScope::new(&plugin, &mut audio_buffers)?; + + for _ in 0..5 { + for buffer in process.audio_buffers().iter_mut() { + if let Some(input) = buffer.port().input() { + if (next_input_mask & (1 << input)) == 0 { + buffer.fill_silence(); + } else { + buffer.fill_white_noise(&mut prng); + } + } + + // TODO: output ports deactivated false positive + } + + process.add_events(note_rng.generate_events(&mut prng, BUFFER_SIZE)); + process.run()?; + } + + Ok(()) + }) + .with_context(|| { + format!( + "Error while processing audio with input mask 0b{:b} and output mask 0b{:b}", + next_input_mask, next_output_mask + ) + })?; + } + Ok(TestStatus::Success { details: None }) } diff --git a/src/tests/plugin/params.rs b/src/tests/plugin/params.rs index d44a07f..e457132 100644 --- a/src/tests/plugin/params.rs +++ b/src/tests/plugin/params.rs @@ -79,7 +79,7 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul // should support either conversion either for all of its parameters, or for // none of them. - let conversions_per_param = 4000usize.div_ceil(param_infos.len()).min(100); + let conversions_per_param = 4000usize.div_ceil(param_infos.len()).clamp(5, 100); let expected_conversions = param_infos.len() * conversions_per_param; let mut num_supported_value_to_text = 0; @@ -90,6 +90,8 @@ pub fn test_param_conversions(library: &PluginLibrary, plugin_id: &str) -> Resul 'param_loop: for (param_id, param_info) in param_infos { let param_name = ¶m_info.name; + let _span = tracing::debug_span!("WithParam", param_id = param_id, param_name = param_name.as_str()).entered(); + 'value_loop: for i in 0..conversions_per_param { let starting_value = param_info.range.start() + (param_info.range.end() - param_info.range.start()) * (i as f64 / (conversions_per_param - 1) as f64); diff --git a/tests/clack-synth/src/lib.rs b/tests/clack-synth/src/lib.rs index 664d583..3dceacb 100644 --- a/tests/clack-synth/src/lib.rs +++ b/tests/clack-synth/src/lib.rs @@ -17,6 +17,7 @@ use clack_extensions::state::PluginState; use clack_plugin::events::spaces::CoreEventSpace; use clack_plugin::prelude::*; use clack_plugin::process::ConstantMask; +use std::f32; use std::ffi::CString; mod oscillator; @@ -74,6 +75,8 @@ impl DefaultPluginFactory for PolySynthPlugin { pub struct PolySynthAudioProcessor<'a> { channels: u32, + active: bool, + poly_osc: PolyOscillator, modulation_values: PolySynthParamModulations, shared: &'a PolySynthPluginShared, @@ -89,6 +92,7 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre audio_config: PluginAudioConfiguration, ) -> Result { Ok(Self { + active: main_thread.active, channels: main_thread.config.get(), poly_osc: PolyOscillator::new(16, audio_config.sample_rate as f32), modulation_values: PolySynthParamModulations::new(), @@ -128,6 +132,11 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre is_non_silent |= self.poly_osc.has_active_voices() } + // it is legal; when an output port is deactivated, the host must not use its contents + if !self.active { + output_buffer.fill(f32::NAN); + } + assert!(output_channels.channel_count() == self.channels); // Copy the first channel to all other channels for mono output @@ -233,6 +242,7 @@ impl PluginAudioPortsConfigImpl for PolySynthPluginMainThread<'_> { fn select(&mut self, config_id: ClapId) -> Result<(), PluginError> { if config_id.get() <= 8 { self.config = config_id; + self.active = true; Ok(()) } else { Err(PluginError::Message("Invalid configuration ID")) From 86909b65504e1d21d97f9c20357733ef8645ddc0 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Wed, 4 Feb 2026 20:42:26 +0400 Subject: [PATCH 056/114] clap-validator.toml support --- Cargo.lock | 10 +++++ Cargo.toml | 1 + README.md | 16 +++++++- src/commands/list.rs | 32 ++++++++++----- src/commands/validate.rs | 50 ++++-------------------- src/config.rs | 41 +++++++++++++++++++ src/debug.rs | 6 +++ src/debug/trace.rs | 49 +++++++---------------- src/main.rs | 4 +- src/plugin.rs | 1 + src/plugin/ext/ambisonic.rs | 16 +++++--- src/plugin/ext/audio_ports_activation.rs | 21 ++++++++-- src/plugin/ext/audio_ports_config.rs | 4 +- src/{ => plugin}/index.rs | 0 src/plugin/instance/audio_thread.rs | 4 +- src/plugin/instance/shared.rs | 10 ++--- src/plugin/process/events.rs | 7 ++-- src/tests.rs | 30 +++++++++----- src/tests/plugin.rs | 22 +++++------ src/validator.rs | 50 +++++++++++++++--------- 20 files changed, 224 insertions(+), 150 deletions(-) create mode 100644 src/config.rs rename src/{ => plugin}/index.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 837a802..80ac5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -139,6 +148,7 @@ name = "clap-validator" version = "0.3.2" dependencies = [ "anyhow", + "basic-toml", "clap", "clap-sys 0.5.0 (git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156)", "core-foundation", diff --git a/Cargo.toml b/Cargo.toml index 9385c16..bc11d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ rustc-hash = "2.1.1" tracing = "0.1.44" tracing-core = "0.1.36" yansi = "1.0.1" +basic-toml = "0.1.10" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" diff --git a/README.md b/README.md index fc97e0f..35e26b3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,21 @@ validator to run the that test in the current process. Use `clap-validator list to list all available tests. ```shell -clap-validator validate --in-process --test-filter /path/to/the/plugin.clap +clap-validator validate --in-process --filter /path/to/the/plugin.clap +``` + +### Tracing + +clap-validator can generate traces of plugin/host call execution during the in-process tests that could be used to diagnose issues or understand plugin behavior. To enable tracing, pass the `--trace` option to `clap-validator validate`. The generated trace files can be opened in [Perfetto](https://perfetto.dev/). + +### Filtering + +By default, all tests are run during validation, including pedantic ones. You can use the `--filter` option to specify a regex of tests to run. Another option is to create a configuration file named `clap-validator.toml` in the current working directory or any of its parent directories. In this file, you can specify which tests to enable or disable. An example configuration file looks like this: + +```toml +# clap-validator.toml +[test] +state-buffered-streams = false ``` ## Building diff --git a/src/commands/list.rs b/src/commands/list.rs index ca3aa50..eb217ad 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,7 +1,7 @@ //! Commands for listing information about the validator or installed plugins. use super::{TextWrapper, println_wrapped, println_wrapped_no_indent}; -use crate::index::PresetIndexResult; +use crate::plugin::index::{PresetIndexResult, index, index_presets}; use crate::plugin::preset_discovery::PresetFile; use anyhow::{Context, Result}; use clap::Subcommand; @@ -49,7 +49,7 @@ pub fn list(command: &ListCommand) -> Result { /// Lists basic information about all installed CLAP plugins. fn list_plugins(json: bool) -> Result { - let plugin_index = crate::index::index(); + let plugin_index = index(); if json { println!( @@ -119,14 +119,14 @@ where P: AsRef, { let preset_index = match plugin_paths { - Some(plugin_paths) => crate::index::index_presets(plugin_paths, false), + Some(plugin_paths) => index_presets(plugin_paths, false), None => { - let plugin_index = crate::index::index(); + let plugin_index = index(); let all_plugin_paths = plugin_index.0.keys(); // This 'true' indicates that plugins that don't support the preset discovery mechanism // should be silently skipped - crate::index::index_presets(all_plugin_paths, true) + index_presets(all_plugin_paths, true) } } .context("Error while crawling presets")?; @@ -337,6 +337,7 @@ where /// Lists all available test cases. fn list_tests(json: bool) -> Result { let list = crate::tests::TestList::default(); + let config = crate::config::Config::from_current()?; if json { println!( @@ -345,15 +346,28 @@ fn list_tests(json: bool) -> Result { ); } else { let mut wrapper = TextWrapper::default(); + let mut print_test = |test: &crate::tests::TestListItem| { + if config.is_test_enabled(&test.name) { + println_wrapped!(wrapper, "- {}: {}\n", test.name.bold(), test.description); + } else { + println_wrapped!( + wrapper, + "- {} {}: {}\n", + test.name.bold(), + "disabled".dim().italic(), + test.description + ); + } + }; println!("Plugin library tests:"); - for (test_name, test_description) in list.plugin_library_tests { - println_wrapped!(wrapper, "- {test_name}: {test_description}"); + for test in list.plugin_library_tests { + print_test(&test); } println!("\nPlugin tests:"); - for (test_name, test_description) in list.plugin_tests { - println_wrapped!(wrapper, "- {test_name}: {test_description}"); + for test in list.plugin_tests { + print_test(&test); } } diff --git a/src/commands/validate.rs b/src/commands/validate.rs index 9ed2025..030a710 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -1,6 +1,7 @@ //! Commands for validating plugins. use super::{TextWrapper, println_wrapped}; +use crate::config::Config; use crate::tests::{TestResult, TestStatus}; use crate::{Verbosity, validator}; use anyhow::{Context, Result}; @@ -27,10 +28,7 @@ pub struct ValidatorSettings { pub json: bool, /// Only run the tests that match this case-insensitive regular expression. #[arg(short = 'f', long)] - pub test_filter: Option, - /// Changes the behavior of -f/--test-filter to skip matching tests instead. - #[arg(short = 'v', long)] - pub invert_filter: bool, + pub filter: Option, /// When running the validation out-of-process, hide the plugin's output. /// /// This can be useful for validating noisy plugins. @@ -81,51 +79,17 @@ pub struct SingleTestSettings { /// The main validator command. This will validate one or more plugins and print the results. pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { - let mut result = validator::validate(verbosity, settings).context("Could not run the validator")?; + let config = Config::from_current()?; + + let mut result = validator::validate(verbosity, settings, &config).context("Could not run the validator")?; let tally = result.tally(); - // Filtering out tests should be done after we did the tally for consistency's sake if settings.only_failed { - // The `.drain_filter()` methods have not been stabilized yet, so to make things - // easy for us we'll just inefficiently rebuild the data structures - result.plugin_library_tests = result - .plugin_library_tests - .into_iter() - .filter_map(|(library_path, tests)| { - let tests: Vec<_> = tests - .into_iter() - .filter(|test| test.status.failed_or_warning()) - .collect(); - if tests.is_empty() { - None - } else { - Some((library_path, tests)) - } - }) - .collect(); - - result.plugin_tests = result - .plugin_tests - .into_iter() - .filter_map(|(plugin_id, tests)| { - let tests: Vec<_> = tests - .into_iter() - .filter(|test| test.status.failed_or_warning()) - .collect(); - if tests.is_empty() { - None - } else { - Some((plugin_id, tests)) - } - }) - .collect(); + result = result.filter(|test| test.status.failed_or_warning()); } if settings.json { - println!( - "{}", - serde_json::to_string_pretty(&result).expect("Could not format JSON") - ); + println!("{}", serde_json::to_string_pretty(&result)?); } else { fn print_test(wrapper: &mut TextWrapper, test: &TestResult) { println_wrapped!( diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ac844a5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,41 @@ +use anyhow::{Context, Result}; +use std::collections::HashMap; + +#[derive(Debug, Default, serde::Deserialize)] +pub struct Config { + pub test: HashMap, +} + +impl Config { + pub fn from_current() -> Result { + // use env var if set + if let Ok(path) = std::env::var("CLAP_VALIDATOR_CONFIG") { + return Self::from_file(&path).context(path); + } + + // scan up and look for clap-validator.toml + let mut current_dir = std::env::current_dir()?.canonicalize()?; + loop { + let config_path = current_dir.join("clap-validator.toml"); + if config_path.exists() { + return Self::from_file(&config_path); + } + + if !current_dir.pop() { + break; + } + } + + Ok(Self::default()) + } + + pub fn from_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + let contents = std::fs::read_to_string(path).with_context(|| path.display().to_string())?; + basic_toml::from_str(&contents).with_context(|| path.display().to_string()) + } + + pub fn is_test_enabled(&self, test_name: &str) -> bool { + self.test.get(test_name).copied().unwrap_or(true) + } +} diff --git a/src/debug.rs b/src/debug.rs index b0ce5f7..211bb1d 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -5,3 +5,9 @@ mod trace; pub use log::*; pub use panic::*; pub use trace::*; + +/// Records a value in the current tracing span and returns it. +pub fn record(name: &'static str, value: T) -> T { + tracing::Span::current().record(name, tracing::field::debug(&value)); + value +} diff --git a/src/debug/trace.rs b/src/debug/trace.rs index cddd114..e0c9b4b 100644 --- a/src/debug/trace.rs +++ b/src/debug/trace.rs @@ -70,14 +70,15 @@ impl Subscriber for ChromeJsonSubscriber { fn new_span(&self, span: &Attributes<'_>) -> Id { THREAD_DATA.with_borrow_mut(|thread| { let id = Id::from_u64(NEXT_SPAN_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)); - let mut args = thread.new_args(); + let mut args = TraceArgs::default(); span.record(&mut args); - thread.active_spans.push(ThreadSpan { + + thread.spans.push(ThreadSpan { id: id.clone(), meta: span.metadata(), - args, uses: 1, + args, }); id @@ -99,11 +100,8 @@ impl Subscriber for ChromeJsonSubscriber { if let Some(span) = thread.span_mut(&id) { span.uses -= 1; if span.uses == 0 { - let span = thread.remove_span(&id); - if let Some(span) = span { - thread.free_args(span.args); - return true; - } + thread.remove_span(&id); + return true; } } @@ -123,7 +121,7 @@ impl Subscriber for ChromeJsonSubscriber { fn current_span(&self) -> Current { THREAD_DATA.with_borrow(|thread| { - if let Some(span) = thread.active_spans.last() { + if let Some(span) = thread.spans.last() { Current::new(span.id.clone(), span.meta) } else { Current::none() @@ -135,8 +133,9 @@ impl Subscriber for ChromeJsonSubscriber { let time = self.start.elapsed().as_micros(); THREAD_DATA.with_borrow_mut(|thread| { - let mut args = thread.new_args(); + let mut args = TraceArgs::default(); event.record(&mut args); + args.values.insert("level", format!("{:?}", event.metadata().level())); self.emit(TraceEvent { name: args.values.get("message").map(|s| s.as_str()).unwrap_or("?"), @@ -147,8 +146,6 @@ impl Subscriber for ChromeJsonSubscriber { pid: 1, ph: "n", }); - - thread.reuse_args.push(args); }); } @@ -211,10 +208,7 @@ struct ThreadData { /// A "stack" of currently active spans on this thread, in the order they were entered /// In most cases (FIFO) this is extremely fast, but this also allows for out-of-order/overlapping spans at the cost of O(n) lookups - active_spans: Vec, - - /// A list of `TraceArgs` for reuse, to minimize allocations. - reuse_args: Vec, + spans: Vec, } impl ThreadData { @@ -224,39 +218,26 @@ impl ThreadData { .name() .map(|s| s.to_string()) .unwrap_or_else(|| format!("{:?}", std::thread::current().id())), - active_spans: Vec::new(), - reuse_args: Vec::new(), + spans: Vec::new(), } } /// Find a span by its ID fn span(&self, id: &Id) -> Option<&ThreadSpan> { - self.active_spans.iter().find(|span| &span.id == id) + self.spans.iter().find(|span| &span.id == id) } /// Find a span by its ID fn span_mut(&mut self, id: &Id) -> Option<&mut ThreadSpan> { - self.active_spans.iter_mut().rfind(|span| &span.id == id) + self.spans.iter_mut().rfind(|span| &span.id == id) } /// Remove a span by its ID, returning it if found fn remove_span(&mut self, id: &Id) -> Option { - self.active_spans + self.spans .iter() .rposition(|span| &span.id == id) - .map(|idx| self.active_spans.remove(idx)) - } - - /// Constructs a new `TraceArgs`, reusing one from the pool if available. - fn new_args(&mut self) -> TraceArgs { - let mut args = self.reuse_args.pop().unwrap_or_default(); - args.values.clear(); - args - } - - /// Returns a `TraceArgs` to the pool for reuse. - fn free_args(&mut self, args: TraceArgs) { - self.reuse_args.push(args); + .map(|idx| self.spans.remove(idx)) } } diff --git a/src/main.rs b/src/main.rs index b9dc974..3a6555b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ use tracing::level_filters::LevelFilter; use yansi::Paint; mod commands; +mod config; mod debug; -mod index; mod plugin; mod tests; mod util; @@ -102,7 +102,7 @@ fn main() -> ExitCode { let status = match &result { Ok(code) => *code, Err(err) => { - tracing::error!("{err:#}"); + eprintln!("{} {err:#}", "error:".red().bold()); ExitCode::FAILURE } }; diff --git a/src/plugin.rs b/src/plugin.rs index 0583094..9c76e92 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,6 +1,7 @@ //! Contains functions for loading and interacting with CLAP plugins. pub mod ext; +pub mod index; pub mod instance; pub mod library; pub mod preset_discovery; diff --git a/src/plugin/ext/ambisonic.rs b/src/plugin/ext/ambisonic.rs index 349dd10..ccfb103 100644 --- a/src/plugin/ext/ambisonic.rs +++ b/src/plugin/ext/ambisonic.rs @@ -1,3 +1,4 @@ +use crate::debug::record; use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; use crate::plugin::util::clap_call; @@ -26,16 +27,19 @@ impl<'a> Extension for Ambisonic<'a> { } impl<'a> Ambisonic<'a> { - #[tracing::instrument(name = "clap_plugin_ambisonic::is_config_supported", level = 1, skip(self))] + #[tracing::instrument( + name = "clap_plugin_ambisonic::is_config_supported", + level = 1, + skip(self), + fields(result) + )] pub fn is_config_supported(&self, config: &clap_ambisonic_config) -> bool { let ambisonic = self.ambisonic.as_ptr(); let plugin = self.plugin.as_ptr(); - unsafe { - clap_call! { ambisonic=>is_config_supported(plugin, config) } - } + unsafe { record("result", clap_call! { ambisonic=>is_config_supported(plugin, config) }) } } - #[tracing::instrument(name = "clap_plugin_ambisonic::get_config", level = 1, skip(self))] + #[tracing::instrument(name = "clap_plugin_ambisonic::get_config", level = 1, skip(self), fields(result))] pub fn get_config(&self, is_input: bool, port_index: u32) -> Option { let ambisonic = self.ambisonic.as_ptr(); let plugin = self.plugin.as_ptr(); @@ -43,7 +47,7 @@ impl<'a> Ambisonic<'a> { unsafe { let mut config = clap_ambisonic_config { ..zeroed() }; let result = clap_call! { ambisonic=>get_config(plugin, is_input, port_index, &mut config) }; - if result { Some(config) } else { None } + if result { Some(record("result", config)) } else { None } } } } diff --git a/src/plugin/ext/audio_ports_activation.rs b/src/plugin/ext/audio_ports_activation.rs index b431579..99f7342 100644 --- a/src/plugin/ext/audio_ports_activation.rs +++ b/src/plugin/ext/audio_ports_activation.rs @@ -1,3 +1,4 @@ +use crate::debug::record; use crate::plugin::ext::Extension; use crate::plugin::instance::Plugin; use crate::plugin::util::clap_call; @@ -33,18 +34,27 @@ impl<'a> AudioPortsActivation<'a> { name = "clap_plugin_audio_ports_activation::can_activate_while_processing", level = 1, skip(self) + fields(result), )] pub fn can_activate_while_processing(&self) -> bool { let audio_ports_activation = self.audio_ports_activation.as_ptr(); let plugin = self.plugin.as_ptr(); unsafe { - clap_call! { audio_ports_activation=>can_activate_while_processing(plugin) } + record( + "result", + clap_call! { audio_ports_activation=>can_activate_while_processing(plugin) }, + ) } } /// Activates or deactivates audio ports while inactive. #[allow(unused)] - #[tracing::instrument(name = "clap_plugin_audio_ports_activation::set_active", level = 1, skip(self))] + #[tracing::instrument( + name = "clap_plugin_audio_ports_activation::set_active", + level = 1, + skip(self), + fields(result) + )] pub fn set_active(&self, is_input: bool, port_index: u32, is_active: bool, sample_size: u32) -> Result<()> { self.plugin.status().assert_inactive(); @@ -52,8 +62,11 @@ impl<'a> AudioPortsActivation<'a> { let plugin = self.plugin.as_ptr(); unsafe { - let success = - clap_call! { audio_ports_activation=>set_active(plugin, is_input, port_index, is_active, sample_size) }; + let success = record( + "result", + clap_call! { audio_ports_activation=>set_active(plugin, is_input, port_index, is_active, sample_size) }, + ); + if success { Ok(()) } else { diff --git a/src/plugin/ext/audio_ports_config.rs b/src/plugin/ext/audio_ports_config.rs index 45d9156..f3850b1 100644 --- a/src/plugin/ext/audio_ports_config.rs +++ b/src/plugin/ext/audio_ports_config.rs @@ -1,3 +1,4 @@ +use crate::debug::record; use crate::plugin::ext::Extension; use crate::plugin::ext::ambisonic::Ambisonic; use crate::plugin::ext::audio_ports::{AudioPort, check_audio_port_info_valid, check_audio_port_type_consistent}; @@ -154,6 +155,7 @@ impl AudioPortsConfig<'_> { unsafe { let mut info = clap_audio_ports_config { ..zeroed() }; + if !clap_call! { audio_ports_config=>get(plugin, index, &mut info) } { anyhow::bail!( "audio_ports_config::get({}) returned false ({} total configs)", @@ -162,7 +164,7 @@ impl AudioPortsConfig<'_> { ); } - Ok(info) + Ok(record("result", info)) } } } diff --git a/src/index.rs b/src/plugin/index.rs similarity index 100% rename from src/index.rs rename to src/plugin/index.rs diff --git a/src/plugin/instance/audio_thread.rs b/src/plugin/instance/audio_thread.rs index e5ed2c5..c3b2d88 100644 --- a/src/plugin/instance/audio_thread.rs +++ b/src/plugin/instance/audio_thread.rs @@ -1,6 +1,7 @@ //! Abstractions for single CLAP plugin instances for audio thread interactions. use super::{Plugin, PluginStatus}; +use crate::debug::record; use crate::plugin::ext::Extension; use crate::plugin::instance::{CallbackEvent, MainThreadTask, PluginShared}; use crate::plugin::process::{InputEventQueue, OutputEventQueue}; @@ -215,9 +216,8 @@ impl<'a> PluginAudioThread<'a> { ), }; - tracing::Span::current().record("result", tracing::field::debug(&result)); - Ok(result) + Ok(record("status", result)) } /// Reset the internal state of the plugin. diff --git a/src/plugin/instance/shared.rs b/src/plugin/instance/shared.rs index 6287d10..8d5f4ed 100644 --- a/src/plugin/instance/shared.rs +++ b/src/plugin/instance/shared.rs @@ -1,4 +1,4 @@ -use crate::debug::fail_test; +use crate::debug::{fail_test, record}; use crate::plugin::ext::Extension; use crate::plugin::ext::audio_ports::AudioPorts; use crate::plugin::ext::audio_ports_config::AudioPortsConfig; @@ -156,13 +156,13 @@ impl PluginShared { self.status().assert_is_not(PluginStatus::Uninitialized); for id in T::IDS { - let span = tracing::trace_span!("clap_plugin::get_extension", extension_id = %id.to_string_lossy(), found = tracing::field::Empty).entered(); + let _span = tracing::trace_span!("clap_plugin::get_extension", extension_id = %id.to_string_lossy(), found = tracing::field::Empty).entered(); let extension_ptr = unsafe { clap_call! { self.clap_plugin=>get_extension(self.clap_plugin, id.as_ptr()) } }; - span.record("found", !extension_ptr.is_null()); + record("found", !extension_ptr.is_null()); if !extension_ptr.is_null() { return NonNull::new(extension_ptr as *mut T::Struct); @@ -376,8 +376,8 @@ impl PluginShared { std::ptr::null() }; - Span::current().record("extension_id", extension_id_cstr.to_string_lossy().as_ref()); - Span::current().record("found", !extension_ptr.is_null()); + record("extension_id", extension_id_cstr.to_string_lossy()); + record("found", !extension_ptr.is_null()); Ok(extension_ptr) }) diff --git a/src/plugin/process/events.rs b/src/plugin/process/events.rs index c1310fa..7311ea4 100644 --- a/src/plugin/process/events.rs +++ b/src/plugin/process/events.rs @@ -1,9 +1,8 @@ -use crate::debug::fail_test; +use crate::debug::{fail_test, record}; use crate::plugin::util::{CHECK_POINTER, Proxy, Proxyable}; use clap_sys::events::*; use std::fmt::Debug; use std::sync::Mutex; -use tracing::Span; #[derive(Debug)] pub struct InputEventQueue(Mutex>); @@ -111,7 +110,7 @@ impl InputEventQueue { let events = state.0.lock().unwrap(); match events.get(index as usize) { Some(event) => { - Span::current().record("event", tracing::field::debug(&event)); + record("event", &event); event.header() } None => { @@ -158,7 +157,7 @@ impl OutputEventQueue { // The monotonicity of the plugin's event insertion order is checked as part of the output // consistency checks - Span::current().record("event", tracing::field::debug(&event)); + record("event", &event); state.0.lock().unwrap().push(event); true } diff --git a/src/tests.rs b/src/tests.rs index a253207..064c7ba 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -13,7 +13,6 @@ use crate::util; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::any::TypeId; -use std::collections::BTreeMap; use std::fmt::Display; use std::fs; use std::path::PathBuf; @@ -68,8 +67,16 @@ pub enum TestStatus { #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] pub struct TestList { - pub plugin_library_tests: BTreeMap, - pub plugin_tests: BTreeMap, + pub plugin_library_tests: Vec, + pub plugin_tests: Vec, +} + +/// A single item in the test list. +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TestListItem { + pub name: String, + pub description: String, } /// An abstraction for a test case. This mostly exists because we need two separate kinds of tests @@ -138,15 +145,20 @@ impl TestStatus { } } +impl TestListItem { + pub fn from<'a, T: TestCase<'a>>(test_case: &T) -> Self { + Self { + name: test_case.to_string(), + description: test_case.description(), + } + } +} + impl Default for TestList { fn default() -> Self { Self { - plugin_library_tests: PluginLibraryTestCase::iter() - .map(|c| (c.to_string(), c.description())) - .collect(), - plugin_tests: PluginTestCase::iter() - .map(|c| (c.to_string(), c.description())) - .collect(), + plugin_library_tests: PluginLibraryTestCase::iter().map(|c| TestListItem::from(&c)).collect(), + plugin_tests: PluginTestCase::iter().map(|c| TestListItem::from(&c)).collect(), } } } diff --git a/src/tests/plugin.rs b/src/tests/plugin.rs index 5271b87..4f48954 100644 --- a/src/tests/plugin.rs +++ b/src/tests/plugin.rs @@ -20,39 +20,39 @@ pub enum PluginTestCase { #[strum(serialize = "descriptor-consistency")] DescriptorConsistency, #[strum(serialize = "features-categories")] - FeaturesCategories, + FeaturesCategories, #[strum(serialize = "features-duplicates")] - FeaturesDuplicates, + FeaturesDuplicates, #[strum(serialize = "layout-audio-ports-activation")] - LayoutAudioPortsActivation, + LayoutAudioPortsActivation, #[strum(serialize = "layout-audio-ports-config")] - LayoutAudioPortsConfig, + LayoutAudioPortsConfig, #[strum(serialize = "layout-configurable-audio-ports")] - LayoutConfigurableAudioPorts, + LayoutConfigurableAudioPorts, #[strum(serialize = "process-audio-basic-out-of-place")] - ProcessAudioBasicOutOfPlace, + ProcessAudioBasicOutOfPlace, #[strum(serialize = "process-audio-basic-in-place")] ProcessAudioBasicInPlace, #[strum(serialize = "process-audio-double-out-of-place")] ProcessAudioDoubleOutOfPlace, #[strum(serialize = "process-audio-double-in-place")] - ProcessAudioDoubleInPlace, + ProcessAudioDoubleInPlace, #[strum(serialize = "process-sleep-constant-mask")] ProcessSleepConstantMask, #[strum(serialize = "process-sleep-process-status")] ProcessSleepProcessStatus, #[strum(serialize = "process-audio-reset-determinism")] - ProcessAudioResetDeterminism, + ProcessAudioResetDeterminism, #[strum(serialize = "process-note-out-of-place-basic")] - ProcessNoteOutOfPlaceBasic, + ProcessNoteOutOfPlaceBasic, #[strum(serialize = "process-note-inconsistent")] ProcessNoteInconsistent, #[strum(serialize = "process-varying-sample-rates")] ProcessVaryingSampleRates, #[strum(serialize = "process-varying-block-sizes")] - ProcessVaryingBlockSizes, + ProcessVaryingBlockSizes, #[strum(serialize = "process-random-block-sizes")] - ProcessRandomBlockSizes, + ProcessRandomBlockSizes, #[strum(serialize = "param-conversions")] ParamConversions, #[strum(serialize = "param-fuzz-basic")] diff --git a/src/validator.rs b/src/validator.rs index d35e45f..d05d24e 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -3,6 +3,7 @@ use crate::Verbosity; use crate::commands::validate::{SingleTestSettings, ValidatorSettings}; +use crate::config::Config; use crate::debug::panic_message; use crate::plugin::library::{PluginLibrary, PluginMetadata}; use crate::tests::{PluginLibraryTestCase, PluginTestCase, SerializedTest, TestCase, TestResult, TestStatus}; @@ -50,14 +51,36 @@ pub struct ValidationTally { pub num_warnings: u32, } +impl ValidationResult { + pub fn filter(mut self, mut f: impl FnMut(&TestResult) -> bool) -> Self { + self.plugin_tests.values_mut().for_each(|tests| tests.retain(&mut f)); + self.plugin_library_tests + .values_mut() + .for_each(|tests| tests.retain(&mut f)); + + self + } +} + /// Run the validator using the specified settings. Returns an error if any of the plugin paths /// could not loaded, or if the plugin ID filter did not match any plugins. -pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result { - let test_regex = settings - .test_filter - .as_ref() - .map(|x| Regex::new(x).with_context(|| format!("Could not parse the test filter regular expression '{}'", x))) - .transpose()?; +pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings, config: &Config) -> Result { + let test_filter = { + let test_filter_regex = settings + .filter + .as_ref() + .map(|x| { + Regex::new(x).with_context(|| format!("Could not parse the test filter regular expression '{}'", x)) + }) + .transpose()?; + + move |id: &str| { + let config_enabled = config.is_test_enabled(id); + let filter_enabled = test_filter_regex.as_ref().is_none_or(|f| f.is_match(id)); + + config_enabled && filter_enabled + } + }; // The tests can optionally be run in parallel. This is not the default since some plugins may // not handle it correctly, event when the plugins are loaded in different processes. It's also @@ -79,7 +102,7 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result>>()?, ); @@ -113,7 +136,7 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result Result<()> { Ok(()) } -/// The filter function for determining whether or not a test should be run based on the validator's -/// settings settings. -fn test_filter<'a, T: TestCase<'a>>(test: &T, settings: &ValidatorSettings, test_filter: Option<&Regex>) -> bool { - let test_name = test.to_string(); - match (test_filter, settings.invert_filter) { - (Some(test_filter), false) if !test_filter.is_match(&test_name) => false, - (Some(test_filter), true) if test_filter.is_match(&test_name) => false, - _ => true, - } -} - /// The filter function for determining whether or not tests should be run for a particular plugin. fn plugin_filter(plugin_metadata: &PluginMetadata, settings: &ValidatorSettings) -> bool { // It's possible to filter by plugin ID in case you want to validate a single plugin From 376375375a56dc04c1dbc07ca06a6730c78fe817 Mon Sep 17 00:00:00 2001 From: Quant1um Date: Thu, 5 Feb 2026 02:19:56 +0400 Subject: [PATCH 057/114] out-of-process plugin scan --- src/commands/list.rs | 669 +++++++++++------- src/commands/validate.rs | 6 +- src/main.rs | 44 +- src/plugin/index.rs | 193 ++--- src/plugin/library.rs | 7 +- src/plugin/preset_discovery/indexer.rs | 32 +- .../preset_discovery/metadata_receiver.rs | 51 +- src/validator.rs | 57 +- 8 files changed, 587 insertions(+), 472 deletions(-) diff --git a/src/commands/list.rs b/src/commands/list.rs index eb217ad..d9c64d1 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,11 +1,15 @@ //! Commands for listing information about the validator or installed plugins. use super::{TextWrapper, println_wrapped, println_wrapped_no_indent}; -use crate::plugin::index::{PresetIndexResult, index, index_presets}; +use crate::Verbosity; +use crate::commands::list::scan_out_of_process::ScanStatus; +use crate::plugin::index::{index_plugins, scan_plugin}; use crate::plugin::preset_discovery::PresetFile; use anyhow::{Context, Result}; use clap::Subcommand; -use std::path::{Path, PathBuf}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use std::collections::BTreeMap; +use std::path::PathBuf; use std::process::ExitCode; use yansi::Paint; @@ -17,12 +21,18 @@ pub enum ListCommand { /// Print JSON instead of a human readable format. #[arg(short, long)] json: bool, + /// Run the plugin indexing in-process instead of out-of-process. + #[arg(long)] + in_process: bool, }, /// Lists the available presets for one, more, or all installed CLAP plugins. Presets { /// Print JSON instead of a human readable format. #[arg(short, long)] json: bool, + /// Run the plugin indexing in-process instead of out-of-process. + #[arg(long)] + in_process: bool, /// Paths to one or more plugins that should be indexed for presets, optional. /// /// All installed plugins are crawled if this value is missing. @@ -36,76 +46,261 @@ pub enum ListCommand { }, } -pub fn list(command: &ListCommand) -> Result { +pub fn list(verbosity: Verbosity, command: &ListCommand) -> Result { match command { - ListCommand::Plugins { json } => list_plugins(*json), - ListCommand::Presets { json, paths } => list_presets(*json, paths.as_deref()), ListCommand::Tests { json } => list_tests(*json), + ListCommand::Plugins { json, in_process } => list_plugins(*json, *in_process, verbosity), + ListCommand::Presets { + json, + in_process, + paths, + } => list_presets(*json, *in_process, verbosity, paths.clone()), } } -// TODO: The indexing here always happens in the same process. We should move this over to out of -// process scanning at some point. +/// List presets for one, more, or all installed CLAP plugins. +fn list_presets(json: bool, in_process: bool, verbosity: Verbosity, paths: Option>) -> Result { + let plugins = match paths { + Some(paths) => paths, + None => index_plugins().context("Error while crawling plugins")?, + }; -/// Lists basic information about all installed CLAP plugins. -fn list_plugins(json: bool) -> Result { - let plugin_index = index(); + let results = if in_process { + plugins + .into_iter() + .map(|path| match scan_plugin(&path, true) { + Ok(library) => (path, ScanStatus::Success { library }), + Err(err) => ( + path, + ScanStatus::Error { + details: format!("{err:#}"), + }, + ), + }) + .collect::>() + } else { + plugins + .into_par_iter() + .map(|path| { + let result = scan_out_of_process::spawn(&path, true, verbosity)?; + Ok((path, result)) + }) + .collect::>>()? + }; if json { println!( "{}", - serde_json::to_string_pretty(&plugin_index).expect("Could not format JSON") + serde_json::to_string_pretty(&results).expect("Could not format JSON") ); } else { let mut wrapper = TextWrapper::default(); - for (i, (plugin_path, metadata)) in plugin_index.0.into_iter().enumerate() { + for (i, (plugin_path, status)) in results.into_iter().enumerate() { if i > 0 { println!(); } - println_wrapped!( - wrapper, - "{}: (CLAP {}.{}.{}, contains {} {})", - plugin_path.display(), - metadata.version.0, - metadata.version.1, - metadata.version.2, - metadata.plugins.len(), - if metadata.plugins.len() == 1 { - "plugin" - } else { - "plugins" - }, - ); - - for plugin in metadata.plugins { - println!(); - println_wrapped!( - wrapper, - " - {} {} ({})", - plugin.name, - plugin.version.as_deref().unwrap_or("(unknown version)"), - plugin.id - ); - - // Whether it makes sense to always show optional fields or not depends on - // the field - if let Some(description) = plugin.description { - println_wrapped_no_indent!(wrapper, " {description}"); + match status { + ScanStatus::Error { details } => { + println_wrapped!(wrapper, "{} - {}: {}", plugin_path.display(), "ERROR".red(), details); } - println!(); - println_wrapped!( - wrapper, - " vendor: {}", - plugin.vendor.as_deref().unwrap_or("(unknown)") - ); - if let Some(manual_url) = plugin.manual_url { - println_wrapped!(wrapper, " manual url: {manual_url}"); + + ScanStatus::Crashed { details } => { + println_wrapped!( + wrapper, + "{} - {}: {}", + plugin_path.display(), + "CRASHED".red().bold(), + details + ); } - if let Some(support_url) = plugin.support_url { - println_wrapped!(wrapper, " support url: {support_url}"); + + ScanStatus::Success { library } => { + println_wrapped!( + wrapper, + "{}: (contains {} {})", + plugin_path.display(), + library.preset_providers.len(), + if library.preset_providers.len() == 1 { + "preset provider" + } else { + "preset providers" + } + ); + println!(); + + for (i, provider) in library.preset_providers.into_iter().enumerate() { + if i > 0 { + println!(); + } + + println_wrapped!( + wrapper, + " - {} ({}) (contains {} {}, {} {}):", + provider.provider_name, + provider.provider_vendor.as_deref().unwrap_or("unknown vendor"), + provider.soundpacks.len(), + if provider.soundpacks.len() == 1 { + "soundpack" + } else { + "soundpacks" + }, + provider.presets.len(), + if provider.presets.len() == 1 { + "preset" + } else { + "presets" + }, + ); + + if !provider.soundpacks.is_empty() { + println!(); + println!(" Soundpacks:"); + + for soundpack in provider.soundpacks { + println!(); + println_wrapped!(wrapper, " - {} ({})", soundpack.name, soundpack.id); + if let Some(description) = soundpack.description { + println_wrapped_no_indent!(wrapper, " {}", description); + } + println!(); + println_wrapped!( + wrapper, + " vendor: {}", + soundpack.vendor.as_deref().unwrap_or("(unknown)") + ); + if let Some(homepage_url) = soundpack.homepage_url { + println_wrapped!(wrapper, " homepage url: {homepage_url}"); + } + if let Some(image_path) = soundpack.image_path { + println_wrapped!(wrapper, " image path: {image_path}"); + } + if let Some(release_timestamp) = soundpack.release_timestamp { + println_wrapped!(wrapper, " released: {release_timestamp}"); + } + println_wrapped!(wrapper, " flags: {}", soundpack.flags); + } + } + + if !provider.presets.is_empty() { + println!(); + println!(" Presets:"); + + for (preset_uri, preset_file) in provider.presets { + println!(); + match preset_file { + PresetFile::Single(preset) => { + println_wrapped!(wrapper, " - {}", preset_uri); + + println!(); + println_wrapped!( + wrapper, + " {} ({})", + preset.name, + preset.plugin_ids_string() + ); + if let Some(description) = preset.description { + println_wrapped_no_indent!(wrapper, " {}", description); + } + println!(); + if !preset.creators.is_empty() { + println_wrapped!( + wrapper, + " {}: {}", + if preset.creators.len() == 1 { + "creator" + } else { + "creators" + }, + preset.creators.join(", ") + ); + } + if let Some(soundpack_id) = preset.soundpack_id { + println_wrapped!(wrapper, " soundpack: {soundpack_id}"); + } + if let Some(creation_time) = preset.creation_time { + println_wrapped!(wrapper, " created: {creation_time}"); + } + if let Some(modification_time) = preset.modification_time { + println_wrapped!(wrapper, " modified: {modification_time}"); + } + println_wrapped!(wrapper, " flags: {}", preset.flags); + if !preset.features.is_empty() { + println_wrapped!( + wrapper, + " features: [{}]", + preset.features.join(", ") + ); + } + if !preset.extra_info.is_empty() { + println_wrapped!(wrapper, " extra info: {:#?}", preset.extra_info); + } + } + PresetFile::Container(presets) => { + println_wrapped!( + wrapper, + " - {} (contains {} {})", + preset_uri, + presets.len(), + if presets.len() == 1 { "preset" } else { "presets" } + ); + + for (load_key, preset) in presets { + println!(); + println_wrapped!( + wrapper, + " - {} ({}, {})", + preset.name, + load_key, + preset.plugin_ids_string() + ); + if let Some(description) = preset.description { + println_wrapped_no_indent!(wrapper, " {}", description); + } + println!(); + if !preset.creators.is_empty() { + println_wrapped!( + wrapper, + " {}: {}", + if preset.creators.len() == 1 { + "creator" + } else { + "creators" + }, + preset.creators.join(", ") + ); + } + if let Some(soundpack_id) = preset.soundpack_id { + println_wrapped!(wrapper, " soundpack: {soundpack_id}"); + } + if let Some(creation_time) = preset.creation_time { + println_wrapped!(wrapper, " created: {creation_time}"); + } + if let Some(modification_time) = preset.modification_time { + println_wrapped!(wrapper, " modified: {modification_time}"); + } + println_wrapped!(wrapper, " flags: {}", preset.flags); + if !preset.features.is_empty() { + println_wrapped!( + wrapper, + " features: [{}]", + preset.features.join(", ") + ); + } + if !preset.extra_info.is_empty() { + println_wrapped!( + wrapper, + " extra info: {:#?}", + preset.extra_info + ); + } + } + } + } + } + } + } } - println_wrapped!(wrapper, " features: [{}]", plugin.features.join(", ")); } } } @@ -113,225 +308,110 @@ fn list_plugins(json: bool) -> Result { Ok(ExitCode::SUCCESS) } -/// Lists presets for one, more, or all plugins. -fn list_presets

(json: bool, plugin_paths: Option<&[P]>) -> Result -where - P: AsRef, -{ - let preset_index = match plugin_paths { - Some(plugin_paths) => index_presets(plugin_paths, false), - None => { - let plugin_index = index(); - let all_plugin_paths = plugin_index.0.keys(); - - // This 'true' indicates that plugins that don't support the preset discovery mechanism - // should be silently skipped - index_presets(all_plugin_paths, true) - } - } - .context("Error while crawling presets")?; - let has_errors = preset_index - .0 - .values() - .any(|result| matches!(result, PresetIndexResult::Error(_))); +/// Lists basic information about all installed CLAP plugins. +fn list_plugins(json: bool, in_process: bool, verbosity: Verbosity) -> Result { + let plugins = index_plugins().context("Error while crawling plugins")?; + let results = if in_process { + plugins + .into_iter() + .map(|path| match scan_plugin(&path, false) { + Ok(library) => (path, ScanStatus::Success { library }), + Err(err) => ( + path, + ScanStatus::Error { + details: format!("{err:#}"), + }, + ), + }) + .collect::>() + } else { + plugins + .into_par_iter() + .map(|path| { + let result = scan_out_of_process::spawn(&path, false, verbosity)?; + Ok((path, result)) + }) + .collect::>>()? + }; if json { println!( "{}", - serde_json::to_string_pretty(&preset_index).expect("Could not format JSON") + serde_json::to_string_pretty(&results).expect("Could not format JSON") ); } else { let mut wrapper = TextWrapper::default(); - for (i, (plugin_path, result)) in preset_index.0.into_iter().enumerate() { + for (i, (plugin_path, status)) in results.into_iter().enumerate() { if i > 0 { println!(); } - let provider_results = match result { - PresetIndexResult::Success(provider_results) => provider_results, - PresetIndexResult::Error(error) => { - println_wrapped!(wrapper, "{}:", plugin_path.display()); - println!(); - println_wrapped!(wrapper, " {}: {}", "FAILED".red(), error); - continue; - } - }; - - println_wrapped!( - wrapper, - "{}: (contains {} {})", - plugin_path.display(), - provider_results.len(), - if provider_results.len() == 1 { - "preset provider" - } else { - "preset providers" + match status { + ScanStatus::Error { details } => { + println_wrapped!(wrapper, "{} - {}: {}", plugin_path.display(), "ERROR".red(), details); } - ); - println!(); - for (i, provider_result) in provider_results.into_iter().enumerate() { - if i > 0 { - println!(); + ScanStatus::Crashed { details } => { + println_wrapped!( + wrapper, + "{} - {}: {}", + plugin_path.display(), + "CRASHED".red().bold(), + details + ); } - println_wrapped!( - wrapper, - " - {} ({}) (contains {} {}, {} {}):", - provider_result.provider_name, - provider_result.provider_vendor.as_deref().unwrap_or("unknown vendor"), - provider_result.soundpacks.len(), - if provider_result.soundpacks.len() == 1 { - "soundpack" - } else { - "soundpacks" - }, - provider_result.presets.len(), - if provider_result.presets.len() == 1 { - "preset" - } else { - "presets" - }, - ); - - if !provider_result.soundpacks.is_empty() { - println!(); - println!(" Soundpacks:"); + ScanStatus::Success { library } => { + println_wrapped!( + wrapper, + "{}: (CLAP {}.{}.{}, contains {} {})", + plugin_path.display(), + library.metadata.version.0, + library.metadata.version.1, + library.metadata.version.2, + library.metadata.plugins.len(), + if library.metadata.plugins.len() == 1 { + "plugin" + } else { + "plugins" + }, + ); - for soundpack in provider_result.soundpacks { + for plugin in library.metadata.plugins { println!(); - println_wrapped!(wrapper, " - {} ({})", soundpack.name, soundpack.id); - if let Some(description) = soundpack.description { - println_wrapped_no_indent!(wrapper, " {}", description); + println_wrapped!( + wrapper, + " - {} {} ({})", + plugin.name, + plugin.version.as_deref().unwrap_or("(unknown version)"), + plugin.id + ); + + // Whether it makes sense to always show optional fields or not depends on + // the field + if let Some(description) = plugin.description { + println_wrapped_no_indent!(wrapper, " {description}"); } println!(); println_wrapped!( wrapper, - " vendor: {}", - soundpack.vendor.as_deref().unwrap_or("(unknown)") + " vendor: {}", + plugin.vendor.as_deref().unwrap_or("(unknown)") ); - if let Some(homepage_url) = soundpack.homepage_url { - println_wrapped!(wrapper, " homepage url: {homepage_url}"); - } - if let Some(image_path) = soundpack.image_path { - println_wrapped!(wrapper, " image path: {image_path}"); + if let Some(manual_url) = plugin.manual_url { + println_wrapped!(wrapper, " manual url: {manual_url}"); } - if let Some(release_timestamp) = soundpack.release_timestamp { - println_wrapped!(wrapper, " released: {release_timestamp}"); - } - println_wrapped!(wrapper, " flags: {}", soundpack.flags); - } - } - - if !provider_result.presets.is_empty() { - println!(); - println!(" Presets:"); - - for (preset_uri, preset_file) in provider_result.presets { - println!(); - match preset_file { - PresetFile::Single(preset) => { - println_wrapped!(wrapper, " - {}", preset_uri); - - println!(); - println_wrapped!(wrapper, " {} ({})", preset.name, preset.plugin_ids_string()); - if let Some(description) = preset.description { - println_wrapped_no_indent!(wrapper, " {}", description); - } - println!(); - if !preset.creators.is_empty() { - println_wrapped!( - wrapper, - " {}: {}", - if preset.creators.len() == 1 { - "creator" - } else { - "creators" - }, - preset.creators.join(", ") - ); - } - if let Some(soundpack_id) = preset.soundpack_id { - println_wrapped!(wrapper, " soundpack: {soundpack_id}"); - } - if let Some(creation_time) = preset.creation_time { - println_wrapped!(wrapper, " created: {creation_time}"); - } - if let Some(modification_time) = preset.modification_time { - println_wrapped!(wrapper, " modified: {modification_time}"); - } - println_wrapped!(wrapper, " flags: {}", preset.flags); - if !preset.features.is_empty() { - println_wrapped!(wrapper, " features: [{}]", preset.features.join(", ")); - } - if !preset.extra_info.is_empty() { - println_wrapped!(wrapper, " extra info: {:#?}", preset.extra_info); - } - } - PresetFile::Container(presets) => { - println_wrapped!( - wrapper, - " - {} (contains {} {})", - preset_uri, - presets.len(), - if presets.len() == 1 { "preset" } else { "presets" } - ); - - for (load_key, preset) in presets { - println!(); - println_wrapped!( - wrapper, - " - {} ({}, {})", - preset.name, - load_key, - preset.plugin_ids_string() - ); - if let Some(description) = preset.description { - println_wrapped_no_indent!(wrapper, " {}", description); - } - println!(); - if !preset.creators.is_empty() { - println_wrapped!( - wrapper, - " {}: {}", - if preset.creators.len() == 1 { - "creator" - } else { - "creators" - }, - preset.creators.join(", ") - ); - } - if let Some(soundpack_id) = preset.soundpack_id { - println_wrapped!(wrapper, " soundpack: {soundpack_id}"); - } - if let Some(creation_time) = preset.creation_time { - println_wrapped!(wrapper, " created: {creation_time}"); - } - if let Some(modification_time) = preset.modification_time { - println_wrapped!(wrapper, " modified: {modification_time}"); - } - println_wrapped!(wrapper, " flags: {}", preset.flags); - if !preset.features.is_empty() { - println_wrapped!(wrapper, " features: [{}]", preset.features.join(", ")); - } - if !preset.extra_info.is_empty() { - println_wrapped!(wrapper, " extra info: {:#?}", preset.extra_info); - } - } - } + if let Some(support_url) = plugin.support_url { + println_wrapped!(wrapper, " support url: {support_url}"); } + println_wrapped!(wrapper, " features: [{}]", plugin.features.join(", ")); } } } } } - Ok(if has_errors { - ExitCode::FAILURE - } else { - ExitCode::SUCCESS - }) + Ok(ExitCode::SUCCESS) } /// Lists all available test cases. @@ -373,3 +453,110 @@ fn list_tests(json: bool) -> Result { Ok(ExitCode::SUCCESS) } + +pub mod scan_out_of_process { + use crate::Verbosity; + use crate::plugin::index::{ScannedPlugin, scan_plugin}; + use anyhow::{Context, Result}; + use clap::{Args, ValueEnum}; + use serde::{Deserialize, Serialize}; + use std::ffi::OsStr; + use std::path::{Path, PathBuf}; + use std::process::{Command, ExitCode}; + use std::time::Duration; + use wait_timeout::ChildExt; + + #[derive(Debug, Args)] + pub struct Settings { + #[arg(long)] + pub plugin_path: PathBuf, + #[arg(long)] + pub output_file: PathBuf, + #[arg(long)] + pub scan_presets: bool, + } + + #[derive(Debug, Serialize, Deserialize)] + pub enum ScanStatus { + Success { library: ScannedPlugin }, + Error { details: String }, + Crashed { details: String }, + } + + pub fn spawn(plugin_path: &Path, scan_presets: bool, verbosity: Verbosity) -> Result { + const WAIT_TIMEOUT: Duration = Duration::from_secs(30); + + // This temporary file will automatically be removed when this function exits + let output_file_path = tempfile::Builder::new() + .suffix(".json") + .tempfile() + .context("Could not create a temporary file path")? + .into_temp_path(); + + let mut command = + Command::new(std::env::current_exe().context("Could not find the path to the current executable")?); + + command + .arg("--verbosity") + .arg(verbosity.to_possible_value().unwrap().get_name()) + .arg("scan-out-of-process") + .args([OsStr::new("--output-file"), output_file_path.as_os_str()]) + .args([OsStr::new("--plugin-path"), plugin_path.as_os_str()]); + + if scan_presets { + command.arg("--scan-presets"); + } + + let status = command + .spawn() + .context("Could not call clap-validator for out-of-process scanning")? + .wait_timeout(WAIT_TIMEOUT) + .context("Error while waiting on clap-validator to finish running the scan")?; + + match status { + None => Ok(ScanStatus::Crashed { + details: format!("Timed out after {} seconds", WAIT_TIMEOUT.as_secs()), + }), + + Some(status) if !status.success() => Ok(ScanStatus::Crashed { + details: status.to_string(), + }), + + _ => { + // At this point, the child process _should_ have written its output to `output_file_path`, + // and we can just parse it from there + let result = serde_json::from_str(&std::fs::read_to_string(&output_file_path).with_context(|| { + format!( + "Could not read the child process output from '{}'", + output_file_path.display() + ) + })?) + .context("Could not parse the child process output to JSON")?; + + Ok(result) + } + } + } + + pub fn run(settings: &Settings) -> Result { + let result = match scan_plugin(&settings.plugin_path, settings.scan_presets) { + Ok(plugin) => ScanStatus::Success { library: plugin }, + Err(err) => ScanStatus::Error { + details: format!("{err:#}"), + }, + }; + + std::fs::write( + &settings.output_file, + serde_json::to_string(&result).context("Could not serialize the test result to JSON")?, + ) + .with_context(|| { + format!( + "Could not write the scan result to '{}'", + settings.output_file.display() + ) + })?; + + Ok(ExitCode::SUCCESS) + } +} diff --git a/src/commands/validate.rs b/src/commands/validate.rs index 030a710..c9d9273 100644 --- a/src/commands/validate.rs +++ b/src/commands/validate.rs @@ -63,7 +63,7 @@ pub struct ValidatorSettings { /// Options for running a single test. This is used for the out-of-process testing method. This /// option is hidden from the CLI as it's merely an implementation detail. #[derive(Debug, Args)] -pub struct SingleTestSettings { +pub struct OutOfProcessSettings { /// The type of test (plugin library or plugin) to run. pub test_type: String, /// The name of the test to run. @@ -169,7 +169,7 @@ pub fn validate(verbosity: Verbosity, settings: &ValidatorSettings) -> Result