diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index 6c347d577..53f6c7b42 100644 --- a/src/wrapper/vst3.rs +++ b/src/wrapper/vst3.rs @@ -10,6 +10,9 @@ pub mod subcategories; mod view; mod wrapper; +#[cfg(test)] +mod setup_processing_tests; + /// Re-export for the wrapper. pub use factory::PluginInfo; pub use vst3_sys; diff --git a/src/wrapper/vst3/context.rs b/src/wrapper/vst3/context.rs index 75b5b5036..88e9d356d 100644 --- a/src/wrapper/vst3/context.rs +++ b/src/wrapper/vst3/context.rs @@ -7,7 +7,7 @@ use vst3_sys::vst::IComponentHandler; use crate::prelude::{ GuiContext, InitContext, ParamPtr, PluginApi, PluginNoteEvent, PluginState, ProcessContext, - Transport, Vst3Plugin, + ProcessMode, Transport, Vst3Plugin, }; use super::inner::{Task, WrapperInner}; @@ -42,6 +42,7 @@ pub(crate) struct WrapperProcessContext<'a, P: Vst3Plugin> { pub(super) input_events_guard: AtomicRefMut<'a, VecDeque>>, pub(super) output_events_guard: AtomicRefMut<'a, VecDeque>>, pub(super) transport: Transport, + pub(super) process_mode: ProcessMode, } /// A [`GuiContext`] implementation for the wrapper. This is passed to the plugin in @@ -86,6 +87,10 @@ impl ProcessContext

for WrapperProcessContext<'_, P> { PluginApi::Vst3 } + fn host_render_mode(&self) -> Option { + Some(self.process_mode) + } + fn execute_background(&self, task: P::BackgroundTask) { let task_posted = self.inner.schedule_background(Task::PluginTask(task)); nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index add0a1a1f..7ae22d260 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -71,8 +71,6 @@ pub(crate) struct WrapperInner { /// The current buffer configuration, containing the sample rate and the maximum block size. /// Will be set in `IAudioProcessor::setupProcessing()`. pub current_buffer_config: AtomicCell>, - /// The current audio processing mode. Set in `IAudioProcessor::setup_processing()`. - pub current_process_mode: AtomicCell, /// The last process status returned by the plugin. This is used for tail handling. pub last_process_status: AtomicCell, /// The current latency in samples, as set by the plugin through the [`InitContext`] and the @@ -296,7 +294,6 @@ impl WrapperInner

{ P::AUDIO_IO_LAYOUTS.first().copied().unwrap_or_default(), ), current_buffer_config: AtomicCell::new(None), - current_process_mode: AtomicCell::new(ProcessMode::Realtime), last_process_status: AtomicCell::new(ProcessStatus::Normal), current_latency: AtomicU32::new(0), // This is initialized just before calling `Plugin::initialize()` so that during the @@ -372,12 +369,17 @@ impl WrapperInner

{ } } - pub fn make_process_context(&self, transport: Transport) -> WrapperProcessContext<'_, P> { + pub fn make_process_context( + &self, + transport: Transport, + process_mode: ProcessMode, + ) -> WrapperProcessContext<'_, P> { WrapperProcessContext { inner: self, input_events_guard: self.input_events.borrow_mut(), output_events_guard: self.output_events.borrow_mut(), transport, + process_mode, } } diff --git a/src/wrapper/vst3/setup_processing_tests.rs b/src/wrapper/vst3/setup_processing_tests.rs new file mode 100644 index 000000000..e65c63fac --- /dev/null +++ b/src/wrapper/vst3/setup_processing_tests.rs @@ -0,0 +1,199 @@ +use crate::prelude::*; +use crate::wrapper::vst3::{vst3_sys, Wrapper}; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use vst3_sys::base::kResultOk; +use vst3_sys::vst::{ + AudioBusBuffers, IAudioProcessor, IComponent, ProcessData, ProcessModes, ProcessSetup, + SymbolicSampleSizes, +}; + +static SEEN_MODE: AtomicUsize = AtomicUsize::new(usize::MAX); +static SEEN_BLOCK: AtomicU32 = AtomicU32::new(0); +static SEEN_PROCESS_MODE: AtomicUsize = AtomicUsize::new(usize::MAX); +static TEST_LOCK: Mutex<()> = Mutex::new(()); + +#[derive(Default)] +struct ProbeParams {} + +// No parameter pointers are exposed by this observer. +unsafe impl Params for ProbeParams { + fn param_map(&self) -> Vec<(String, crate::params::internals::ParamPtr, String)> { + Vec::new() + } +} + +#[derive(Default)] +struct ModeProbe { + params: Arc, +} + +fn code(mode: ProcessMode) -> usize { + match mode { + ProcessMode::Realtime => 0, + ProcessMode::Buffered => 1, + ProcessMode::Offline => 2, + } +} + +impl Plugin for ModeProbe { + const NAME: &'static str = "Setup mode observer"; + const VENDOR: &'static str = "Intrect QA"; + const URL: &'static str = ""; + const EMAIL: &'static str = ""; + const VERSION: &'static str = "0.0.0"; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: std::num::NonZeroU32::new(2), + main_output_channels: std::num::NonZeroU32::new(2), + aux_input_ports: &[], + aux_output_ports: &[], + names: PortNames::const_default(), + }]; + type SysExMessage = (); + type BackgroundTask = (); + + fn params(&self) -> Arc { + self.params.clone() + } + + fn initialize( + &mut self, + _: &AudioIOLayout, + config: &BufferConfig, + _: &mut impl InitContext, + ) -> bool { + SEEN_MODE.store(code(config.process_mode), Ordering::SeqCst); + SEEN_BLOCK.store(config.max_buffer_size, Ordering::SeqCst); + true + } + + fn process( + &mut self, + _: &mut Buffer, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + SEEN_PROCESS_MODE.store( + context.host_render_mode().map(code).unwrap_or(usize::MAX), + Ordering::SeqCst, + ); + ProcessStatus::Normal + } +} + +impl Vst3Plugin for ModeProbe { + const VST3_CLASS_ID: [u8; 16] = *b"ModeSetupProbe01"; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[Vst3SubCategory::Fx]; +} + +#[test] +fn activation_receives_current_setup_mode_for_fresh_and_reused_wrappers() { + let _guard = TEST_LOCK.lock().unwrap(); + let modes = [ + ProcessModes::kRealtime as i32, + ProcessModes::kPrefetch as i32, + ProcessModes::kOffline as i32, + ]; + let mut mismatches = Vec::new(); + for seed in 0..3 { + for target in 0..3 { + let wrapper = Wrapper::::new(); + for (step, requested) in [seed, target].into_iter().enumerate() { + let block = 128 + step as u32 * 384; + let setup = ProcessSetup { + process_mode: modes[requested], + symbolic_sample_size: SymbolicSampleSizes::kSample32 as i32, + max_samples_per_block: block as i32, + sample_rate: 48000.0, + }; + SEEN_MODE.store(usize::MAX, Ordering::SeqCst); + unsafe { + assert_eq!( + IAudioProcessor::setup_processing(&*wrapper, &setup), + kResultOk + ); + assert_eq!(IComponent::set_active(&*wrapper, 1), kResultOk); + } + let actual = SEEN_MODE.load(Ordering::SeqCst); + assert_eq!(SEEN_BLOCK.load(Ordering::SeqCst), block); + println!( + "seed={seed} target={target} step={step} requested={requested} actual={actual}" + ); + if actual != requested { + mismatches.push((seed, target, step, requested, actual)); + } + unsafe { + assert_eq!(IComponent::set_active(&*wrapper, 0), kResultOk); + } + } + } + } + assert!( + mismatches.is_empty(), + "current setup mode was not delivered: {mismatches:?}" + ); +} + +#[test] +fn process_reports_realtime_prefetch_switches_without_new_setup() { + let _guard = TEST_LOCK.lock().unwrap(); + let wrapper = Wrapper::::new(); + let setup = ProcessSetup { + process_mode: ProcessModes::kRealtime as i32, + symbolic_sample_size: SymbolicSampleSizes::kSample32 as i32, + max_samples_per_block: 8, + sample_rate: 48000.0, + }; + unsafe { + assert_eq!( + IAudioProcessor::setup_processing(&*wrapper, &setup), + kResultOk + ); + assert_eq!(IComponent::set_active(&*wrapper, 1), kResultOk); + assert_eq!(IAudioProcessor::set_processing(&*wrapper, 1), kResultOk); + } + let mut input = [[0.25_f32; 8]; 2]; + let mut output = [[0.0_f32; 8]; 2]; + let mut input_ptrs = [input[0].as_mut_ptr().cast(), input[1].as_mut_ptr().cast()]; + let mut output_ptrs = [output[0].as_mut_ptr().cast(), output[1].as_mut_ptr().cast()]; + let mut input_bus = AudioBusBuffers { + num_channels: 2, + silence_flags: 0, + buffers: input_ptrs.as_mut_ptr(), + }; + let mut output_bus = AudioBusBuffers { + num_channels: 2, + silence_flags: 0, + buffers: output_ptrs.as_mut_ptr(), + }; + // ProcessData's interface/context fields are nullable SDK pointers. + let mut data: ProcessData = unsafe { std::mem::zeroed() }; + data.symbolic_sample_size = SymbolicSampleSizes::kSample32 as i32; + data.num_samples = 8; + data.num_inputs = 1; + data.num_outputs = 1; + data.inputs = &mut input_bus; + data.outputs = &mut output_bus; + let mut observed = Vec::new(); + for mode in [ + ProcessModes::kRealtime, + ProcessModes::kPrefetch, + ProcessModes::kRealtime, + ] { + data.process_mode = mode as i32; + SEEN_PROCESS_MODE.store(usize::MAX, Ordering::SeqCst); + unsafe { + assert_eq!(IAudioProcessor::process(&*wrapper, &mut data), kResultOk); + } + observed.push(SEEN_PROCESS_MODE.load(Ordering::SeqCst)); + } + unsafe { + assert_eq!(IAudioProcessor::set_processing(&*wrapper, 0), kResultOk); + assert_eq!(IComponent::set_active(&*wrapper, 0), kResultOk); + } + assert_eq!( + observed, + vec![0, 1, 0], + "callback's mode must not remain the setup mode" + ); +} diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index cab922cf9..3196b11bc 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -10,10 +10,10 @@ use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tr use vst3_sys::base::{IBStream, IPluginBase}; use vst3_sys::utils::SharedVstPtr; use vst3_sys::vst::{ - kNoParamId, kNoParentUnitId, kNoProgramListId, kRootUnitId, Event, EventTypes, IAudioProcessor, - IComponent, IEditController, IEventList, IMidiMapping, INoteExpressionController, - IParamValueQueue, IParameterChanges, IProcessContextRequirements, IUnitInfo, - AudioBusBuffers, LegacyMidiCCOutEvent, NoteExpressionTypeInfo, NoteExpressionValueDescription, + kNoParamId, kNoParentUnitId, kNoProgramListId, kRootUnitId, AudioBusBuffers, Event, EventTypes, + IAudioProcessor, IComponent, IEditController, IEventList, IMidiMapping, + INoteExpressionController, IParamValueQueue, IParameterChanges, IProcessContextRequirements, + IUnitInfo, LegacyMidiCCOutEvent, NoteExpressionTypeInfo, NoteExpressionValueDescription, NoteOffEvent, NoteOnEvent, ParameterFlags, PolyPressureEvent, ProgramListInfo, TChar, UnitInfo, }; use vst3_sys::VST3; @@ -55,6 +55,18 @@ impl Wrapper

{ pub fn new() -> Box { Self::allocate(WrapperInner::new()) } + + fn process_mode_from_host(mode: i32) -> ProcessMode { + match mode { + n if n == ProcessModes::kRealtime as i32 => ProcessMode::Realtime, + n if n == ProcessModes::kPrefetch as i32 => ProcessMode::Buffered, + n if n == ProcessModes::kOffline as i32 => ProcessMode::Offline, + n => { + nih_debug_assert_failure!("Unknown rendering mode '{}', defaulting to realtime", n); + ProcessMode::Realtime + } + } + } } impl Drop for Wrapper

{ @@ -870,25 +882,16 @@ impl IAudioProcessor for Wrapper

{ vst3_sys::vst::SymbolicSampleSizes::kSample32 as i32 ); - // This is needed when activating the plugin and when restoring state + let mode = Self::process_mode_from_host(setup.process_mode); + + // Activation must receive this setup's mode, including the first offline setup. self.inner.current_buffer_config.store(Some(BufferConfig { sample_rate: setup.sample_rate as f32, min_buffer_size: None, max_buffer_size: setup.max_samples_per_block as u32, - process_mode: self.inner.current_process_mode.load(), + process_mode: mode, })); - let mode = match setup.process_mode { - n if n == ProcessModes::kRealtime as i32 => ProcessMode::Realtime, - n if n == ProcessModes::kPrefetch as i32 => ProcessMode::Buffered, - n if n == ProcessModes::kOffline as i32 => ProcessMode::Offline, - n => { - nih_debug_assert_failure!("Unknown rendering mode '{}', defaulting to realtime", n); - ProcessMode::Realtime - } - }; - self.inner.current_process_mode.store(mode); - // Initializing the plugin happens in `IAudioProcessor::set_active()` because the host may // still change the channel layouts at this point @@ -938,6 +941,8 @@ impl IAudioProcessor for Wrapper

{ process_wrapper(|| { // We need to handle incoming automation first let data = &*data; + // Realtime/prefetch can switch without a new setupProcessing call. + let process_mode = Self::process_mode_from_host(data.process_mode); let sample_rate = self .inner .current_buffer_config @@ -1373,7 +1378,7 @@ impl IAudioProcessor for Wrapper

{ inputs: buffers.aux_inputs, outputs: buffers.aux_outputs, }; - let mut context = self.inner.make_process_context(transport); + let mut context = self.inner.make_process_context(transport, process_mode); let result = plugin.process(buffers.main_buffer, &mut aux, &mut context); self.inner.last_process_status.store(result); result