Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/wrapper/vst3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/wrapper/vst3/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -42,6 +42,7 @@ pub(crate) struct WrapperProcessContext<'a, P: Vst3Plugin> {
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<PluginNoteEvent<P>>>,
pub(super) output_events_guard: AtomicRefMut<'a, VecDeque<PluginNoteEvent<P>>>,
pub(super) transport: Transport,
pub(super) process_mode: ProcessMode,
}

/// A [`GuiContext`] implementation for the wrapper. This is passed to the plugin in
Expand Down Expand Up @@ -86,6 +87,10 @@ impl<P: Vst3Plugin> ProcessContext<P> for WrapperProcessContext<'_, P> {
PluginApi::Vst3
}

fn host_render_mode(&self) -> Option<ProcessMode> {
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...");
Expand Down
10 changes: 6 additions & 4 deletions src/wrapper/vst3/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `IAudioProcessor::setupProcessing()`.
pub current_buffer_config: AtomicCell<Option<BufferConfig>>,
/// The current audio processing mode. Set in `IAudioProcessor::setup_processing()`.
pub current_process_mode: AtomicCell<ProcessMode>,
/// The last process status returned by the plugin. This is used for tail handling.
pub last_process_status: AtomicCell<ProcessStatus>,
/// The current latency in samples, as set by the plugin through the [`InitContext`] and the
Expand Down Expand Up @@ -296,7 +294,6 @@ impl<P: Vst3Plugin> WrapperInner<P> {
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
Expand Down Expand Up @@ -372,12 +369,17 @@ impl<P: Vst3Plugin> WrapperInner<P> {
}
}

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,
}
}

Expand Down
199 changes: 199 additions & 0 deletions src/wrapper/vst3/setup_processing_tests.rs
Original file line number Diff line number Diff line change
@@ -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<ProbeParams>,
}

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<dyn Params> {
self.params.clone()
}

fn initialize(
&mut self,
_: &AudioIOLayout,
config: &BufferConfig,
_: &mut impl InitContext<Self>,
) -> 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<Self>,
) -> 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::<ModeProbe>::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::<ModeProbe>::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"
);
}
41 changes: 23 additions & 18 deletions src/wrapper/vst3/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,6 +55,18 @@ impl<P: Vst3Plugin> Wrapper<P> {
pub fn new() -> Box<Self> {
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<P: Vst3Plugin> Drop for Wrapper<P> {
Expand Down Expand Up @@ -870,25 +882,16 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
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

Expand Down Expand Up @@ -938,6 +941,8 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
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
Expand Down Expand Up @@ -1373,7 +1378,7 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
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
Expand Down
Loading