diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 2ba9abc8..e2962b43 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -6,6 +6,29 @@ new and what's changed, this document lists all breaking changes in reverse chronological order. If a new feature did not require any changes to existing code then it will not be listed here. +## [2023-02-20] + +- The way audio IO layouts are configured has changed completely to align better + with NIH-plug's current and future supported plugin API backends. Rather than + defining a default layout and allowing the host/backend to change the channel + counts by polling the `Plugin::accepts_bus_config()` function, the plugin now + explicitly enumerates all supported audio IO layouts in a declarative fashion. + This change gives the plugin more options for defining alternative audio port + layouts including layouts with variable numbers of channels and ports, while + simultaneously removing ambiguities and behavior that was previously governed + by heuristics. + + All types surrounding bus layouts and port names have changed slightly to + accommodate this change. Take a look at the updated examples for more details + on how this works. The `Plugin::AUDIO_IO_LAYOUTS` field's documentation also + contains an example for how to initialize the layouts slice. + +- As a result of the above change, NIH-plug's standalones no longer have + `--input` and `--output` command line arguments to change the number of input + and output channels. Instead, they now have an `--audio-layout` option that + lets the user select an audio layout from the list of available layouts by + index. `--audio-layout=help` can be used to list those layouts. + ## [2023-02-01] - The `Vst3Plugin::VST3_CATEGORIES` string constant has been replaced by a diff --git a/README.md b/README.md index a526a52a..bcb61b55 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,8 @@ Scroll down for more information on the underlying plugin framework. - MIDI SysEx is also supported. Plugins can define their own structs or sum types to wrap around those messages so they don't need to interact with raw byte buffers in the process function. -- Support for flexible dynamic buffer configurations, including multiple input - and output busses. +- Support for flexible dynamic buffer configurations, including variable numbers + of input and output ports. - A plugin bundler accessible through the `cargo xtask bundle ` command that automatically detects which plugin targets your plugin exposes and creates the correct diff --git a/plugins/buffr_glitch/src/lib.rs b/plugins/buffr_glitch/src/lib.rs index bd9f3aac..c7276b7d 100644 --- a/plugins/buffr_glitch/src/lib.rs +++ b/plugins/buffr_glitch/src/lib.rs @@ -24,6 +24,8 @@ mod envelope; /// recording buffer's size. pub const MAX_OCTAVE_SHIFT: u32 = 2; +/// The number of channels supported by the plugin. We'll only do stereo for now. +const NUM_CHANNELS: u32 = 2; /// The maximum size of an audio block. We'll split up the audio in blocks and render smoothed /// values to buffers since these values may need to be reused for multiple voices. const MAX_BLOCK_SIZE: usize = 64; @@ -181,8 +183,12 @@ impl Plugin for BuffrGlitch { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + // We'll only do stereo for now + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(NUM_CHANNELS), + main_output_channels: NonZeroU32::new(NUM_CHANNELS), + ..AudioIOLayout::const_default() + }]; const MIDI_INPUT: MidiConfig = MidiConfig::Basic; @@ -193,23 +199,21 @@ impl Plugin for BuffrGlitch { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // We'll only do stereo for now - config.num_input_channels == config.num_output_channels && config.num_input_channels == 2 - } - fn initialize( &mut self, - bus_config: &BusConfig, + audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { + let num_output_channels = audio_io_layout + .main_output_channels + .expect("Plugin does not have a main output") + .get() as usize; self.sample_rate = buffer_config.sample_rate; for voice in &mut self.voices { - voice.buffer.resize( - bus_config.num_input_channels as usize, - buffer_config.sample_rate, - ); + voice + .buffer + .resize(num_output_channels, buffer_config.sample_rate); } true @@ -443,6 +447,7 @@ impl Vst3Plugin for BuffrGlitch { const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ Vst3SubCategory::Fx, Vst3SubCategory::Synth, + Vst3SubCategory::Stereo, Vst3SubCategory::Custom("Glitch"), ]; } diff --git a/plugins/crisp/src/lib.rs b/plugins/crisp/src/lib.rs index 8f4b51ea..668b5060 100644 --- a/plugins/crisp/src/lib.rs +++ b/plugins/crisp/src/lib.rs @@ -301,8 +301,12 @@ impl Plugin for Crisp { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = NUM_CHANNELS; - const DEFAULT_OUTPUT_CHANNELS: u32 = NUM_CHANNELS; + // We'll add a SIMD version in a bit which only supports stereo + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(NUM_CHANNELS), + main_output_channels: NonZeroU32::new(NUM_CHANNELS), + ..AudioIOLayout::const_default() + }]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -317,20 +321,12 @@ impl Plugin for Crisp { editor::create(self.params.clone(), self.params.editor_state.clone()) } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // We'll add a SIMD version in a bit which only supports stereo - config.num_input_channels == config.num_output_channels - && config.num_input_channels == NUM_CHANNELS - } - fn initialize( &mut self, - bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { - nih_debug_assert_eq!(bus_config.num_input_channels, NUM_CHANNELS); - nih_debug_assert_eq!(bus_config.num_output_channels, NUM_CHANNELS); self.sample_rate = buffer_config.sample_rate; // The filter coefficients need to be reinitialized when loading a patch @@ -500,7 +496,6 @@ impl ClapPlugin for Crisp { const CLAP_FEATURES: &'static [ClapFeature] = &[ ClapFeature::AudioEffect, ClapFeature::Stereo, - ClapFeature::Mono, ClapFeature::Distortion, ]; } @@ -511,6 +506,7 @@ impl Vst3Plugin for Crisp { Vst3SubCategory::Fx, Vst3SubCategory::Filter, Vst3SubCategory::Distortion, + Vst3SubCategory::Stereo, ]; } diff --git a/plugins/crossover/src/lib.rs b/plugins/crossover/src/lib.rs index 670946cd..8f5b52e4 100644 --- a/plugins/crossover/src/lib.rs +++ b/plugins/crossover/src/lib.rs @@ -166,22 +166,24 @@ impl Plugin for Crossover { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = NUM_CHANNELS; - const DEFAULT_OUTPUT_CHANNELS: u32 = NUM_CHANNELS; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(NUM_CHANNELS), + main_output_channels: NonZeroU32::new(NUM_CHANNELS), - const DEFAULT_AUX_OUTPUTS: Option = Some(AuxiliaryIOConfig { - // Two to five of these busses will be used at a time - num_busses: 5, - num_channels: NUM_CHANNELS, - }); + aux_input_ports: &[], + // Two to five of these ports will be used at a time + aux_output_ports: &[new_nonzero_u32(NUM_CHANNELS); 5], - const PORT_NAMES: PortNames = PortNames { - main_input: None, - // We won't output any sound here - main_output: Some("The Void"), - aux_inputs: None, - aux_outputs: Some(&["Band 1", "Band 2", "Band 3", "Band 4", "Band 5"]), - }; + names: PortNames { + layout: Some("Up to five bands"), + + main_input: None, + // We won't output any sound here + main_output: Some("The Void"), + aux_inputs: &[], + aux_outputs: &["Band 1", "Band 2", "Band 3", "Band 4", "Band 5"], + }, + }]; type SysExMessage = (); type BackgroundTask = (); @@ -190,16 +192,9 @@ impl Plugin for Crossover { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // Only do stereo - config.num_input_channels == NUM_CHANNELS - && config.num_output_channels == NUM_CHANNELS - && config.aux_output_busses.num_channels == NUM_CHANNELS - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, context: &mut impl InitContext, ) -> bool { diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 041c16bb..dbdc1047 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -34,6 +34,8 @@ mod filter; mod params; mod spectrum; +/// The number of channels we support. Hardcoded to simplify the SIMD version. +const NUM_CHANNELS: u32 = 2; /// The maximum number of samples to iterate over at a time. const MAX_BLOCK_SIZE: usize = 64; @@ -81,8 +83,7 @@ impl Default for Diopser { let bypass_smoother = Arc::new(Smoother::new(SmoothingStyle::Linear(10.0))); // We only do stereo right now so this is simple - let (spectrum_input, spectrum_output) = - SpectrumInput::new(Self::DEFAULT_OUTPUT_CHANNELS as usize); + let (spectrum_input, spectrum_output) = SpectrumInput::new(NUM_CHANNELS as usize); Self { params: Arc::new(DiopserParams::new( @@ -113,8 +114,12 @@ impl Plugin for Diopser { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + // The SIMD version only supports stereo + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(NUM_CHANNELS), + main_output_channels: NonZeroU32::new(NUM_CHANNELS), + ..AudioIOLayout::const_default() + }]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -151,14 +156,9 @@ impl Plugin for Diopser { } } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // The SIMD version only supports stereo - config.num_input_channels == config.num_output_channels && config.num_input_channels == 2 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { @@ -355,8 +355,11 @@ impl ClapPlugin for Diopser { impl Vst3Plugin for Diopser { const VST3_CLASS_ID: [u8; 16] = *b"DiopserPlugRvdH."; - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Fx, Vst3SubCategory::Filter]; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Fx, + Vst3SubCategory::Filter, + Vst3SubCategory::Stereo, + ]; } nih_export_clap!(Diopser); diff --git a/plugins/examples/gain/src/lib.rs b/plugins/examples/gain/src/lib.rs index b53403a5..726c27a4 100644 --- a/plugins/examples/gain/src/lib.rs +++ b/plugins/examples/gain/src/lib.rs @@ -120,11 +120,27 @@ impl Plugin for Gain { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + // The first audio IO layout is used as the default. The other layouts may be selected either + // explicitly or automatically by the host or the user depending on the plugin API/backend. + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), - const DEFAULT_AUX_INPUTS: Option = None; - const DEFAULT_AUX_OUTPUTS: Option = None; + aux_input_ports: &[], + aux_output_ports: &[], + + // Individual ports and the layout as a whole can be named here. By default these names + // are generated as needed. This layout will be called 'Stereo', while the other one is + // given the name 'Mono' based no the number of input and output channels. + names: PortNames::const_default(), + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; const MIDI_INPUT: MidiConfig = MidiConfig::None; // Setting this to `true` will tell the wrapper to split the buffer up into smaller blocks @@ -147,11 +163,6 @@ impl Plugin for Gain { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // This works with any symmetrical IO layout - config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 - } - // This plugin doesn't need any special initialization, but if you need to do anything expensive // then this would be the place. State is kept around when the host reconfigures the // plugin. If we do need special initialization, we could implement the `initialize()` and/or diff --git a/plugins/examples/gain_gui_egui/src/lib.rs b/plugins/examples/gain_gui_egui/src/lib.rs index 99e91bcb..d7687bd0 100644 --- a/plugins/examples/gain_gui_egui/src/lib.rs +++ b/plugins/examples/gain_gui_egui/src/lib.rs @@ -78,8 +78,18 @@ impl Plugin for Gain { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -153,14 +163,9 @@ impl Plugin for Gain { ) } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // This works with any symmetrical IO layout - config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { diff --git a/plugins/examples/gain_gui_iced/src/lib.rs b/plugins/examples/gain_gui_iced/src/lib.rs index 63de5197..53ae1e23 100644 --- a/plugins/examples/gain_gui_iced/src/lib.rs +++ b/plugins/examples/gain_gui_iced/src/lib.rs @@ -75,8 +75,18 @@ impl Plugin for Gain { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -95,14 +105,9 @@ impl Plugin for Gain { ) } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // This works with any symmetrical IO layout - config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { diff --git a/plugins/examples/gain_gui_vizia/src/lib.rs b/plugins/examples/gain_gui_vizia/src/lib.rs index 0e1cfc3e..b9353c80 100644 --- a/plugins/examples/gain_gui_vizia/src/lib.rs +++ b/plugins/examples/gain_gui_vizia/src/lib.rs @@ -74,8 +74,18 @@ impl Plugin for Gain { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -94,14 +104,9 @@ impl Plugin for Gain { ) } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // This works with any symmetrical IO layout - config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { diff --git a/plugins/examples/midi_inverter/src/lib.rs b/plugins/examples/midi_inverter/src/lib.rs index 271494d2..4a9ddb84 100644 --- a/plugins/examples/midi_inverter/src/lib.rs +++ b/plugins/examples/midi_inverter/src/lib.rs @@ -26,8 +26,8 @@ impl Plugin for MidiInverter { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 0; - const DEFAULT_OUTPUT_CHANNELS: u32 = 0; + // This plugin doesn't have any audio IO + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[]; const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs; const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs; diff --git a/plugins/examples/poly_mod_synth/src/lib.rs b/plugins/examples/poly_mod_synth/src/lib.rs index 5527ae1c..375dbb4e 100644 --- a/plugins/examples/poly_mod_synth/src/lib.rs +++ b/plugins/examples/poly_mod_synth/src/lib.rs @@ -148,8 +148,11 @@ impl Plugin for PolyModSynth { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }]; // We won't need any MIDI CCs here, we just want notes and polyphonic modulation const MIDI_INPUT: MidiConfig = MidiConfig::Basic; @@ -612,8 +615,11 @@ impl ClapPlugin for PolyModSynth { // modulation impl Vst3Plugin for PolyModSynth { const VST3_CLASS_ID: [u8; 16] = *b"PolyM0dSynth1337"; - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Instrument, Vst3SubCategory::Synth]; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Instrument, + Vst3SubCategory::Synth, + Vst3SubCategory::Stereo, + ]; } nih_export_clap!(PolyModSynth); diff --git a/plugins/examples/sine/src/lib.rs b/plugins/examples/sine/src/lib.rs index 4d2d4597..321558f7 100644 --- a/plugins/examples/sine/src/lib.rs +++ b/plugins/examples/sine/src/lib.rs @@ -105,8 +105,19 @@ impl Plugin for Sine { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 0; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + // This is also the default and can be omitted here + main_input_channels: None, + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: None, + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; const MIDI_INPUT: MidiConfig = MidiConfig::Basic; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -118,14 +129,9 @@ impl Plugin for Sine { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // This can output to any number of channels, but it doesn't take any audio inputs - config.num_input_channels == 0 && config.num_output_channels > 0 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { diff --git a/plugins/examples/stft/src/lib.rs b/plugins/examples/stft/src/lib.rs index cf106817..5aac42c5 100644 --- a/plugins/examples/stft/src/lib.rs +++ b/plugins/examples/stft/src/lib.rs @@ -90,8 +90,12 @@ impl Plugin for Stft { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + // We'll only do stereo for simplicity's sake + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -102,14 +106,9 @@ impl Plugin for Stft { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // We'll only do stereo for simplicity's sake - config.num_input_channels == config.num_output_channels && config.num_input_channels == 2 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, _buffer_config: &BufferConfig, context: &mut impl InitContext, ) -> bool { @@ -172,15 +171,17 @@ impl ClapPlugin for Stft { const CLAP_FEATURES: &'static [ClapFeature] = &[ ClapFeature::AudioEffect, ClapFeature::Stereo, - ClapFeature::Mono, ClapFeature::Utility, ]; } impl Vst3Plugin for Stft { const VST3_CLASS_ID: [u8; 16] = *b"StftMoistestPlug"; - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Fx, Vst3SubCategory::Tools]; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Fx, + Vst3SubCategory::Tools, + Vst3SubCategory::Stereo, + ]; } nih_export_clap!(Stft); diff --git a/plugins/examples/sysex/src/lib.rs b/plugins/examples/sysex/src/lib.rs index 8aa7aff3..975ca2fa 100644 --- a/plugins/examples/sysex/src/lib.rs +++ b/plugins/examples/sysex/src/lib.rs @@ -51,8 +51,8 @@ impl Plugin for SysEx { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 0; - const DEFAULT_OUTPUT_CHANNELS: u32 = 0; + // This plugin doesn't have any audio IO + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; diff --git a/plugins/loudness_war_winner/src/lib.rs b/plugins/loudness_war_winner/src/lib.rs index f20d01f9..124c6a70 100644 --- a/plugins/loudness_war_winner/src/lib.rs +++ b/plugins/loudness_war_winner/src/lib.rs @@ -120,8 +120,18 @@ impl Plugin for LoudnessWarWinner { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; type SysExMessage = (); type BackgroundTask = (); @@ -130,22 +140,20 @@ impl Plugin for LoudnessWarWinner { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 - } - fn initialize( &mut self, - bus_config: &BusConfig, + audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { self.sample_rate = buffer_config.sample_rate; - self.bp_filters.resize( - bus_config.num_output_channels as usize, - [filter::Biquad::default(); 4], - ); + let num_output_channels = audio_io_layout + .main_output_channels + .expect("Plugin does not have a main output") + .get() as usize; + self.bp_filters + .resize(num_output_channels, [filter::Biquad::default(); 4]); self.update_bp_filters(); self.silence_fadeout_start_samples = diff --git a/plugins/puberty_simulator/src/lib.rs b/plugins/puberty_simulator/src/lib.rs index a049f022..a68930d4 100644 --- a/plugins/puberty_simulator/src/lib.rs +++ b/plugins/puberty_simulator/src/lib.rs @@ -165,8 +165,12 @@ impl Plugin for PubertySimulator { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + // We'll only do stereo for simplicity's sake + 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 = (); @@ -175,14 +179,9 @@ impl Plugin for PubertySimulator { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // We'll only do stereo for simplicity's sake - config.num_input_channels == config.num_output_channels && config.num_input_channels == 2 - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, _buffer_config: &BufferConfig, context: &mut impl InitContext, ) -> bool { @@ -431,8 +430,11 @@ impl ClapPlugin for PubertySimulator { impl Vst3Plugin for PubertySimulator { const VST3_CLASS_ID: [u8; 16] = *b"PubertySim..RvdH"; - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Fx, Vst3SubCategory::PitchShift]; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Fx, + Vst3SubCategory::PitchShift, + Vst3SubCategory::Stereo, + ]; } nih_export_clap!(PubertySimulator); diff --git a/plugins/safety_limiter/src/lib.rs b/plugins/safety_limiter/src/lib.rs index 0c949753..144b42b1 100644 --- a/plugins/safety_limiter/src/lib.rs +++ b/plugins/safety_limiter/src/lib.rs @@ -155,8 +155,18 @@ impl Plugin for SafetyLimiter { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; type SysExMessage = (); type BackgroundTask = (); @@ -165,13 +175,9 @@ impl Plugin for SafetyLimiter { self.params.clone() } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - config.num_input_channels == config.num_output_channels - } - fn initialize( &mut self, - _bus_config: &BusConfig, + _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { diff --git a/plugins/spectral_compressor/src/lib.rs b/plugins/spectral_compressor/src/lib.rs index d0fbf87e..e1cef6ce 100644 --- a/plugins/spectral_compressor/src/lib.rs +++ b/plugins/spectral_compressor/src/lib.rs @@ -132,10 +132,7 @@ impl Default for SpectralCompressor { fn default() -> Self { // Changing any of the compressor threshold or ratio parameters will set an atomic flag in // this object that causes the compressor thresholds and ratios to be recalcualted - let compressor_bank = compressor_bank::CompressorBank::new( - Self::DEFAULT_OUTPUT_CHANNELS as usize, - MAX_WINDOW_SIZE, - ); + let compressor_bank = compressor_bank::CompressorBank::new(2, MAX_WINDOW_SIZE); SpectralCompressor { params: Arc::new(SpectralCompressorParams::new(&compressor_bank)), @@ -149,7 +146,7 @@ impl Default for SpectralCompressor { }, // These three will be set to the correct values in the initialize function - stft: util::StftHelper::new(Self::DEFAULT_OUTPUT_CHANNELS as usize, MAX_WINDOW_SIZE, 0), + stft: util::StftHelper::new(2, MAX_WINDOW_SIZE, 0), window_function: Vec::with_capacity(MAX_WINDOW_SIZE), dry_wet_mixer: dry_wet_mixer::DryWetMixer::new(0, 0, 0), compressor_bank, @@ -256,12 +253,24 @@ impl Plugin for SpectralCompressor { const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - const DEFAULT_INPUT_CHANNELS: u32 = 2; - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; - const DEFAULT_AUX_INPUTS: Option = Some(AuxiliaryIOConfig { - num_busses: 1, - num_channels: 2, - }); + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + + aux_input_ports: &[new_nonzero_u32(2)], + + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + + aux_input_ports: &[new_nonzero_u32(1)], + + ..AudioIOLayout::const_default() + }, + ]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -276,35 +285,31 @@ impl Plugin for SpectralCompressor { editor::create(self.params.clone(), self.editor_state.clone()) } - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // We can support any channel layout as long as the number of channels is consistent - config.num_input_channels == config.num_output_channels - && config.num_input_channels > 0 - && config.aux_input_busses.num_busses == 1 - && config.aux_input_busses.num_channels == config.num_input_channels - } - fn initialize( &mut self, - bus_config: &BusConfig, + audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, context: &mut impl InitContext, ) -> bool { // Needed to update the compressors later self.buffer_config = *buffer_config; - // This plugin can accept any number of channels, so we need to resize channel-dependent - // data structures accordinly - if self.stft.num_channels() != bus_config.num_output_channels as usize { + // This plugin can accept a variable number of audio channels, so we need to resize + // channel-dependent data structures accordingly + let num_output_channels = audio_io_layout + .main_output_channels + .expect("Plugin does not have a main output") + .get() as usize; + if self.stft.num_channels() != num_output_channels { self.stft = util::StftHelper::new(self.stft.num_channels(), MAX_WINDOW_SIZE, 0); } self.dry_wet_mixer.resize( - bus_config.num_output_channels as usize, + num_output_channels, buffer_config.max_buffer_size as usize, MAX_WINDOW_SIZE, ); self.compressor_bank - .update_capacity(bus_config.num_output_channels as usize, MAX_WINDOW_SIZE); + .update_capacity(num_output_channels, MAX_WINDOW_SIZE); // Planning with RustFFT is very fast, but it will still allocate we we'll plan all of the // FFTs we might need in advance @@ -572,6 +577,7 @@ impl ClapPlugin for SpectralCompressor { const CLAP_FEATURES: &'static [ClapFeature] = &[ ClapFeature::AudioEffect, ClapFeature::Stereo, + ClapFeature::Mono, ClapFeature::PhaseVocoder, ClapFeature::Compressor, ClapFeature::Custom("nih:spectral"), diff --git a/src/audio_setup.rs b/src/audio_setup.rs index 4c8062c3..1c271aa4 100644 --- a/src/audio_setup.rs +++ b/src/audio_setup.rs @@ -1,55 +1,77 @@ -//! Types and definitions surrounding a plugin's audio bus setup. +//! Types and definitions surrounding a plugin's audio IO setup. + +use std::num::NonZeroU32; use crate::buffer::Buffer; -/// The plugin's IO configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BusConfig { - /// The number of input channels for the plugin. - pub num_input_channels: u32, - /// The number of output channels for the plugin. - pub num_output_channels: u32, - /// Any additional sidechain inputs. - pub aux_input_busses: AuxiliaryIOConfig, - /// Any additional outputs. - pub aux_output_busses: AuxiliaryIOConfig, +/// A description of a plugin's audio IO configuration. The [`Plugin`][crate::prelude::Plugin] +/// defines a list of supported audio IO configs, with the first one acting as the default layout. +/// Depending on the plugin API, the host may pick a different configuration from the list and use +/// that instead. The final chosen configuration is passed as an argument to the +/// [`Plugin::initialize()`][crate::prelude::Plugin::initialize] function so the plugin can allocate +/// its data structures based on the number of audio channels it needs to process. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AudioIOLayout { + /// The number of main input channels for the plugin, if it has a main input port. This can be + /// set to `None` if the plugin does not have one. + pub main_input_channels: Option, + /// The number of main output channels for the plugin, if it has a main output port. This can be + /// set to `None` if the plugin does not have one. + pub main_output_channels: Option, + /// The plugin's additional sidechain inputs, if it has any. Use the [`new_nonzero_u32()`] + /// function to construct these values until const `Option::unwrap()` gets stabilized + /// (). + pub aux_input_ports: &'static [NonZeroU32], + /// The plugin's additional outputs, if it has any. Use the [`new_nonzero_u32()`] function to + /// construct these values until const `Option::unwrap()` gets stabilized + /// (). + pub aux_output_ports: &'static [NonZeroU32], + + /// Optional names for the audio ports. Defining these can be useful for plugins with multiple + /// output and input ports. + pub names: PortNames, } -/// Configuration for auxiliary inputs or outputs on [`BusConfig`]. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct AuxiliaryIOConfig { - /// The number of auxiliary input or output busses. - pub num_busses: u32, - /// The number of channels in each bus. - pub num_channels: u32, +/// Construct a `NonZeroU32` value at compile time. Equivalent to `NonZeroU32::new(n).unwrap()`. +pub const fn new_nonzero_u32(n: u32) -> NonZeroU32 { + match NonZeroU32::new(n) { + Some(n) => n, + None => panic!("'new_nonzero_u32()' called with a zero value"), + } } /// Contains auxiliary (sidechain) input and output buffers for a process call. pub struct AuxiliaryBuffers<'a> { - /// All auxiliary (sidechain) inputs defined for this plugin. The data in these buffers can - /// safely be overwritten. Auxiliary inputs can be defined by setting - /// [`Plugin::DEFAULT_AUX_INPUTS`][`crate::prelude::Plugin::DEFAULT_AUX_INPUTS`]. + /// Buffers for all auxiliary (sidechain) inputs defined for this plugin. The data in these + /// buffers can safely be overwritten. Auxiliary inputs can be defined using the + /// [`AudioIOLayout::aux_input_ports`] field. pub inputs: &'a mut [Buffer<'a>], - /// Get all auxiliary outputs defined for this plugin. Auxiliary outputs can be defined by - /// setting [`Plugin::DEFAULT_AUX_OUTPUTS`][`crate::prelude::Plugin::DEFAULT_AUX_OUTPUTS`]. + /// Buffers for all auxiliary outputs defined for this plugin. Auxiliary outputs can be defined using the + /// [`AudioIOLayout::aux_output_ports`] field. pub outputs: &'a mut [Buffer<'a>], } -/// Contains names for the main input and output ports as well as for all of the auxiliary input and -/// output ports. Setting these is optional, but it makes working with multi-output plugins much -/// more convenient. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +/// Contains names for the ports defined in an `AudioIOLayout`. Setting these is optional, but it +/// makes working with multi-output plugins much more convenient. +/// +/// All of these names should start with a capital letter to be consistent with automatically +/// generated names. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct PortNames { + /// The name for the audio IO layout as a whole. Useful when a plugin has multiple distinct + /// layouts. Will be generated if not set. + pub layout: Option<&'static str>, + /// The name for the main input port. Will be generated if not set. pub main_input: Option<&'static str>, /// The name for the main output port. Will be generated if not set. pub main_output: Option<&'static str>, /// Names for auxiliary (sidechain) input ports. Will be generated if not set or if this slice /// does not contain enough names. - pub aux_inputs: Option<&'static [&'static str]>, + pub aux_inputs: &'static [&'static str], /// Names for auxiliary output ports. Will be generated if not set or if this slice does not /// contain enough names. - pub aux_outputs: Option<&'static [&'static str]>, + pub aux_outputs: &'static [&'static str], } /// Configuration for (the host's) audio buffers. @@ -82,3 +104,101 @@ pub enum ProcessMode { /// processed. Offline, } + +impl AudioIOLayout { + /// [`AudioIOLayout::default()`], but as a const function. Used when initializing + /// `Plugin::AUDIO_IO_LAYOUTS`. () + pub const fn const_default() -> Self { + Self { + main_input_channels: None, + main_output_channels: None, + aux_input_ports: &[], + aux_output_ports: &[], + names: PortNames::const_default(), + } + } + + /// A descriptive name for the layout. This is taken from `PortNames::layout` if set. Otherwise + /// it is generated based on the layout. + pub fn name(&self) -> String { + if let Some(name) = self.names.layout { + return name.to_owned(); + } + + // If the name is not set then we'll try to come up with something descriptive + match ( + self.main_input_channels + .map(NonZeroU32::get) + .unwrap_or_default(), + self.main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default(), + self.aux_input_ports.len(), + self.aux_output_ports.len(), + ) { + (0, 0, 0, 0) => String::from("Empty"), + (_, 1, 0, _) | (1, 0, _, _) => String::from("Mono"), + (_, 2, 0, _) | (2, 0, _, _) => String::from("Stereo"), + (_, 1, _, _) => String::from("Mono with sidechain"), + (_, 2, _, _) => String::from("Stereo with sidechain"), + // These probably, hopefully won't occur + (i, o, 0, 0) => format!("{i} inputs, {o} outputs"), + (i, o, _, 0) => format!("{i} inputs, {o} outputs, with sidechain"), + // And these don't make much sense, suggestions for something better are welcome + (i, o, 0, aux_o) => format!("{i} inputs, {o}*{} outputs", aux_o + 1), + (i, o, aux_i, aux_o) => format!("{i}*{} inputs, {o}*{} outputs", aux_i + 1, aux_o + 1), + } + } + + /// The name for the main input port. Either generated or taken from the `names` field. + pub fn main_input_name(&self) -> String { + self.names.main_input.unwrap_or("Input").to_owned() + } + + /// The name for the main output port. Either generated or taken from the `names` field. + pub fn main_output_name(&self) -> String { + self.names.main_input.unwrap_or("Output").to_owned() + } + + /// The name for the auxiliary input port with the given index. Either generated or taken from + /// the `names` field. + pub fn aux_input_name(&self, idx: usize) -> Option { + if idx >= self.aux_input_ports.len() { + None + } else { + match self.names.aux_inputs.get(idx) { + Some(name) => Some(String::from(*name)), + None if self.aux_input_ports.len() == 1 => Some(String::from("Sidechain Input")), + None => Some(format!("Sidechain Input {}", idx + 1)), + } + } + } + + /// The name for the auxiliary output port with the given index. Either generated or taken from + /// the `names` field. + pub fn aux_output_name(&self, idx: usize) -> Option { + if idx >= self.aux_output_ports.len() { + None + } else { + match self.names.aux_outputs.get(idx) { + Some(name) => Some(String::from(*name)), + None if self.aux_output_ports.len() == 1 => Some(String::from("Auxiliary Output")), + None => Some(format!("Auxiliary Output {}", idx + 1)), + } + } + } +} + +impl PortNames { + /// [`PortNames::default()`], but as a const function. Used when initializing + /// `Plugin::AUDIO_IO_LAYOUTS`. () + pub const fn const_default() -> Self { + Self { + layout: None, + main_input: None, + main_output: None, + aux_inputs: &[], + aux_outputs: &[], + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b44ab9c6..7244eca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,11 +36,10 @@ //! 1. When the host loads the plugin, your plugin object will be instantiated using its //! [`Default`] implementation. The plugin should refrain from performing expensive //! calculations or IO at this point. -//! 2. The host or the plugin wrapper will call -//! [`Plugin::accepts_bus_config()`][prelude::Plugin::accepts_bus_config()] several times with -//! different IO configurations to poll whether your plugin supports certain IO configurations. -//! The plugin should not do any work at this point and just reply with boolean whether it -//! supports the configuration or not. +//! 2. The host will select an audio IO layout from +//! [`Plugin::AUDIO_IO_LAYOUTS`][prelude::Plugin::AUDIO_IO_LAYOUTS]. The first layout is always +//! used as the default one, and should reflect the plugin's most commonly used configuration. +//! Usually this is a stereo layout. //! 3. After that, [`Plugin::initialize()`][prelude::Plugin::initialize()] will be called with the //! the selected IO configuration and the audio buffer settings. Here you should allocate any //! data structures you need or precompute data that depends on the sample rate or maximum diff --git a/src/plugin.rs b/src/plugin.rs index d454404c..6e9d0966 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use crate::audio_setup::{AuxiliaryBuffers, AuxiliaryIOConfig, BufferConfig, BusConfig, PortNames}; +use crate::audio_setup::{AudioIOLayout, AuxiliaryBuffers, BufferConfig}; use crate::buffer::Buffer; use crate::context::gui::AsyncExecutor; use crate::context::init::InitContext; @@ -48,38 +48,33 @@ pub trait Plugin: Default + Send + 'static { /// but just in case they do this should only contain decimals values and dots. const VERSION: &'static str; - /// The default number of input channels. This merely serves as a default. The host will probe - /// the plugin's supported configuration using - /// [`accepts_bus_config()`][Self::accepts_bus_config()], and the selected configuration is - /// passed to [`initialize()`][Self::initialize()]. Some hosts like, like Bitwig and Ardour, use - /// the defaults instead of setting up the busses properly. + /// The plugin's supported audio IO layouts. The first config will be used as the default config + /// if the host doesn't or can't select an alternative configuration. Because of that it's + /// recommended to begin this slice with a stereo layout. For maximum compatibility with the + /// different plugin formats this default layout should also include all of the plugin's + /// auxiliary input and output ports, if the plugin has any. If the slice is empty, then the + /// plugin will not have any audio IO. /// - /// Setting this to zero causes the plugin to have no main input bus. - const DEFAULT_INPUT_CHANNELS: u32 = 2; - /// The default number of output channels. All of the same caveats mentioned for - /// `DEFAULT_INPUT_CHANNELS` apply here. + /// Both [`AudioIOLayout`] and [`PortNames`][crate::prelude::PortNames] have `.const_default()` + /// functions for compile-time equivalents to `Default::default()`: /// - /// Setting this to zero causes the plugin to have no main output bus. - const DEFAULT_OUTPUT_CHANNELS: u32 = 2; - - /// If set, then the plugin will have this many sidechain input busses with a default number of - /// channels. Not all hosts support more than one sidechain input bus. Negotiating the actual - /// configuration works the same was as with `DEFAULT_INPUT_CHANNELS`. - const DEFAULT_AUX_INPUTS: Option = None; - /// If set, then the plugin will have this many auxiliary output busses with a default number of - /// channels. Negotiating the actual configuration works the same was as with - /// `DEFAULT_INPUT_CHANNELS`. - const DEFAULT_AUX_OUTPUTS: Option = None; - - /// Optional names for the main and auxiliary input and output ports. Will be generated if not - /// set. This is mostly useful to give descriptive names to the outputs for multi-output - /// plugins. - const PORT_NAMES: PortNames = PortNames { - main_input: None, - main_output: None, - aux_inputs: None, - aux_outputs: None, - }; + /// ``` + /// # use nih_plug::prelude::*; + /// const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + /// main_input_channels: NonZeroU32::new(2), + /// main_output_channels: NonZeroU32::new(2), + /// + /// aux_input_ports: &[new_nonzero_u32(2)], + /// + /// ..AudioIOLayout::const_default() + /// }]; + /// ``` + /// + /// # Note + /// + /// Some plugin hosts, like Ableton Live, don't support MIDI-only plugins and may refuse to load + /// plugins with no main output or with zero main output channels. + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout]; /// Whether the plugin accepts note events, and what which events it wants to receive. If this /// is set to [`MidiConfig::None`], then the plugin won't receive any note events. @@ -152,30 +147,28 @@ pub trait Plugin: Default + Send + 'static { // The following functions follow the lifetime of the plugin. // - /// Whether the plugin supports a bus config. This only acts as a check, and the plugin - /// shouldn't do anything beyond returning true or false. - fn accepts_bus_config(&self, config: &BusConfig) -> bool { - config.num_input_channels == Self::DEFAULT_INPUT_CHANNELS - && config.num_output_channels == Self::DEFAULT_OUTPUT_CHANNELS - } - - /// Initialize the plugin for the given bus and buffer configurations. These configurations will - /// not change until this function is called again, so feel free to copy these objects to your - /// plugin's object. If the plugin is being restored from an old state, then that state will - /// have already been restored at this point. If based on those parameters (or for any reason - /// whatsoever) the plugin needs to introduce latency, then you can do so here using the process - /// context. Depending on how the host restores plugin state, this function may also be called - /// twice in rapid succession. If the plugin fails to initialize for whatever reason, then this - /// should return `false`. + /// Initialize the plugin for the given audio IO configuration. From this point onwards the + /// audio IO layouts and the buffer sizes are fixed until this function is called again. /// /// Before this point, the plugin should not have done any expensive initialization. Please /// don't be that plugin that takes twenty seconds to scan. /// /// After this function [`reset()`][Self::reset()] will always be called. If you need to clear /// state, such as filters or envelopes, then you should do so in that function instead. + /// + /// - If you need to access this information in your process function, then you can copy the + /// values to your plugin instance's object. + /// - If the plugin is being restored from an old state, + /// then that state will have already been restored at this point. + /// - If based on those parameters (or for any reason whatsoever) the plugin needs to introduce + /// latency, then you can do so here using the process context. + /// - Depending on how the host restores plugin state, this function may be called multiple + /// times in rapid succession. It may thus be useful to check if the initialization work for + /// the current bufffer and audio IO configurations has already been performed first. + /// - If the plugin fails to initialize for whatever reason, then this should return `false`. fn initialize( &mut self, - bus_config: &BusConfig, + audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, context: &mut impl InitContext, ) -> bool { diff --git a/src/prelude.rs b/src/prelude.rs index f9e6d2d2..83a61a51 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,6 @@ +// Used in [`AudioIOLayout`] +pub use std::num::NonZeroU32; + // Re-export the macros, derive macros are already re-exported from their respective modules pub use crate::debug::*; @@ -11,7 +14,7 @@ pub use crate::formatters; pub use crate::util; pub use crate::audio_setup::{ - AuxiliaryBuffers, AuxiliaryIOConfig, BufferConfig, BusConfig, PortNames, ProcessMode, + new_nonzero_u32, AudioIOLayout, AuxiliaryBuffers, BufferConfig, PortNames, ProcessMode, }; pub use crate::buffer::Buffer; pub use crate::context::gui::{AsyncExecutor, GuiContext, ParamSetter}; diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index c5c2ce8c..15cdab41 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -68,6 +68,7 @@ use std::cmp; use std::collections::{HashMap, HashSet, VecDeque}; use std::ffi::{c_void, CStr}; use std::mem; +use std::num::NonZeroU32; use std::os::raw::c_char; use std::ptr; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -78,7 +79,7 @@ use std::time::Duration; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; use super::descriptor::PluginDescriptor; use super::util::ClapPtr; -use crate::audio_setup::{AuxiliaryBuffers, BufferConfig, BusConfig, ProcessMode}; +use crate::audio_setup::{AudioIOLayout, AuxiliaryBuffers, BufferConfig, ProcessMode}; use crate::buffer::Buffer; use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; @@ -127,8 +128,8 @@ pub struct Wrapper { is_processing: AtomicBool, /// The current IO configuration, modified through the `clap_plugin_audio_ports_config` - /// extension. - current_bus_config: AtomicCell, + /// extension. Initialized to the plugin's first audio IO configuration. + current_audio_io_layout: AtomicCell, /// The current buffer configuration, containing the sample rate and the maximum block size. /// Will be set in `clap_plugin::activate()`. current_buffer_config: AtomicCell>, @@ -181,14 +182,6 @@ pub struct Wrapper { host_callback: ClapPtr, clap_plugin_audio_ports_config: clap_plugin_audio_ports_config, - /// During initialization we'll ask `P` which bus configurations it supports. The host can then - /// use the audio ports config extension to choose a configuration. Right now we only query mono - /// and stereo configurations, with and without inputs, as well as the plugin's default input - /// and output channel counts if that does not match one of those configurations (to do the - /// least surprising thing). - /// - /// TODO: Support surround setups once a plugin needs that - supported_bus_configs: Vec, // The main `clap_plugin` vtable. A pointer to this `Wrapper

` instance is stored in the // `plugin_data` field. This pointer is set after creating the `Arc>`. @@ -538,52 +531,6 @@ impl Wrapper

{ } } - // Query all sensible bus configurations supported by the plugin. We don't do surround or - // anything beyond stereo right now. - let mut supported_bus_configs = Vec::new(); - for num_output_channels in [1, 2] { - for num_input_channels in [0, num_output_channels] { - #[allow(clippy::single_element_loop)] - for num_aux_channels in [num_output_channels] { - let bus_config = BusConfig { - num_input_channels, - num_output_channels, - // We won't support a variable number of busses until that's required, so - // we'll always use the number of auxiliary busses specified by the plugin - aux_input_busses: P::DEFAULT_AUX_INPUTS - .map(|mut aux| { - aux.num_channels = num_aux_channels; - aux - }) - .unwrap_or_default(), - aux_output_busses: P::DEFAULT_AUX_OUTPUTS - .map(|mut aux| { - aux.num_channels = num_aux_channels; - aux - }) - .unwrap_or_default(), - }; - if plugin.accepts_bus_config(&bus_config) { - supported_bus_configs.push(bus_config); - } - } - } - } - - // In the off chance that the default config specified by the plugin is not in the above - // list, we'll try that as well. - let default_bus_config = BusConfig { - num_input_channels: P::DEFAULT_INPUT_CHANNELS, - num_output_channels: P::DEFAULT_OUTPUT_CHANNELS, - aux_input_busses: P::DEFAULT_AUX_INPUTS.unwrap_or_default(), - aux_output_busses: P::DEFAULT_AUX_OUTPUTS.unwrap_or_default(), - }; - if !supported_bus_configs.contains(&default_bus_config) - && plugin.accepts_bus_config(&default_bus_config) - { - supported_bus_configs.push(default_bus_config); - } - let wrapper = Self { this: AtomicRefCell::new(Weak::new()), @@ -596,12 +543,9 @@ impl Wrapper

{ editor_scaling_factor: AtomicF32::new(1.0), is_processing: AtomicBool::new(false), - current_bus_config: AtomicCell::new(BusConfig { - num_input_channels: P::DEFAULT_INPUT_CHANNELS, - num_output_channels: P::DEFAULT_OUTPUT_CHANNELS, - aux_input_busses: P::DEFAULT_AUX_INPUTS.unwrap_or_default(), - aux_output_busses: P::DEFAULT_AUX_OUTPUTS.unwrap_or_default(), - }), + current_audio_io_layout: AtomicCell::new( + P::AUDIO_IO_LAYOUTS.first().copied().unwrap_or_default(), + ), current_buffer_config: AtomicCell::new(None), current_process_mode: AtomicCell::new(ProcessMode::Realtime), input_events: AtomicRefCell::new(VecDeque::with_capacity(512)), @@ -643,7 +587,6 @@ impl Wrapper

{ get: Some(Self::ext_audio_ports_config_get), select: Some(Self::ext_audio_ports_config_select), }, - supported_bus_configs, clap_plugin_audio_ports: clap_plugin_audio_ports { count: Some(Self::ext_audio_ports_count), @@ -1748,12 +1691,12 @@ impl Wrapper

{ ); } - let bus_config = self.current_bus_config.load(); + let audio_io_layout = self.current_audio_io_layout.load(); if let Some(buffer_config) = self.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.make_init_context(); let mut plugin = self.plugin.lock(); - plugin.initialize(&bus_config, &buffer_config, &mut init_context); + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context); process_wrapper(|| plugin.reset()); } @@ -1841,7 +1784,7 @@ impl Wrapper

{ check_null_ptr!(false, plugin, (*plugin).plugin_data); let wrapper = &*((*plugin).plugin_data as *const Self); - let bus_config = wrapper.current_bus_config.load(); + let audio_io_layout = wrapper.current_audio_io_layout.load(); let buffer_config = BufferConfig { sample_rate: sample_rate as f32, min_buffer_size: Some(min_frames_count), @@ -1857,7 +1800,7 @@ impl Wrapper

{ // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = wrapper.make_init_context(); let mut plugin = wrapper.plugin.lock(); - if plugin.initialize(&bus_config, &buffer_config, &mut init_context) { + if plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context) { // NOTE: `Plugin::reset()` is called in `clap_plugin::start_processing()` instead of in // this function @@ -1867,7 +1810,13 @@ impl Wrapper

{ .output_buffer .borrow_mut() .set_slices(0, |output_slices| { - output_slices.resize_with(bus_config.num_output_channels as usize, || &mut []); + output_slices.resize_with( + audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize, + || &mut [], + ); // All slices must have the same length, so if the number of output channels has // changed since the last call then we should make sure to clear any old // (dangling) slices to be consistent @@ -1878,43 +1827,34 @@ impl Wrapper

{ // inputs. The slices will be assigned in the process function as this object may have // been moved before then. let mut aux_input_storage = wrapper.aux_input_storage.borrow_mut(); - aux_input_storage - .resize_with(bus_config.aux_input_busses.num_busses as usize, Vec::new); - for bus_storage in aux_input_storage.iter_mut() { - bus_storage - .resize_with(bus_config.aux_input_busses.num_channels as usize, Vec::new); - for channel_storage in bus_storage { + let mut aux_input_buffers = wrapper.aux_input_buffers.borrow_mut(); + aux_input_storage.resize_with(audio_io_layout.aux_input_ports.len(), Vec::new); + aux_input_buffers.resize_with(audio_io_layout.aux_input_ports.len(), Buffer::default); + for ((buffer_storage, buffer), num_channels) in aux_input_storage + .iter_mut() + .zip(aux_input_buffers.iter_mut()) + .zip(audio_io_layout.aux_input_ports.iter()) + { + buffer_storage.resize_with(num_channels.get() as usize, Vec::new); + for channel_storage in buffer_storage { channel_storage.resize(max_frames_count as usize, 0.0); } - } - let mut aux_input_buffers = wrapper.aux_input_buffers.borrow_mut(); - aux_input_buffers.resize_with( - bus_config.aux_input_busses.num_busses as usize, - Buffer::default, - ); - for buffer in aux_input_buffers.iter_mut() { buffer.set_slices(0, |channel_slices| { - channel_slices - .resize_with(bus_config.aux_input_busses.num_channels as usize, || { - &mut [] - }); + channel_slices.resize_with(num_channels.get() as usize, || &mut []); channel_slices.fill_with(|| &mut []); }); } // And the same thing for the output buffers let mut aux_output_buffers = wrapper.aux_output_buffers.borrow_mut(); - aux_output_buffers.resize_with( - bus_config.aux_output_busses.num_busses as usize, - Buffer::default, - ); - for buffer in aux_output_buffers.iter_mut() { + aux_output_buffers.resize_with(audio_io_layout.aux_output_ports.len(), Buffer::default); + for (buffer, num_channels) in aux_output_buffers + .iter_mut() + .zip(audio_io_layout.aux_output_ports.iter()) + { buffer.set_slices(0, |channel_slices| { - channel_slices - .resize_with(bus_config.aux_output_busses.num_channels as usize, || { - &mut [] - }); + channel_slices.resize_with(num_channels.get() as usize, || &mut []); channel_slices.fill_with(|| &mut []); }); } @@ -1984,14 +1924,18 @@ impl Wrapper

{ // Before doing anything, clear out any auxiliary outputs since they may contain // uninitialized data when the host assumes that we'll always write something there - let current_bus_config = wrapper.current_bus_config.load(); - let has_main_input = current_bus_config.num_input_channels > 0; - let has_main_output = current_bus_config.num_output_channels > 0; + let current_audio_io_layout = wrapper.current_audio_io_layout.load(); + let (aux_input_start_idx, aux_output_start_idx) = { + let has_main_input = current_audio_io_layout.main_input_channels.is_some(); + let has_main_output = current_audio_io_layout.main_output_channels.is_some(); + ( + if has_main_input { 1 } else { 0 }, + if has_main_output { 1 } else { 0 }, + ) + }; if process.audio_outputs_count > 0 && !process.audio_outputs.is_null() { - for output_idx in - if has_main_output { 1 } else { 0 }..process.audio_outputs_count as isize - { - let host_output = process.audio_outputs.offset(output_idx); + for output_idx in aux_output_start_idx..process.audio_outputs_count as usize { + let host_output = process.audio_outputs.add(output_idx); if !(*host_output).data32.is_null() { for channel_idx in 0..(*host_output).channel_count as isize { ptr::write_bytes( @@ -2160,13 +2104,9 @@ impl Wrapper

{ .zip(aux_input_buffers.iter_mut()) .enumerate() { - let host_input_idx = if has_main_input { - auxiliary_input_idx as isize + 1 - } else { - auxiliary_input_idx as isize - }; - let host_input = process.audio_inputs.offset(host_input_idx); - if host_input_idx >= process.audio_inputs_count as isize + let host_input_idx = auxiliary_input_idx + aux_input_start_idx; + let host_input = process.audio_inputs.add(host_input_idx); + if host_input_idx >= process.audio_inputs_count as usize || process.audio_inputs.is_null() || (*host_input).data32.is_null() // Would only happen if the user configured zero channels for the @@ -2174,7 +2114,7 @@ impl Wrapper

{ || storage.is_empty() || (*host_input).channel_count != buffer.channels() as u32 { - nih_debug_assert!(host_input_idx < process.audio_inputs_count as isize); + nih_debug_assert!(host_input_idx < process.audio_inputs_count as usize); nih_debug_assert!(!process.audio_inputs.is_null()); nih_debug_assert!(!(*host_input).data32.is_null()); nih_debug_assert!(!storage.is_empty()); @@ -2217,18 +2157,14 @@ impl Wrapper

{ // And the same thing for auxiliary output buffers let mut aux_output_buffers = wrapper.aux_output_buffers.borrow_mut(); for (auxiliary_output_idx, buffer) in aux_output_buffers.iter_mut().enumerate() { - let host_output_idx = if has_main_output { - auxiliary_output_idx as isize + 1 - } else { - auxiliary_output_idx as isize - }; - let host_output = process.audio_outputs.offset(host_output_idx); - if host_output_idx >= process.audio_outputs_count as isize + let host_output_idx = auxiliary_output_idx + aux_output_start_idx; + let host_output = process.audio_outputs.add(host_output_idx); + if host_output_idx >= process.audio_outputs_count as usize || process.audio_outputs.is_null() || (*host_output).data32.is_null() || buffer.channels() == 0 { - nih_debug_assert!(host_output_idx < process.audio_outputs_count as isize); + nih_debug_assert!(host_output_idx < process.audio_outputs_count as usize); nih_debug_assert!(!process.audio_outputs.is_null()); nih_debug_assert!(!(*host_output).data32.is_null()); @@ -2402,14 +2338,16 @@ impl Wrapper

{ // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = wrapper.make_init_context(); - let bus_config = wrapper.current_bus_config.load(); + let audio_io_layout = wrapper.current_audio_io_layout.load(); let buffer_config = wrapper.current_buffer_config.load().unwrap(); let mut plugin = wrapper.plugin.lock(); // FIXME: This is obviously not realtime-safe, but loading presets without doing // this could lead to inconsistencies. It's the plugin's responsibility to // not perform any realtime-unsafe work when the initialize function is // called a second time if it supports runtime preset loading. - permit_alloc(|| plugin.initialize(&bus_config, &buffer_config, &mut init_context)); + permit_alloc(|| { + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context) + }); plugin.reset(); // Reinitialize the plugin after loading state so it can respond to the new parameter values @@ -2481,9 +2419,8 @@ impl Wrapper

{ unsafe extern "C" fn ext_audio_ports_config_count(plugin: *const clap_plugin) -> u32 { check_null_ptr!(0, plugin, (*plugin).plugin_data); - let wrapper = &*((*plugin).plugin_data as *const Self); - wrapper.supported_bus_configs.len() as u32 + P::AUDIO_IO_LAYOUTS.len() as u32 } unsafe extern "C" fn ext_audio_ports_config_get( @@ -2492,37 +2429,24 @@ impl Wrapper

{ config: *mut clap_audio_ports_config, ) -> bool { check_null_ptr!(false, plugin, (*plugin).plugin_data, config); - let wrapper = &*((*plugin).plugin_data as *const Self); - match wrapper.supported_bus_configs.get(index as usize) { - Some(bus_config) => { - // We don't support variable auxiliary IO configs right now, so we don't need to - // specify sidechain inputs and aux outputs in these descriptions - let name = match bus_config { - BusConfig { - num_input_channels: _, - num_output_channels: 1, - .. - } => String::from("Mono"), - BusConfig { - num_input_channels: _, - num_output_channels: 2, - .. - } => String::from("Stereo"), - BusConfig { - num_input_channels, - num_output_channels, - .. - } => format!("{num_input_channels} inputs, {num_output_channels} outputs"), - }; - let input_port_type = match bus_config.num_input_channels { - 1 => CLAP_PORT_MONO.as_ptr(), - 2 => CLAP_PORT_STEREO.as_ptr(), + // This function directly maps to `P::AUDIO_IO_LAYOUTS`, and we thus also don't need to + // access the `wrapper` instance + match P::AUDIO_IO_LAYOUTS.get(index as usize) { + Some(audio_io_layout) => { + let name = audio_io_layout.name(); + + let main_input_channels = audio_io_layout.main_input_channels.map(NonZeroU32::get); + let main_output_channels = + audio_io_layout.main_output_channels.map(NonZeroU32::get); + let input_port_type = match main_input_channels { + Some(1) => CLAP_PORT_MONO.as_ptr(), + Some(2) => CLAP_PORT_STEREO.as_ptr(), _ => ptr::null(), }; - let output_port_type = match bus_config.num_output_channels { - 1 => CLAP_PORT_MONO.as_ptr(), - 2 => CLAP_PORT_STEREO.as_ptr(), + let output_port_type = match main_output_channels { + Some(1) => CLAP_PORT_MONO.as_ptr(), + Some(2) => CLAP_PORT_STEREO.as_ptr(), _ => ptr::null(), }; @@ -2531,21 +2455,17 @@ impl Wrapper

{ let config = &mut *config; config.id = index; strlcpy(&mut config.name, &name); - config.input_port_count = if bus_config.num_input_channels > 0 { - 1 - } else { - 0 - } + bus_config.aux_input_busses.num_busses; - config.output_port_count = if bus_config.num_output_channels > 0 { - 1 - } else { - 0 - } + bus_config.aux_output_busses.num_busses; - config.has_main_input = bus_config.num_output_channels > 0; - config.main_input_channel_count = bus_config.num_output_channels; + config.input_port_count = (if main_input_channels.is_some() { 1 } else { 0 } + + audio_io_layout.aux_input_ports.len()) + as u32; + config.output_port_count = (if main_output_channels.is_some() { 1 } else { 0 } + + audio_io_layout.aux_output_ports.len()) + as u32; + config.has_main_input = main_input_channels.is_some(); + config.main_input_channel_count = main_input_channels.unwrap_or_default(); config.main_input_port_type = input_port_type; - config.has_main_output = bus_config.num_output_channels > 0; - config.main_output_channel_count = bus_config.num_output_channels; + config.has_main_output = main_output_channels.is_some(); + config.main_output_channel_count = main_output_channels.unwrap_or_default(); config.main_output_port_type = output_port_type; true @@ -2569,9 +2489,9 @@ impl Wrapper

{ let wrapper = &*((*plugin).plugin_data as *const Self); // We use the vector indices for the config ID - match wrapper.supported_bus_configs.get(config_id as usize) { - Some(bus_config) => { - wrapper.current_bus_config.store(*bus_config); + match P::AUDIO_IO_LAYOUTS.get(config_id as usize) { + Some(audio_io_layout) => { + wrapper.current_audio_io_layout.store(*audio_io_layout); true } @@ -2590,25 +2510,25 @@ impl Wrapper

{ check_null_ptr!(0, plugin, (*plugin).plugin_data); let wrapper = &*((*plugin).plugin_data as *const Self); - let bus_config = wrapper.current_bus_config.load(); + let audio_io_layout = wrapper.current_audio_io_layout.load(); if is_input { - let main_busses = if bus_config.num_input_channels > 0 { + let main_ports = if audio_io_layout.main_input_channels.is_some() { 1 } else { 0 }; - let aux_busses = bus_config.aux_input_busses.num_busses; + let aux_ports = audio_io_layout.aux_input_ports.len(); - main_busses + aux_busses + (main_ports + aux_ports) as u32 } else { - let main_busses = if bus_config.num_output_channels > 0 { + let main_ports = if audio_io_layout.main_output_channels.is_some() { 1 } else { 0 }; - let aux_busses = bus_config.aux_output_busses.num_busses; + let aux_ports = audio_io_layout.aux_output_ports.len(); - main_busses + aux_busses + (main_ports + aux_ports) as u32 } } @@ -2633,9 +2553,9 @@ impl Wrapper

{ return false; } - let current_bus_config = wrapper.current_bus_config.load(); - let has_main_input = current_bus_config.num_input_channels > 0; - let has_main_output = current_bus_config.num_output_channels > 0; + let current_audio_io_layout = wrapper.current_audio_io_layout.load(); + let has_main_input = current_audio_io_layout.main_input_channels.is_some(); + let has_main_output = current_audio_io_layout.main_output_channels.is_some(); // Whether this port is a main port or an auxiliary (sidechain) port let is_main_port = @@ -2656,11 +2576,22 @@ impl Wrapper

{ _ => CLAP_INVALID_ID, }; - let channel_count = match (is_input, is_main_port) { - (true, true) => current_bus_config.num_input_channels, - (false, true) => current_bus_config.num_output_channels, - (true, false) => current_bus_config.aux_input_busses.num_channels, - (false, false) => current_bus_config.aux_output_busses.num_channels, + let channel_count = match (index, is_input) { + (0, true) if has_main_input => { + current_audio_io_layout.main_input_channels.unwrap().get() + } + (0, false) if has_main_output => { + current_audio_io_layout.main_output_channels.unwrap().get() + } + // `index` is off by one for the auxiliary ports if the plugin has a main port + (n, true) if has_main_input => { + current_audio_io_layout.aux_input_ports[n as usize - 1].get() + } + (n, false) if has_main_output => { + current_audio_io_layout.aux_output_ports[n as usize - 1].get() + } + (n, true) => current_audio_io_layout.aux_input_ports[n as usize].get(), + (n, false) => current_audio_io_layout.aux_output_ports[n as usize].get(), }; let port_type = match channel_count { @@ -2674,46 +2605,25 @@ impl Wrapper

{ let info = &mut *info; info.id = stable_id; match (is_input, is_main_port) { - (true, true) => strlcpy(&mut info.name, P::PORT_NAMES.main_input.unwrap_or("Input")), - (false, true) => strlcpy( - &mut info.name, - P::PORT_NAMES.main_output.unwrap_or("Output"), - ), + (true, true) => strlcpy(&mut info.name, ¤t_audio_io_layout.main_input_name()), + (false, true) => strlcpy(&mut info.name, ¤t_audio_io_layout.main_output_name()), (true, false) => { - let aux_input_idx = if has_main_input { index - 1 } else { index }; - let custom_port_name = P::PORT_NAMES - .aux_inputs - .map(|names| names[aux_input_idx as usize]); - if current_bus_config.aux_input_busses.num_busses <= 1 { - strlcpy( - &mut info.name, - custom_port_name.unwrap_or("Sidechain Input"), - ); - } else { - strlcpy( - &mut info.name, - custom_port_name - .unwrap_or(&format!("Sidechain Input {}", aux_input_idx + 1)), - ); - } + let aux_input_idx = if has_main_input { index - 1 } else { index } as usize; + strlcpy( + &mut info.name, + ¤t_audio_io_layout + .aux_input_name(aux_input_idx) + .expect("Out of bounds auxiliary input port"), + ); } (false, false) => { - let aux_output_idx = if has_main_output { index - 1 } else { index }; - let custom_port_name = P::PORT_NAMES - .aux_outputs - .map(|names| names[aux_output_idx as usize]); - if current_bus_config.aux_output_busses.num_busses <= 1 { - strlcpy( - &mut info.name, - custom_port_name.unwrap_or("Auxiliary Output"), - ); - } else { - strlcpy( - &mut info.name, - custom_port_name - .unwrap_or(&format!("Auxiliary Output {}", aux_output_idx + 1)), - ); - } + let aux_output_idx = if has_main_output { index - 1 } else { index } as usize; + strlcpy( + &mut info.name, + ¤t_audio_io_layout + .aux_output_name(aux_output_idx) + .expect("Out of bounds auxiliary output port"), + ); } }; info.flags = if is_main_port { @@ -3301,12 +3211,12 @@ impl Wrapper

{ return false; } - let bus_config = wrapper.current_bus_config.load(); + let audio_io_layout = wrapper.current_audio_io_layout.load(); if let Some(buffer_config) = wrapper.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = wrapper.make_init_context(); let mut plugin = wrapper.plugin.lock(); - plugin.initialize(&bus_config, &buffer_config, &mut init_context); + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context); // TODO: This also goes for the VST3 version, but should we call reset here? Won't the // host always restart playback? Check this with a couple of hosts and remove the // duplicate reset if it's not needed. diff --git a/src/wrapper/standalone.rs b/src/wrapper/standalone.rs index c7a29ae3..d9ce1e04 100644 --- a/src/wrapper/standalone.rs +++ b/src/wrapper/standalone.rs @@ -196,15 +196,6 @@ fn run_wrapper>(backend: B, config: WrapperConfig) -> b fn print_error(error: WrapperError) { match error { - WrapperError::IncompatibleConfig { - input_channels, - output_channels, - } => { - nih_error!( - "The plugin does not support the {input_channels} channel input and \ - {output_channels} channel output configuration", - ); - } WrapperError::InitializationFailed => { nih_error!("The plugin failed to initialize"); } diff --git a/src/wrapper/standalone/backend/cpal.rs b/src/wrapper/standalone/backend/cpal.rs index a32a7b19..9aa664d0 100644 --- a/src/wrapper/standalone/backend/cpal.rs +++ b/src/wrapper/standalone/backend/cpal.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU32; + use anyhow::{Context, Result}; use cpal::{ traits::*, Device, InputCallbackInfo, OutputCallbackInfo, Sample, SampleFormat, Stream, @@ -8,7 +10,7 @@ use rtrb::RingBuffer; use super::super::config::WrapperConfig; use super::Backend; -use crate::audio_setup::{AuxiliaryIOConfig, BusConfig}; +use crate::audio_setup::AudioIOLayout; use crate::buffer::Buffer; use crate::context::process::Transport; use crate::midi::{MidiConfig, PluginNoteEvent}; @@ -17,7 +19,7 @@ use crate::plugin::Plugin; /// Uses CPAL for audio and midir for MIDI. pub struct Cpal { config: WrapperConfig, - bus_config: BusConfig, + audio_io_layout: AudioIOLayout, input: Option<(Device, StreamConfig, SampleFormat)>, @@ -136,9 +138,10 @@ impl Cpal { /// Initialize the backend with the specified host. Returns an error if this failed for whatever /// reason. pub fn new(config: WrapperConfig, cpal_host_id: cpal::HostId) -> Result { + let audio_io_layout = config.audio_io_layout_or_exit::

(); let host = cpal::host_from_id(cpal_host_id).context("The Audio API is unavailable")?; - if config.input_device.is_none() && P::DEFAULT_INPUT_CHANNELS > 0 { + if config.input_device.is_none() && audio_io_layout.main_input_channels.is_some() { nih_log!( "Audio inputs are not connected automatically to prevent feedback. Use the \ '--input-device' option to choose an input device." @@ -192,16 +195,12 @@ impl Cpal { .context("No default audio output device available")?, }; - let bus_config = BusConfig { - num_input_channels: config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS), - num_output_channels: config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS), - // TODO: Support these in the standalone - aux_input_busses: AuxiliaryIOConfig::default(), - aux_output_busses: AuxiliaryIOConfig::default(), - }; let requested_sample_rate = cpal::SampleRate(config.sample_rate as u32); let requested_buffer_size = cpal::BufferSize::Fixed(config.period_size); - + let num_input_channels = audio_io_layout + .main_input_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; let input = input_device .map(|device| -> Result<(Device, StreamConfig, SampleFormat)> { let input_configs: Vec<_> = device @@ -209,7 +208,7 @@ impl Cpal { .context("Could not get supported audio input configurations")? .filter(|c| match c.buffer_size() { cpal::SupportedBufferSize::Range { min, max } => { - c.channels() as u32 == bus_config.num_input_channels + c.channels() as usize == num_input_channels && (c.min_sample_rate()..=c.max_sample_rate()) .contains(&requested_sample_rate) && (min..=max).contains(&&config.period_size) @@ -227,7 +226,7 @@ impl Cpal { format!( "The audio input device does not support {} audio channels at a \ sample rate of {} Hz and a period size of {} samples", - bus_config.num_input_channels, config.sample_rate, config.period_size, + num_input_channels, config.sample_rate, config.period_size, ) })?; @@ -243,12 +242,16 @@ impl Cpal { }) .transpose()?; + let num_output_channels = audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; let output_configs: Vec<_> = output_device .supported_output_configs() .context("Could not get supported audio output configurations")? .filter(|c| match c.buffer_size() { cpal::SupportedBufferSize::Range { min, max } => { - c.channels() as u32 == bus_config.num_output_channels + c.channels() as usize == num_output_channels && (c.min_sample_rate()..=c.max_sample_rate()) .contains(&requested_sample_rate) && (min..=max).contains(&&config.period_size) @@ -265,7 +268,7 @@ impl Cpal { format!( "The audio output device does not support {} audio channels at a sample rate \ of {} Hz and a period size of {} samples", - bus_config.num_output_channels, config.sample_rate, config.period_size, + num_output_channels, config.sample_rate, config.period_size, ) })?; let output_config = StreamConfig { @@ -282,7 +285,7 @@ impl Cpal { Ok(Cpal { config, - bus_config, + audio_io_layout, input, @@ -328,12 +331,15 @@ impl Cpal { // We'll receive interlaced input samples from CPAL. These need to converted to deinterlaced // channels, processed, and then copied those back to an interlaced buffer for the output. // This needs to be wrapped in a struct like this and boxed because the `channels` vectors - // need to live just as long as `buffer` when they get moved into the closure. FIXME: This - // is pretty nasty, come up with a cleaner alternative - let mut channels = vec![ - vec![0.0f32; self.config.period_size as usize]; - self.bus_config.num_output_channels as usize - ]; + // need to live just as long as `buffer` when they get moved into the closure. + // FIXME: This is pretty nasty, come up with a cleaner alternative + let num_output_channels = self + .audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; + let mut channels = + vec![vec![0.0f32; self.config.period_size as usize]; num_output_channels]; let mut buffer = Buffer::default(); unsafe { buffer.set_slices(0, |output_slices| { diff --git a/src/wrapper/standalone/backend/dummy.rs b/src/wrapper/standalone/backend/dummy.rs index da53db5f..704d094f 100644 --- a/src/wrapper/standalone/backend/dummy.rs +++ b/src/wrapper/standalone/backend/dummy.rs @@ -1,8 +1,9 @@ +use std::num::NonZeroU32; use std::time::{Duration, Instant}; use super::super::config::WrapperConfig; use super::Backend; -use crate::audio_setup::{AuxiliaryIOConfig, BusConfig}; +use crate::audio_setup::AudioIOLayout; use crate::buffer::Buffer; use crate::context::process::Transport; use crate::midi::PluginNoteEvent; @@ -13,7 +14,7 @@ use crate::plugin::Plugin; /// useful for testing plugin GUIs. pub struct Dummy { config: WrapperConfig, - bus_config: BusConfig, + audio_io_layout: AudioIOLayout, } impl Backend

for Dummy { @@ -33,10 +34,13 @@ impl Backend

for Dummy { let interval = Duration::from_secs_f32(self.config.period_size as f32 / self.config.sample_rate); - let mut channels = vec![ - vec![0.0f32; self.config.period_size as usize]; - self.bus_config.num_output_channels as usize - ]; + let num_output_channels = self + .audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; + let mut channels = + vec![vec![0.0f32; self.config.period_size as usize]; num_output_channels]; let mut buffer = Buffer::default(); unsafe { buffer.set_slices(self.config.period_size as usize, |output_slices| { @@ -81,13 +85,7 @@ impl Backend

for Dummy { impl Dummy { pub fn new(config: WrapperConfig) -> Self { Self { - bus_config: BusConfig { - num_input_channels: config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS), - num_output_channels: config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS), - // TODO: Support these in the standalone - aux_input_busses: AuxiliaryIOConfig::default(), - aux_output_busses: AuxiliaryIOConfig::default(), - }, + audio_io_layout: config.audio_io_layout_or_exit::

(), config, } } diff --git a/src/wrapper/standalone/backend/jack.rs b/src/wrapper/standalone/backend/jack.rs index 4c027173..b0de5709 100644 --- a/src/wrapper/standalone/backend/jack.rs +++ b/src/wrapper/standalone/backend/jack.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::num::NonZeroU32; use std::sync::Arc; use anyhow::{Context, Result}; @@ -18,6 +19,9 @@ use crate::plugin::Plugin; use crate::wrapper::util::{clamp_input_event_timing, clamp_output_event_timing}; /// Uses JACK audio and MIDI. +// +// TODO: Use the bus names +// TODO: Support auxiliary inputs and outputs pub struct Jack { config: WrapperConfig, /// The JACK client, wrapped in an option since it needs to be transformed into an `AsyncClient` @@ -193,13 +197,14 @@ impl Jack { /// generic argument is to get the name for the client, and to know whether or not the /// standalone should expose JACK MIDI ports. pub fn new(config: WrapperConfig) -> Result { + let audio_io_layout = config.audio_io_layout_or_exit::

(); let (client, status) = Client::new(P::NAME, ClientOptions::NO_START_SERVER) .context("Error while initializing the JACK client")?; if !status.is_empty() { anyhow::bail!("The JACK server returned an error: {status:?}"); } - if config.connect_jack_inputs.is_none() && P::DEFAULT_INPUT_CHANNELS > 0 { + if config.connect_jack_inputs.is_none() && audio_io_layout.main_input_channels.is_some() { nih_log!( "Audio inputs are not connected automatically to prevent feedback. Use the \ '--connect-jack-inputs' option to connect the input ports." @@ -207,7 +212,10 @@ impl Jack { } let mut inputs = Vec::new(); - let num_input_channels = config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS); + let num_input_channels = audio_io_layout + .main_input_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; for port_no in 1..num_input_channels + 1 { inputs.push(client.register_port(&format!("input_{port_no}"), AudioIn)?); } @@ -216,7 +224,10 @@ impl Jack { // no. So the connections are made just after activating the client in the `run()` function // above. let mut outputs = Vec::new(); - let num_output_channels = config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS); + let num_output_channels = audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize; for port_no in 1..num_output_channels + 1 { outputs.push(client.register_port(&format!("output_{port_no}"), AudioOut)?); } diff --git a/src/wrapper/standalone/config.rs b/src/wrapper/standalone/config.rs index eff97839..9e82c636 100644 --- a/src/wrapper/standalone/config.rs +++ b/src/wrapper/standalone/config.rs @@ -1,4 +1,8 @@ use clap::{Parser, ValueEnum}; +use std::num::NonZeroU32; + +use crate::audio_setup::AudioIOLayout; +use crate::plugin::Plugin; /// Configuration for a standalone plugin that would normally be provided by the DAW. #[derive(Debug, Clone, Parser)] @@ -22,15 +26,14 @@ pub struct WrapperConfig { #[clap(value_parser, long)] pub output_device: Option, - // These will default to the plugin's default input and output channel count. We could set the - // default value here to match those, but that would require a custom Args+FromArgMatches - // implementation and access to the `Plugin` type. - /// The number of input channels. - #[clap(value_parser, short = 'i', long)] - pub input_channels: Option, - /// The number of output channels. - #[clap(value_parser, short = 'o', long)] - pub output_channels: Option, + /// The audio layout to use. Defaults to the first layout. + /// + /// Specifying an empty argument or other invalid value will list all available audio layouts. + // + // NOTE: This takes a `String` instead of a `usize` so we can list the layouts when the argument + // is invalid + #[clap(value_parser, short = 'l', long)] + pub audio_layout: Option, /// The audio backend's sample rate. /// /// This setting is ignored when using the JACK backend. @@ -104,3 +107,67 @@ pub enum BackendType { /// Does not playback or receive any audio or MIDI. Dummy, } + +impl WrapperConfig { + /// Get the audio IO layout for a plugin based on this configuration. Exits the application if + /// the IO layout could not be parsed from the config. This doesn't return a `Result` to be able to differentiate between backend-specific errors and config parsing errors. + pub fn audio_io_layout_or_exit(&self) -> AudioIOLayout { + // The layouts are one-indexed here + match &self.audio_layout { + Some(audio_layout) if !P::AUDIO_IO_LAYOUTS.is_empty() => { + match audio_layout.parse::() { + Ok(n) if n >= 1 && n - 1 < P::AUDIO_IO_LAYOUTS.len() => { + P::AUDIO_IO_LAYOUTS[n - 1] + } + _ => { + // This is made to be consistent with how audio input and output devices are + // listed in the CPAL backend + let mut layouts_str = String::new(); + for (idx, layout) in P::AUDIO_IO_LAYOUTS.iter().enumerate() { + let num_input_channels = layout + .main_input_channels + .map(NonZeroU32::get) + .unwrap_or_default(); + let num_output_channels = layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default(); + layouts_str.push_str(&format!( + "\n{}: {} ({} input {}, {} output {}{}{})", + idx + 1, + layout.name(), + num_input_channels, + if num_input_channels == 1 { + "channel" + } else { + "channels" + }, + num_output_channels, + if num_output_channels == 1 { + "channel" + } else { + "channels" + }, + if layout.aux_input_ports.is_empty() { + String::new() + } else { + format!("{} sidechain inputs", layout.aux_input_ports.len()) + }, + if layout.aux_output_ports.is_empty() { + String::new() + } else { + format!("{} sidechain outputs", layout.aux_output_ports.len()) + }, + )) + } + + nih_log!("The available audio layouts are:{layouts_str}"); + + std::process::exit(1); + } + } + } + _ => P::AUDIO_IO_LAYOUTS.first().copied().unwrap_or_default(), + } + } +} diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index db1e09a4..6516d9c7 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -14,7 +14,7 @@ use super::backend::Backend; use super::config::WrapperConfig; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; use crate::audio_setup::AuxiliaryBuffers; -use crate::audio_setup::{AuxiliaryIOConfig, BufferConfig, BusConfig, ProcessMode}; +use crate::audio_setup::{AudioIOLayout, BufferConfig, ProcessMode}; use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; use crate::editor::{Editor, ParentWindowHandle}; @@ -67,7 +67,7 @@ pub struct Wrapper> { param_id_to_ptr: HashMap, /// The bus and buffer configurations are static for the standalone target. - bus_config: BusConfig, + audio_io_layout: AudioIOLayout, buffer_config: BufferConfig, /// Parameter changes that have been output by the GUI that have not yet been set in the plugin. @@ -106,11 +106,6 @@ pub enum Task { /// Errors that may arise while initializing the wrapped plugins. #[derive(Debug, Clone, Copy)] pub enum WrapperError { - /// The plugin does not accept the IO configuration from the config. - IncompatibleConfig { - input_channels: u32, - output_channels: u32, - }, /// The plugin returned `false` during initialization. InitializationFailed, } @@ -178,6 +173,11 @@ impl> Wrapper { /// Instantiate a new instance of the standalone wrapper. Returns an error if the plugin does /// not accept the IO configuration from the wrapper config. pub fn new(backend: B, config: WrapperConfig) -> Result, WrapperError> { + // The backend has already queried this, so this will never cause the program to exit + // TODO: Do the validation and parsing in the argument parser so this value can be stored on + // the config itself. Right now clap doesn't support this. + let audio_io_layout = config.audio_io_layout_or_exit::

(); + let plugin = P::default(); let task_executor = Mutex::new(plugin.task_executor()); let params = plugin.params(); @@ -214,11 +214,11 @@ impl> Wrapper { } // TODO: Sidechain inputs and auxiliary outputs - if P::DEFAULT_AUX_INPUTS.is_some() { - nih_log!("Sidechain inputs are not yet supported in this standalone version"); + if !audio_io_layout.aux_input_ports.is_empty() { + nih_warn!("Sidechain inputs are not yet supported in this standalone version"); } - if P::DEFAULT_AUX_OUTPUTS.is_some() { - nih_log!("Auxiliary outputs are not yet supported in this standalone version"); + if !audio_io_layout.aux_output_ports.is_empty() { + nih_warn!("Auxiliary outputs are not yet supported in this standalone version"); } let wrapper = Arc::new(Wrapper { @@ -242,13 +242,7 @@ impl> Wrapper { .map(|(param_id, param_ptr, _)| (param_id, param_ptr)) .collect(), - bus_config: BusConfig { - num_input_channels: config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS), - num_output_channels: config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS), - // TODO: Expose additional sidechain IO in the JACK backend - aux_input_busses: AuxiliaryIOConfig::default(), - aux_output_busses: AuxiliaryIOConfig::default(), - }, + audio_io_layout, buffer_config: BufferConfig { sample_rate: config.sample_rate, min_buffer_size: None, @@ -289,24 +283,15 @@ impl> Wrapper { }) .map(|editor| Arc::new(Mutex::new(editor))); - // Right now the IO configuration is fixed in the standalone target, so if the plugin cannot - // work with this then we cannot initialize the plugin at all. + // Before initializing the plugin, make sure all smoothers are set the the default values + for param in wrapper.param_id_to_ptr.values() { + unsafe { param.update_smoother(wrapper.buffer_config.sample_rate, true) }; + } + { let mut plugin = wrapper.plugin.lock(); - if !plugin.accepts_bus_config(&wrapper.bus_config) { - return Err(WrapperError::IncompatibleConfig { - input_channels: wrapper.bus_config.num_input_channels, - output_channels: wrapper.bus_config.num_output_channels, - }); - } - - // Before initializing the plugin, make sure all smoothers are set the the default values - for param in wrapper.param_id_to_ptr.values() { - unsafe { param.update_smoother(wrapper.buffer_config.sample_rate, true) }; - } - if !plugin.initialize( - &wrapper.bus_config, + &wrapper.audio_io_layout, &wrapper.buffer_config, &mut wrapper.make_init_context(), ) { @@ -558,7 +543,7 @@ impl> Wrapper { // runtime preset loading. permit_alloc(|| { plugin.initialize( - &self.bus_config, + &self.audio_io_layout, &self.buffer_config, &mut self.make_init_context(), ) diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 8be4262c..b5932253 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -14,7 +14,7 @@ use super::note_expressions::NoteExpressionController; use super::param_units::ParamUnits; use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START}; use super::view::WrapperView; -use crate::audio_setup::{BufferConfig, BusConfig, ProcessMode}; +use crate::audio_setup::{AudioIOLayout, BufferConfig, ProcessMode}; use crate::buffer::Buffer; use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; @@ -66,8 +66,11 @@ pub(crate) struct WrapperInner { /// Whether the plugin is currently processing audio. In other words, the last state /// `IAudioProcessor::setActive()` has been called with. pub is_processing: AtomicBool, - /// The current bus configuration, modified through `IAudioProcessor::setBusArrangements()`. - pub current_bus_config: AtomicCell, + /// The current audio IO layout. Modified through `IAudioProcessor::setBusArrangements()` after + /// matching the proposed bus arrangement to one of the supported ones. The plugin's first audio + /// IO layout is chosen as the default. Because of the way VST3 works it's not possible to + /// change the number of busses from that default, only the channel counts can change. + pub current_audio_io_layout: AtomicCell, /// The current buffer configuration, containing the sample rate and the maximum block size. /// Will be set in `IAudioProcessor::setupProcessing()`. pub current_buffer_config: AtomicCell>, @@ -305,12 +308,9 @@ impl WrapperInner

{ // will try using the plugin's default not yet initialized bus arrangement. Because of // that, we'll always initialize this configuration even before the host requests a // channel layout. - current_bus_config: AtomicCell::new(BusConfig { - num_input_channels: P::DEFAULT_INPUT_CHANNELS, - num_output_channels: P::DEFAULT_OUTPUT_CHANNELS, - aux_input_busses: P::DEFAULT_AUX_INPUTS.unwrap_or_default(), - aux_output_busses: P::DEFAULT_AUX_OUTPUTS.unwrap_or_default(), - }), + current_audio_io_layout: AtomicCell::new( + 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), @@ -517,12 +517,12 @@ impl WrapperInner

{ ); } - let bus_config = self.current_bus_config.load(); + let audio_io_layout = self.current_audio_io_layout.load(); if let Some(buffer_config) = self.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.make_init_context(); let mut plugin = self.plugin.lock(); - plugin.initialize(&bus_config, &buffer_config, &mut init_context); + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context); process_wrapper(|| plugin.reset()); } diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 5ba7503a..792afc0b 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -2,6 +2,7 @@ use std::borrow::Borrow; use std::cmp; use std::ffi::c_void; use std::mem::{self, MaybeUninit}; +use std::num::NonZeroU32; use std::ptr; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -26,9 +27,7 @@ use super::util::{ }; use super::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END}; use super::view::WrapperView; -use crate::audio_setup::{ - AuxiliaryBuffers, AuxiliaryIOConfig, BufferConfig, BusConfig, ProcessMode, -}; +use crate::audio_setup::{AuxiliaryBuffers, BufferConfig, ProcessMode}; use crate::buffer::Buffer; use crate::context::process::Transport; use crate::midi::sysex::SysExMessage; @@ -89,10 +88,7 @@ impl IComponent for Wrapper

{ type_: vst3_sys::vst::MediaType, dir: vst3_sys::vst::BusDirection, ) -> i32 { - // HACK: Bitwig will not call the process function at all if the plugin does not have any - // audio IO, so we'll add a zero channel output to work around this if that is the - // case - let no_main_audio_io = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_OUTPUT_CHANNELS == 0; + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); // A plugin has a main input and output bus if the default number of channels is non-zero, // and a plugin can also have auxiliary input and output busses @@ -100,22 +96,25 @@ impl IComponent for Wrapper

{ x if x == vst3_sys::vst::MediaTypes::kAudio as i32 && dir == vst3_sys::vst::BusDirections::kInput as i32 => { - let main_busses = if P::DEFAULT_INPUT_CHANNELS > 0 { 1 } else { 0 }; - let aux_busses = P::DEFAULT_AUX_INPUTS.unwrap_or_default().num_busses as i32; + let main_busses = if current_audio_io_layout.main_input_channels.is_some() { + 1 + } else { + 0 + }; + let aux_busses = current_audio_io_layout.aux_input_ports.len() as i32; main_busses + aux_busses } x if x == vst3_sys::vst::MediaTypes::kAudio as i32 && dir == vst3_sys::vst::BusDirections::kOutput as i32 => { - let main_busses = if P::DEFAULT_OUTPUT_CHANNELS > 0 { 1 } else { 0 }; - let aux_busses = P::DEFAULT_AUX_OUTPUTS.unwrap_or_default().num_busses as i32; + // HACK: Bitwig will not call the process function at all if the plugin does not have any + // audio IO, so we'll add a zero channel output to work around this if that is the + // case + let main_busses = 1; + let aux_busses = current_audio_io_layout.aux_output_ports.len() as i32; - if no_main_audio_io { - 1 - } else { - main_busses + aux_busses - } + main_busses + aux_busses } x if x == vst3_sys::vst::MediaTypes::kEvent as i32 && dir == vst3_sys::vst::BusDirections::kInput as i32 @@ -142,13 +141,13 @@ impl IComponent for Wrapper

{ ) -> tresult { check_null_ptr!(info); - // HACK: Bitwig will not call the process function at all if the plugin does not have any - // audio IO, so we'll add a zero channel output to work around this if that is the - // case - let no_main_audio_io = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_OUTPUT_CHANNELS == 0; + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); match (type_, dir, index) { - (t, _, _) if t == vst3_sys::vst::MediaTypes::kAudio as i32 => { + (t, d, _) + if t == vst3_sys::vst::MediaTypes::kAudio as i32 + && d == vst3_sys::vst::BusDirections::kInput as i32 => + { *info = mem::zeroed(); let info = &mut *info; @@ -156,87 +155,71 @@ impl IComponent for Wrapper

{ info.direction = dir; info.flags = vst3_sys::vst::BusFlags::kDefaultActive as u32; - // This is fun since main IO is optional - let bus_config = self.inner.current_bus_config.load(); - if dir == vst3_sys::vst::BusDirections::kInput as i32 { - let aux_inputs_only = - P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_AUX_INPUTS.is_some(); - let aux_input_start_idx = if aux_inputs_only { 0 } else { 1 }; - if !aux_inputs_only && index == 0 { - info.bus_type = vst3_sys::vst::BusTypes::kMain as i32; - info.channel_count = bus_config.num_input_channels as i32; - u16strlcpy(&mut info.name, P::PORT_NAMES.main_input.unwrap_or("Input")); + let has_main_input = current_audio_io_layout.main_input_channels.is_some(); + let aux_input_start_idx = if has_main_input { 1 } else { 0 }; + let aux_input_idx = (index - aux_input_start_idx).max(0) as usize; + if index == 0 && has_main_input { + info.bus_type = vst3_sys::vst::BusTypes::kMain as i32; + info.channel_count = + current_audio_io_layout.main_input_channels.unwrap().get() as i32; + u16strlcpy(&mut info.name, ¤t_audio_io_layout.main_input_name()); - kResultOk - } else if (aux_input_start_idx - ..(aux_input_start_idx + bus_config.aux_input_busses.num_busses as i32)) - .contains(&index) - { - info.bus_type = vst3_sys::vst::BusTypes::kAux as i32; - info.channel_count = bus_config.aux_input_busses.num_channels as i32; + kResultOk + } else if aux_input_idx < current_audio_io_layout.aux_input_ports.len() { + info.bus_type = vst3_sys::vst::BusTypes::kAux as i32; + info.channel_count = + current_audio_io_layout.aux_input_ports[aux_input_idx].get() as i32; + u16strlcpy( + &mut info.name, + ¤t_audio_io_layout + .aux_input_name(aux_input_idx) + .expect("Out of bounds auxiliary input port"), + ); - let aux_input_idx = index - aux_input_start_idx; - let custom_port_name = P::PORT_NAMES - .aux_inputs - .map(|names| names[aux_input_idx as usize]); - if bus_config.aux_input_busses.num_busses <= 1 { - u16strlcpy( - &mut info.name, - custom_port_name.unwrap_or("Sidechain Input"), - ); - } else { - u16strlcpy( - &mut info.name, - custom_port_name - .unwrap_or(&format!("Sidechain Input {}", aux_input_idx + 1)), - ); - } + kResultOk + } else { + kInvalidArgument + } + } + (t, d, _) + if t == vst3_sys::vst::MediaTypes::kAudio as i32 + && d == vst3_sys::vst::BusDirections::kOutput as i32 => + { + *info = mem::zeroed(); - kResultOk - } else { - kInvalidArgument - } - } else if dir == vst3_sys::vst::BusDirections::kOutput as i32 { - let aux_outputs_only = - P::DEFAULT_OUTPUT_CHANNELS == 0 && P::DEFAULT_AUX_OUTPUTS.is_some(); - let aux_output_start_idx = if aux_outputs_only { 0 } else { 1 }; - if (!aux_outputs_only || no_main_audio_io) && index == 0 { - info.bus_type = vst3_sys::vst::BusTypes::kMain as i32; - info.channel_count = bus_config.num_output_channels as i32; - u16strlcpy( - &mut info.name, - P::PORT_NAMES.main_output.unwrap_or("Output"), - ); + let info = &mut *info; + info.media_type = vst3_sys::vst::MediaTypes::kAudio as i32; + info.direction = dir; + info.flags = vst3_sys::vst::BusFlags::kDefaultActive as u32; - kResultOk - } else if (aux_output_start_idx - ..(aux_output_start_idx + bus_config.aux_output_busses.num_busses as i32)) - .contains(&index) - { - info.bus_type = vst3_sys::vst::BusTypes::kAux as i32; - info.channel_count = bus_config.aux_output_busses.num_channels as i32; + // HACK: Bitwig will not call the process function at all if the plugin does not have any + // audio IO, so we'll add a zero channel output to work around this if that is the + // case + let aux_output_start_idx = 1; + let aux_output_idx = (index - aux_output_start_idx).max(0) as usize; + if index == 0 { + info.bus_type = vst3_sys::vst::BusTypes::kMain as i32; + // NOTE: See above, this becomes a 0 channel output if the plugin doesn't have a + // main output + info.channel_count = current_audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as i32; + u16strlcpy(&mut info.name, ¤t_audio_io_layout.main_output_name()); - let aux_output_idx = index - aux_output_start_idx; - let custom_port_name = P::PORT_NAMES - .aux_outputs - .map(|names| names[aux_output_idx as usize]); - if bus_config.aux_output_busses.num_busses <= 1 { - u16strlcpy( - &mut info.name, - custom_port_name.unwrap_or("Auxiliary Output"), - ); - } else { - u16strlcpy( - &mut info.name, - custom_port_name - .unwrap_or(&format!("Auxiliary Output {}", aux_output_idx + 1)), - ); - } + kResultOk + } else if aux_output_idx < current_audio_io_layout.aux_output_ports.len() { + info.bus_type = vst3_sys::vst::BusTypes::kAux as i32; + info.channel_count = + current_audio_io_layout.aux_output_ports[aux_output_idx].get() as i32; + u16strlcpy( + &mut info.name, + ¤t_audio_io_layout + .aux_output_name(aux_output_idx) + .expect("Out of bounds auxiliary output port"), + ); - kResultOk - } else { - kInvalidArgument - } + kResultOk } else { kInvalidArgument } @@ -284,6 +267,8 @@ impl IComponent for Wrapper

{ ) -> tresult { check_null_ptr!(in_info, out_info); + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); + *out_info = mem::zeroed(); let in_info = &*in_info; @@ -292,8 +277,8 @@ impl IComponent for Wrapper

{ (t, 0) if t == vst3_sys::vst::MediaTypes::kAudio as i32 // We only have an IO pair when the plugin has both a main input and a main output - && P::DEFAULT_INPUT_CHANNELS > 0 - && P::DEFAULT_OUTPUT_CHANNELS > 0 => + && current_audio_io_layout.main_input_channels.is_some() + && current_audio_io_layout.main_output_channels.is_some() => { out_info.media_type = vst3_sys::vst::MediaTypes::kAudio as i32; out_info.bus_index = in_info.bus_index; @@ -321,21 +306,27 @@ impl IComponent for Wrapper

{ type_: vst3_sys::vst::MediaType, dir: vst3_sys::vst::BusDirection, index: i32, - _state: vst3_sys::base::TBool, + state: vst3_sys::base::TBool, ) -> tresult { - // HACK: Bitwig will not call the process function at all if the plugin does not have any - // audio IO, so we'll add a zero channel output to work around this if that is the - // case - let no_main_audio_io = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_OUTPUT_CHANNELS == 0; + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); + + // We don't support this, and we'll just pretend to support enabling busses (even though + // they're enabled by default) in case a host requires this + if state != 1 { + return kResultFalse; + } - // We don't need any special handling here match (type_, dir, index) { (t, d, _) if t == vst3_sys::vst::MediaTypes::kAudio as i32 && d == vst3_sys::vst::BusDirections::kInput as i32 => { - let main_busses = if P::DEFAULT_INPUT_CHANNELS > 0 { 1 } else { 0 }; - let aux_busses = P::DEFAULT_AUX_INPUTS.unwrap_or_default().num_busses as i32; + let main_busses = if current_audio_io_layout.main_input_channels.is_some() { + 1 + } else { + 0 + }; + let aux_busses = current_audio_io_layout.aux_input_ports.len() as i32; if (0..main_busses + aux_busses).contains(&index) { kResultOk @@ -347,12 +338,11 @@ impl IComponent for Wrapper

{ if t == vst3_sys::vst::MediaTypes::kAudio as i32 && d == vst3_sys::vst::BusDirections::kOutput as i32 => { - let main_busses = if P::DEFAULT_OUTPUT_CHANNELS > 0 || no_main_audio_io { - 1 - } else { - 0 - }; - let aux_busses = P::DEFAULT_AUX_OUTPUTS.unwrap_or_default().num_busses as i32; + // HACK: Bitwig will not call the process function at all if the plugin does not have any + // audio IO, so we'll add a zero channel output to work around this if that is the + // case + let main_busses = 1; + let aux_busses = current_audio_io_layout.aux_output_ports.len() as i32; if (0..main_busses + aux_busses).contains(&index) { kResultOk @@ -391,9 +381,9 @@ impl IComponent for Wrapper

{ // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.inner.make_init_context(); - let bus_config = self.inner.current_bus_config.load(); + let audio_io_layout = self.inner.current_audio_io_layout.load(); let mut plugin = self.inner.plugin.lock(); - if plugin.initialize(&bus_config, &buffer_config, &mut init_context) { + if plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context) { // NOTE: We don't call `Plugin::reset()` here. The call is done in `set_process()` // instead. Otherwise we would call the function twice, and `set_process()` needs // to be called after this function before the plugin may process audio again. @@ -404,8 +394,13 @@ impl IComponent for Wrapper

{ .output_buffer .borrow_mut() .set_slices(0, |output_slices| { - output_slices - .resize_with(bus_config.num_output_channels as usize, || &mut []); + output_slices.resize_with( + audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() as usize, + || &mut [], + ); // All slices must have the same length, so if the number of output // channels has changed since the last call then we should make sure to // clear any old (dangling) slices to be consistent @@ -416,45 +411,36 @@ impl IComponent for Wrapper

{ // sidechain inputs. The slices will be assigned in the process function as this // object may have been moved before then. let mut aux_input_storage = self.inner.aux_input_storage.borrow_mut(); - aux_input_storage - .resize_with(bus_config.aux_input_busses.num_busses as usize, Vec::new); - for bus_storage in aux_input_storage.iter_mut() { - bus_storage.resize_with( - bus_config.aux_input_busses.num_channels as usize, - Vec::new, - ); - for channel_storage in bus_storage { + let mut aux_input_buffers = self.inner.aux_input_buffers.borrow_mut(); + aux_input_storage.resize_with(audio_io_layout.aux_input_ports.len(), Vec::new); + aux_input_buffers + .resize_with(audio_io_layout.aux_input_ports.len(), Buffer::default); + for ((buffer_storage, buffer), num_channels) in aux_input_storage + .iter_mut() + .zip(aux_input_buffers.iter_mut()) + .zip(audio_io_layout.aux_input_ports.iter()) + { + buffer_storage.resize_with(num_channels.get() as usize, Vec::new); + for channel_storage in buffer_storage { channel_storage.resize(buffer_config.max_buffer_size as usize, 0.0); } - } - let mut aux_input_buffers = self.inner.aux_input_buffers.borrow_mut(); - aux_input_buffers.resize_with( - bus_config.aux_input_busses.num_busses as usize, - Buffer::default, - ); - for buffer in aux_input_buffers.iter_mut() { buffer.set_slices(0, |channel_slices| { - channel_slices.resize_with( - bus_config.aux_input_busses.num_channels as usize, - || &mut [], - ); + channel_slices.resize_with(num_channels.get() as usize, || &mut []); channel_slices.fill_with(|| &mut []); }); } // And the same thing for the output buffers let mut aux_output_buffers = self.inner.aux_output_buffers.borrow_mut(); - aux_output_buffers.resize_with( - bus_config.aux_output_busses.num_busses as usize, - Buffer::default, - ); - for buffer in aux_output_buffers.iter_mut() { + aux_output_buffers + .resize_with(audio_io_layout.aux_output_ports.len(), Buffer::default); + for (buffer, num_channels) in aux_output_buffers + .iter_mut() + .zip(audio_io_layout.aux_output_ports.iter()) + { buffer.set_slices(0, |channel_slices| { - channel_slices.resize_with( - bus_config.aux_output_busses.num_channels as usize, - || &mut [], - ); + channel_slices.resize_with(num_channels.get() as usize, || &mut []); channel_slices.fill_with(|| &mut []); }); } @@ -522,9 +508,9 @@ impl IComponent for Wrapper

{ if let Some(buffer_config) = self.inner.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.inner.make_init_context(); - let bus_config = self.inner.current_bus_config.load(); + let audio_io_layout = self.inner.current_audio_io_layout.load(); let mut plugin = self.inner.plugin.lock(); - plugin.initialize(&bus_config, &buffer_config, &mut init_context); + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context); // TODO: This also goes for the CLAP version, but should we call reset here? Won't the // host always restart playback? Check this with a couple of hosts and remove the // duplicate reset if it's not needed. @@ -789,89 +775,80 @@ impl IAudioProcessor for Wrapper

{ ) -> tresult { check_null_ptr!(inputs, outputs); - // HACK: Bitwig will not call the process function at all if the plugin does not have any - // audio IO, so we'll add a zero channel output to work around this if that is the - // case - let no_main_audio_io = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_OUTPUT_CHANNELS == 0; - // Why are these signed integers again? if num_ins < 0 || num_outs < 0 { return kInvalidArgument; } - // Every auxiliary input or output needs to have the same number of channels. In order to - // support plugins with no main IO but with auxiliary IO, we'll need to take that into - // account when asserting this. If that's the case, then the first bus for that direction - // will have been marked auxiliary. - let aux_inputs_only = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_AUX_INPUTS.is_some(); - let num_input_channels = if aux_inputs_only || num_ins < 1 { - 0 - } else { - (*inputs).count_ones() - }; + // NIH-plug no longer supports flexible IO layouts. Instead we'll try to find an audio IO + // layout that matches the host's requested layout. + let matching_layout = P::AUDIO_IO_LAYOUTS + .iter() + .find(|layout| { + // If the number of ports/busses doesn't match then we can immediately discard the + // layout. VST3 doesn't allow for optional switchable ports like CLAP does. Only the + // channel counts can change. + let num_layout_ins = if layout.main_input_channels.is_some() { + 1 + } else { + 0 + } + layout.aux_input_ports.len(); + // HACK: Bitwig will not call the process function at all if the plugin does not + // have any audio IO, so we'll add a zero channel output to work around this + // if that is the case + let num_layout_outs = 1 + layout.aux_input_ports.len(); + if num_ins as usize != num_layout_ins || num_outs as usize != num_layout_outs { + return false; + } - let aux_input_start_idx = if aux_inputs_only { 0 } else { 1 }; - let num_aux_input_busses = (num_ins as u32).saturating_sub(aux_input_start_idx); - let num_aux_input_channels = if num_aux_input_busses == 0 { - 0 - } else { - (*inputs.offset(aux_input_start_idx as isize)).count_ones() - }; - for i in 1..num_aux_input_busses { - if (*inputs.offset((aux_input_start_idx + i) as isize)).count_ones() - != num_aux_input_channels - { - nih_debug_assert_failure!("Mismatching auxiliary input bus channels set by host"); - return kResultFalse; + // NOTE: We completely ignore the speaker arrangements and only look at the channel + // counts here. This may cause issues at some point, but it works for now. + let has_main_input = layout.main_input_channels.is_some(); + let aux_input_start_idx = if has_main_input { 0 } else { 1 }; + if has_main_input + && (*inputs).count_ones() != layout.main_input_channels.unwrap().get() + { + return false; + } + for (aux_input_idx, channel_count) in layout.aux_input_ports.iter().enumerate() { + if (*inputs.add(aux_input_idx + aux_input_start_idx)).count_ones() + != channel_count.get() + { + return false; + } + } + + // See above, there is always a main output + let aux_output_start_idx = 1; + if (*outputs).count_ones() + != layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or_default() + { + return false; + } + for (aux_output_idx, channel_count) in layout.aux_output_ports.iter().enumerate() { + if (*outputs.add(aux_output_idx + aux_output_start_idx)).count_ones() + != channel_count.get() + { + return false; + } + } + + true + }) + .copied(); + + match matching_layout { + Some(layout) => { + // This layout is used from hereon onwards, at least until this function is called + // again + self.inner.current_audio_io_layout.store(layout); + + kResultOk } - } - - let aux_outputs_only = P::DEFAULT_OUTPUT_CHANNELS == 0 && P::DEFAULT_AUX_OUTPUTS.is_some(); - let num_output_channels = if (aux_outputs_only && !no_main_audio_io) || num_outs < 1 { - 0 - } else { - (*outputs).count_ones() - }; - - let aux_output_start_idx = if aux_outputs_only { 0 } else { 1 }; - let num_aux_output_busses = (num_outs as u32).saturating_sub(aux_output_start_idx); - let num_aux_output_channels = if num_aux_output_busses == 0 { - 0 - } else { - (*outputs.offset(aux_output_start_idx as isize)).count_ones() - }; - for i in 1..num_aux_output_busses { - if (*outputs.offset((aux_output_start_idx + i) as isize)).count_ones() - != num_aux_output_channels - { - nih_debug_assert_failure!("Mismatching auxiliary output bus channels set by host"); - return kResultFalse; - } - } - - let proposed_config = BusConfig { - num_input_channels, - num_output_channels, - aux_input_busses: AuxiliaryIOConfig { - num_busses: num_aux_input_busses, - num_channels: num_aux_input_channels, - }, - aux_output_busses: AuxiliaryIOConfig { - num_busses: num_aux_output_busses, - num_channels: num_aux_output_channels, - }, - }; - if self - .inner - .plugin - .lock() - .accepts_bus_config(&proposed_config) - { - self.inner.current_bus_config.store(proposed_config); - - kResultOk - } else { - kResultFalse + None => kResultFalse, } } @@ -883,11 +860,6 @@ impl IAudioProcessor for Wrapper

{ ) -> tresult { check_null_ptr!(arr); - // HACK: Bitwig will not call the process function at all if the plugin does not have any - // audio IO, so we'll add a zero channel output to work around this if that is the - // case - let no_main_audio_io = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_OUTPUT_CHANNELS == 0; - let channel_count_to_map = |count| match count { 0 => vst3_sys::vst::kEmpty, 1 => vst3_sys::vst::kMono, @@ -905,31 +877,28 @@ impl IAudioProcessor for Wrapper

{ } }; - let bus_config = self.inner.current_bus_config.load(); + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); let num_channels = if dir == vst3_sys::vst::BusDirections::kInput as i32 { - let aux_inputs_only = P::DEFAULT_INPUT_CHANNELS == 0 && P::DEFAULT_AUX_INPUTS.is_some(); - let aux_input_start_idx = if aux_inputs_only { 0 } else { 1 }; - if !aux_inputs_only && index == 0 { - bus_config.num_input_channels - } else if (aux_input_start_idx - ..(aux_input_start_idx + bus_config.aux_input_busses.num_busses as i32)) - .contains(&index) - { - bus_config.aux_input_busses.num_channels + let has_main_input = current_audio_io_layout.main_input_channels.is_some(); + let aux_input_start_idx = if has_main_input { 1 } else { 0 }; + let aux_input_idx = (index - aux_input_start_idx).max(0) as usize; + if index == 0 && has_main_input { + current_audio_io_layout.main_input_channels.unwrap().get() + } else if aux_input_idx < current_audio_io_layout.aux_input_ports.len() { + current_audio_io_layout.aux_input_ports[aux_input_idx].get() } else { return kInvalidArgument; } } else if dir == vst3_sys::vst::BusDirections::kOutput as i32 { - let aux_outputs_only = - P::DEFAULT_OUTPUT_CHANNELS == 0 && P::DEFAULT_AUX_OUTPUTS.is_some(); - let aux_output_start_idx = if aux_outputs_only { 0 } else { 1 }; - if (!aux_outputs_only || no_main_audio_io) && index == 0 { - bus_config.num_output_channels - } else if (aux_output_start_idx - ..(aux_output_start_idx + bus_config.aux_output_busses.num_busses as i32)) - .contains(&index) - { - bus_config.aux_output_busses.num_channels + // HACK: Bitwig will not call the process function at all if the plugin does not have any + // audio IO, so we'll add a zero channel output to work around this if that is the + // case + let aux_output_start_idx = 1; + let aux_output_idx = (index - aux_output_start_idx).max(0) as usize; + if index == 0 { + current_audio_io_layout.main_output_channels.unwrap().get() + } else if aux_output_idx < current_audio_io_layout.aux_output_ports.len() { + current_audio_io_layout.aux_output_ports[aux_output_idx].get() } else { return kInvalidArgument; } @@ -1052,13 +1021,12 @@ impl IAudioProcessor for Wrapper

{ // Before doing anything, clear out any auxiliary outputs since they may contain // uninitialized data when the host assumes that we'll always write something there - let current_bus_config = self.inner.current_bus_config.load(); - let has_main_input = current_bus_config.num_input_channels > 0; - // HACK: Bitwig requires VST3 plugins to always have a main output. We'll however still - // use this variable here to maintain consistency between the backends. - let has_main_output = true; + let current_audio_io_layout = self.inner.current_audio_io_layout.load(); + let has_main_input = current_audio_io_layout.main_input_channels.is_some(); if !data.outputs.is_null() { - for output_idx in if has_main_output { 1 } else { 0 }..data.num_outputs as isize { + // HACK: Bitwig requires VST3 plugins to always have a main output. We'll however + // still use this variable here to maintain consistency between the backends. + for output_idx in 1..data.num_outputs as isize { let host_output = data.outputs.offset(output_idx); if !(*host_output).buffers.is_null() { for channel_idx in 0..(*host_output).num_channels as isize { @@ -1454,11 +1422,9 @@ impl IAudioProcessor for Wrapper

{ // And the same thing for auxiliary output buffers let mut aux_output_buffers = self.inner.aux_output_buffers.borrow_mut(); for (auxiliary_output_idx, buffer) in aux_output_buffers.iter_mut().enumerate() { - let host_output_idx = if has_main_output { - auxiliary_output_idx as isize + 1 - } else { - auxiliary_output_idx as isize - }; + // HACK: Bitwig requires VST3 plugins to always have a main output. We'll however still + // use this variable here to maintain consistency between the backends. + let host_output_idx = auxiliary_output_idx as isize + 1; let host_output = data.outputs.offset(host_output_idx); if host_output_idx >= data.num_outputs as isize || data.outputs.is_null() @@ -1824,14 +1790,16 @@ impl IAudioProcessor for Wrapper

{ // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.inner.make_init_context(); - let bus_config = self.inner.current_bus_config.load(); + let audio_io_layout = self.inner.current_audio_io_layout.load(); let buffer_config = self.inner.current_buffer_config.load().unwrap(); let mut plugin = self.inner.plugin.lock(); // FIXME: This is obviously not realtime-safe, but loading presets without doing // this could lead to inconsistencies. It's the plugin's responsibility to // not perform any realtime-unsafe work when the initialize function is // called a second time if it supports runtime preset loading. - permit_alloc(|| plugin.initialize(&bus_config, &buffer_config, &mut init_context)); + permit_alloc(|| { + plugin.initialize(&audio_io_layout, &buffer_config, &mut init_context) + }); plugin.reset(); let task_posted = self.inner.schedule_gui(Task::ParameterValuesChanged);