diff --git a/src/wrapper/au/context.rs b/src/wrapper/au/context.rs index f0ffb46f..137421e7 100644 --- a/src/wrapper/au/context.rs +++ b/src/wrapper/au/context.rs @@ -12,7 +12,7 @@ use crate::context::process::{ProcessContext, Transport}; use crate::context::PluginApi; use crate::params::internals::ParamPtr; use crate::plugin::Plugin; -use crate::prelude::PluginNoteEvent; +use crate::prelude::{PluginNoteEvent, ProcessMode}; use crate::wrapper::state::PluginState; /// Cell shared between the wrapper and its `InitContext` / `ProcessContext` @@ -56,6 +56,8 @@ impl InitContext

for AuInitContext

{ pub(super) struct AuProcessContext<'a, P: Plugin> { pub sink: Arc, pub transport: Transport, + /// `kAudioUnitProperty_OfflineRender` as of the start of this render block. + pub process_mode: ProcessMode, pub input_events: &'a mut VecDeque>, pub output_events: &'a mut VecDeque>, pub _marker: std::marker::PhantomData

, @@ -66,6 +68,10 @@ impl ProcessContext

for AuProcessContext<'_, P> { PluginApi::Au } + fn host_render_mode(&self) -> Option { + Some(self.process_mode) + } + fn execute_background(&self, _task: P::BackgroundTask) {} fn execute_gui(&self, _task: P::BackgroundTask) {} diff --git a/src/wrapper/au/wrapper.rs b/src/wrapper/au/wrapper.rs index 622aff0a..f56c3c0a 100644 --- a/src/wrapper/au/wrapper.rs +++ b/src/wrapper/au/wrapper.rs @@ -200,6 +200,13 @@ pub struct Wrapper { /// the toggle too. bypass: AtomicBool, + /// Host-controlled offline rendering (`kAudioUnitProperty_OfflineRender`). + /// Hosts may toggle it while initialized, for instance around a bounce, so + /// render reads it per block and reports it through + /// `ProcessContext::host_render_mode()`. `Initialize` also feeds it into + /// `BufferConfig::process_mode`. + offline_render: AtomicBool, + /// Index of the BYPASS-flagged param in `params_by_id`, if any. bypass_param_idx: Option, @@ -530,6 +537,7 @@ impl Wrapper

{ last_render_error: AtomicI32::new(au::noErr), initialized: AtomicBool::new(false), bypass: AtomicBool::new(false), + offline_render: AtomicBool::new(false), bypass_param_idx, processing_gate: crate::wrapper::util::processing_gate::ProcessingGate::new(), plugin: UnsafeCell::new(plugin), @@ -621,6 +629,14 @@ impl Wrapper

{ fn is_initialized(&self) -> bool { self.initialized.load(Ordering::Acquire) } + #[inline] + fn process_mode(&self) -> ProcessMode { + if self.offline_render.load(Ordering::Acquire) { + ProcessMode::Offline + } else { + ProcessMode::Realtime + } + } /// Mark `mask` of property bits as needing notification. Cheap atomic OR; /// callable from any thread. @@ -785,7 +801,7 @@ impl Wrapper

{ sample_rate: sr as f32, min_buffer_size: None, max_buffer_size: max_frames, - process_mode: ProcessMode::Realtime, + process_mode: this.process_mode(), }; let mut ctx = AuInitContext::

{ @@ -1006,7 +1022,7 @@ impl Wrapper

{ sample_rate: sr as f32, min_buffer_size: None, max_buffer_size: self.max_frames_per_slice(), - process_mode: ProcessMode::Realtime, + process_mode: self.process_mode(), }) } else { None @@ -1120,6 +1136,9 @@ impl Wrapper

{ au::kAudioUnitProperty_BypassEffect if scope == au::kAudioUnitScope_Global => { respond(std::mem::size_of::() as u32, true) } + au::kAudioUnitProperty_OfflineRender if scope == au::kAudioUnitScope_Global => { + respond(std::mem::size_of::() as u32, true) + } au::kAudioUnitProperty_LastRenderError if scope == au::kAudioUnitScope_Global => { respond(std::mem::size_of::() as u32, false) } @@ -1362,6 +1381,15 @@ impl Wrapper

{ } au::noErr } + au::kAudioUnitProperty_OfflineRender if scope == au::kAudioUnitScope_Global => { + require_output!(au::UInt32); + let on = u32::from(this.offline_render.load(Ordering::Acquire)); + unsafe { + *(out_data as *mut au::UInt32) = on; + *io_data_size = std::mem::size_of::() as u32; + } + au::noErr + } au::kAudioUnitProperty_LastRenderError if scope == au::kAudioUnitScope_Global => { require_output!(au::OSStatus); unsafe { @@ -1658,6 +1686,14 @@ impl Wrapper

{ } au::noErr } + // Unlike the stream configuration this may change while initialized: hosts + // flip it around a bounce without re-initializing the unit. + au::kAudioUnitProperty_OfflineRender if scope == au::kAudioUnitScope_Global => { + payload!(au::UInt32); + let v = unsafe { *(in_data as *const au::UInt32) }; + this.offline_render.store(v != 0, Ordering::Release); + au::noErr + } au::kAudioUnitProperty_MakeConnection if scope == au::kAudioUnitScope_Input => { payload!(AudioUnitConnection); // Only the main input is connectable; aux inputs keep using @@ -2531,6 +2567,7 @@ impl Wrapper

{ let mut process_ctx = AuProcessContext::

{ sink: this.sink.clone(), transport, + process_mode: this.process_mode(), input_events: &mut midi_state.input_events, output_events: &mut midi_state.output_events, _marker: PhantomData, @@ -3697,6 +3734,196 @@ mod tests { } } + /// Records the render mode the wrapper reports through both the + /// `initialize()` buffer config and the process context. + #[derive(Default)] + struct ModeProbe { + params: Arc, + initialized_with: Vec, + processed_with: Vec>, + } + + impl Plugin for ModeProbe { + const NAME: &'static str = "AU offline render probe"; + const VENDOR: &'static str = "NIH-plug"; + const URL: &'static str = "https://github.com/Intrect-io/nih-plug"; + const EMAIL: &'static str = "test@example.com"; + const VERSION: &'static str = "0.0.0"; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }]; + type SysExMessage = (); + type BackgroundTask = (); + fn params(&self) -> Arc { + self.params.clone() + } + fn initialize( + &mut self, + _: &AudioIOLayout, + buffer_config: &BufferConfig, + _: &mut impl InitContext, + ) -> bool { + self.initialized_with.push(buffer_config.process_mode); + true + } + fn process( + &mut self, + _: &mut Buffer, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + self.processed_with.push(context.host_render_mode()); + ProcessStatus::Normal + } + } + + impl AuPlugin for ModeProbe { + const AU_TYPE: [u8; 4] = *b"aufx"; + const AU_SUBTYPE: [u8; 4] = *b"TOfl"; + const AU_MANUFACTURER: [u8; 4] = *b"Test"; + } + + /// Hosts such as Logic set `kAudioUnitProperty_OfflineRender` for a bounce, + /// possibly while the unit is already initialized. Plugins that trade output + /// completeness for real-time safety rely on seeing that as `Offline`. + #[test] + fn offline_render_property_drives_reported_process_mode() { + type TestWrapper = Wrapper; + let wrapper = TestWrapper::new() as *mut c_void; + let set = |scope: au::AudioUnitScope, value: u32, size: usize| unsafe { + TestWrapper::set_property( + wrapper, + au::kAudioUnitProperty_OfflineRender, + scope, + 0, + &value as *const u32 as *const c_void, + size as u32, + ) + }; + let get = |scope: au::AudioUnitScope| { + let mut value = u32::MAX; + let mut size = mem::size_of::() as u32; + let status = unsafe { + TestWrapper::get_property( + wrapper, + au::kAudioUnitProperty_OfflineRender, + scope, + 0, + &mut value as *mut u32 as *mut c_void, + &mut size, + ) + }; + (status, value, size) + }; + let render = || { + let mut channels = [[0.25f32; 8]; 2]; + let mut storage = vec![0u64; bl_byte_size(2).div_ceil(mem::size_of::())]; + let list = storage.as_mut_ptr() as *mut au::AudioBufferList; + unsafe { + (*list).mNumberBuffers = 2; + for (i, samples) in channels.iter_mut().enumerate() { + *(*list).mBuffers.as_mut_ptr().add(i) = au::AudioBuffer { + mNumberChannels: 1, + mDataByteSize: 32, + mData: samples.as_mut_ptr() as *mut c_void, + }; + } + } + let mut time: au::AudioTimeStamp = unsafe { mem::zeroed() }; + time.mFlags = au::kAudioTimeStampSampleTimeValid; + let mut flags = 0; + assert_eq!( + unsafe { TestWrapper::render(wrapper, &mut flags, &time, 0, 8, list) }, + au::noErr + ); + }; + let probe = || unsafe { &*TestWrapper::from_ptr(wrapper).plugin.get() }; + + // Property info: a writable UInt32 on the global scope only. + let mut size = 0; + let mut writable = 0; + assert_eq!( + unsafe { + TestWrapper::get_property_info( + wrapper, + au::kAudioUnitProperty_OfflineRender, + au::kAudioUnitScope_Global, + 0, + &mut size, + &mut writable, + ) + }, + au::noErr + ); + assert_eq!(size, mem::size_of::() as u32); + assert_eq!(writable, 1); + for scope in [au::kAudioUnitScope_Input, au::kAudioUnitScope_Output] { + assert_eq!( + unsafe { + TestWrapper::get_property_info( + wrapper, + au::kAudioUnitProperty_OfflineRender, + scope, + 0, + &mut size, + &mut writable, + ) + }, + au::kAudioUnitErr_InvalidProperty + ); + assert_eq!(get(scope).0, au::kAudioUnitErr_InvalidProperty); + assert_eq!(set(scope, 1, 4), au::kAudioUnitErr_InvalidProperty); + } + + // Get/Set round trip, defaulting to real-time rendering. + assert_eq!(get(au::kAudioUnitScope_Global), (au::noErr, 0, 4)); + assert_eq!( + set(au::kAudioUnitScope_Global, 1, 2), + au::kAudioUnitErr_InvalidPropertyValue + ); + assert_eq!(get(au::kAudioUnitScope_Global), (au::noErr, 0, 4)); + + // Set before Initialize: `BufferConfig::process_mode` sees it. + assert_eq!(set(au::kAudioUnitScope_Global, 1, 4), au::noErr); + assert_eq!(get(au::kAudioUnitScope_Global), (au::noErr, 1, 4)); + assert_eq!(unsafe { TestWrapper::initialize(wrapper) }, au::noErr); + assert_eq!(probe().initialized_with, [ProcessMode::Offline]); + render(); + assert_eq!(probe().processed_with, [Some(ProcessMode::Offline)]); + + // Toggled between renders without re-initialising: the process + // context follows the property on the very next block. + assert_eq!(set(au::kAudioUnitScope_Global, 0, 4), au::noErr); + assert_eq!(get(au::kAudioUnitScope_Global), (au::noErr, 0, 4)); + render(); + assert_eq!(set(au::kAudioUnitScope_Global, 1, 4), au::noErr); + render(); + assert_eq!(set(au::kAudioUnitScope_Global, 0, 4), au::noErr); + render(); + assert_eq!( + probe().processed_with, + [ + Some(ProcessMode::Offline), + Some(ProcessMode::Realtime), + Some(ProcessMode::Offline), + Some(ProcessMode::Realtime), + ] + ); + assert_eq!(probe().initialized_with, [ProcessMode::Offline]); + + // A cleared property before re-initialising yields a real-time config. + assert_eq!(unsafe { TestWrapper::uninitialize(wrapper) }, au::noErr); + assert_eq!(unsafe { TestWrapper::initialize(wrapper) }, au::noErr); + assert_eq!( + probe().initialized_with, + [ProcessMode::Offline, ProcessMode::Realtime] + ); + + assert_eq!(unsafe { TestWrapper::close(wrapper) }, au::noErr); + } + #[derive(Default)] struct InstrumentParams; @@ -3830,6 +4057,10 @@ mod tests { au::kAudioUnitProperty_InPlaceProcessing, au::kAudioUnitScope_Global, ), + ( + au::kAudioUnitProperty_OfflineRender, + au::kAudioUnitScope_Global, + ), ]; let mut output = 0u64; diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index 6c347d57..53f6c7b4 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 75b5b503..88e9d356 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 c471b341..3fd08138 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 @@ -299,7 +297,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 @@ -377,12 +374,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 00000000..e65c63fa --- /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 fc0d5c9e..a50a29d2 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -90,6 +90,18 @@ impl Wrapper

{ }; } } + + 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

{ @@ -908,25 +920,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 @@ -988,6 +991,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 @@ -1423,7 +1428,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