1
0
Fork 0

Completely reword the audio IO layout system

Instead of a VST3-style polling function to test if a plugin supports a
certain layout, the plugin now explicitly enumerates the supported
layouts. This aligns better with non-VST3 plugin formats.
This commit is contained in:
Robbert van der Helm 2023-02-20 16:57:32 +01:00
parent 36c48157db
commit e8fd18ab80
32 changed files with 972 additions and 838 deletions

View file

@ -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

View file

@ -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 <package> <build_arguments>` command that automatically
detects which plugin targets your plugin exposes and creates the correct

View file

@ -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<Self>,
) -> 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"),
];
}

View file

@ -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<Self>,
) -> 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,
];
}

View file

@ -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<AuxiliaryIOConfig> = 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<Self>,
) -> bool {

View file

@ -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<Self>,
) -> 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);

View file

@ -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<AuxiliaryIOConfig> = None;
const DEFAULT_AUX_OUTPUTS: Option<AuxiliaryIOConfig> = 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

View file

@ -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<Self>,
) -> bool {

View file

@ -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<Self>,
) -> bool {

View file

@ -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<Self>,
) -> bool {

View file

@ -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;

View file

@ -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);

View file

@ -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<Self>,
) -> bool {

View file

@ -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<Self>,
) -> 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);

View file

@ -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;

View file

@ -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<Self>,
) -> 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 =

View file

@ -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<Self>,
) -> 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);

View file

@ -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<Self>,
) -> bool {

View file

@ -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<AuxiliaryIOConfig> = 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<Self>,
) -> 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"),

View file

@ -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<NonZeroU32>,
/// 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<NonZeroU32>,
/// 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
/// (<https://github.com/rust-lang/rust/issues/67441>).
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
/// (<https://github.com/rust-lang/rust/issues/67441>).
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`. (<https://github.com/rust-lang/rust/issues/67792>)
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<String> {
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<String> {
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`. (<https://github.com/rust-lang/rust/issues/67792>)
pub const fn const_default() -> Self {
Self {
layout: None,
main_input: None,
main_output: None,
aux_inputs: &[],
aux_outputs: &[],
}
}
}

View file

@ -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

View file

@ -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<AuxiliaryIOConfig> = 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<AuxiliaryIOConfig> = 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<Self>,
) -> bool {

View file

@ -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};

View file

@ -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<P: ClapPlugin> {
is_processing: AtomicBool,
/// The current IO configuration, modified through the `clap_plugin_audio_ports_config`
/// extension.
current_bus_config: AtomicCell<BusConfig>,
/// extension. Initialized to the plugin's first audio IO configuration.
current_audio_io_layout: AtomicCell<AudioIOLayout>,
/// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `clap_plugin::activate()`.
current_buffer_config: AtomicCell<Option<BufferConfig>>,
@ -181,14 +182,6 @@ pub struct Wrapper<P: ClapPlugin> {
host_callback: ClapPtr<clap_host>,
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<BusConfig>,
// The main `clap_plugin` vtable. A pointer to this `Wrapper<P>` instance is stored in the
// `plugin_data` field. This pointer is set after creating the `Arc<Wrapper<P>>`.
@ -538,52 +531,6 @@ impl<P: ClapPlugin> Wrapper<P> {
}
}
// 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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
);
}
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
.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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
.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<P: ClapPlugin> Wrapper<P> {
|| 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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
_ => 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<P: ClapPlugin> Wrapper<P> {
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, &current_audio_io_layout.main_input_name()),
(false, true) => strlcpy(&mut info.name, &current_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,
&current_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,
&current_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<P: ClapPlugin> Wrapper<P> {
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.

View file

@ -196,15 +196,6 @@ fn run_wrapper<P: Plugin, B: Backend<P>>(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");
}

View file

@ -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<P: Plugin>(config: WrapperConfig, cpal_host_id: cpal::HostId) -> Result<Self> {
let audio_io_layout = config.audio_io_layout_or_exit::<P>();
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| {

View file

@ -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<P: Plugin> Backend<P> for Dummy {
@ -33,10 +34,13 @@ impl<P: Plugin> Backend<P> 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<P: Plugin> Backend<P> for Dummy {
impl Dummy {
pub fn new<P: Plugin>(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::<P>(),
config,
}
}

View file

@ -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<P: Plugin>(config: WrapperConfig) -> Result<Self> {
let audio_io_layout = config.audio_io_layout_or_exit::<P>();
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)?);
}

View file

@ -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<String>,
// 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<u32>,
/// The number of output channels.
#[clap(value_parser, short = 'o', long)]
pub output_channels: Option<u32>,
/// 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<String>,
/// 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<P: Plugin>(&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::<usize>() {
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(),
}
}
}

View file

@ -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<P: Plugin, B: Backend<P>> {
param_id_to_ptr: HashMap<String, ParamPtr>,
/// 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<P: Plugin> {
/// 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<P: Plugin, B: Backend<P>> Wrapper<P, B> {
/// 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<Arc<Self>, 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::<P>();
let plugin = P::default();
let task_executor = Mutex::new(plugin.task_executor());
let params = plugin.params();
@ -214,11 +214,11 @@ impl<P: Plugin, B: Backend<P>> Wrapper<P, B> {
}
// 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<P: Plugin, B: Backend<P>> Wrapper<P, B> {
.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<P: Plugin, B: Backend<P>> Wrapper<P, B> {
})
.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<P: Plugin, B: Backend<P>> Wrapper<P, B> {
// runtime preset loading.
permit_alloc(|| {
plugin.initialize(
&self.bus_config,
&self.audio_io_layout,
&self.buffer_config,
&mut self.make_init_context(),
)

View file

@ -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<P: Vst3Plugin> {
/// 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<BusConfig>,
/// 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<AudioIOLayout>,
/// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `IAudioProcessor::setupProcessing()`.
pub current_buffer_config: AtomicCell<Option<BufferConfig>>,
@ -305,12 +308,9 @@ impl<P: Vst3Plugin> WrapperInner<P> {
// 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<P: Vst3Plugin> WrapperInner<P> {
);
}
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());
}

View file

@ -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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IComponent for Wrapper<P> {
) -> 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<P: Vst3Plugin> IComponent for Wrapper<P> {
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, &current_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,
&current_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, &current_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,
&current_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<P: Vst3Plugin> IComponent for Wrapper<P> {
) -> 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<P: Vst3Plugin> IComponent for Wrapper<P> {
(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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IComponent for Wrapper<P> {
// 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<P: Vst3Plugin> IComponent for Wrapper<P> {
.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<P: Vst3Plugin> IComponent for Wrapper<P> {
// 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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
) -> 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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
) -> 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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
}
};
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
// 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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
// 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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
// 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);