diff --git a/pedalboard/BufferUtils.h b/pedalboard/BufferUtils.h index 34536397..8d8c03f8 100644 --- a/pedalboard/BufferUtils.h +++ b/pedalboard/BufferUtils.h @@ -91,8 +91,8 @@ juce::AudioBuffer copyPyArrayIntoJuceBuffer( if (numChannels == 0) { throw std::runtime_error("No channels passed!"); - } else if (numChannels > 2) { - throw std::runtime_error("More than two channels received!"); + } else if (numChannels > 32) { + throw std::runtime_error("More than 32 channels received!"); } juce::AudioBuffer ioBuffer(numChannels, numSamples); @@ -173,8 +173,8 @@ const juce::AudioBuffer convertPyArrayIntoJuceBuffer( if (numChannels == 0) { throw std::runtime_error("No channels passed!"); - } else if (numChannels > 2) { - throw std::runtime_error("More than two channels received!"); + } else if (numChannels > 32) { + throw std::runtime_error("More than 32 channels received!"); } T **channelPointers = (T **)alloca(numChannels * sizeof(T *)); diff --git a/tests/plugins/effect/.gitignore b/tests/plugins/effect/.gitignore new file mode 100644 index 00000000..463aec32 --- /dev/null +++ b/tests/plugins/effect/.gitignore @@ -0,0 +1,2 @@ +*/Builds* +*/JuceLibraryCode* \ No newline at end of file diff --git a/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.cpp b/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.cpp new file mode 100644 index 00000000..06c3179f --- /dev/null +++ b/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.cpp @@ -0,0 +1,40 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin editor. + + ============================================================================== +*/ + +#include "PluginProcessor.h" +#include "PluginEditor.h" + +//============================================================================== +UnityGainNChannelAudioProcessorEditor::UnityGainNChannelAudioProcessorEditor (UnityGainNChannelAudioProcessor& p) + : AudioProcessorEditor (&p), audioProcessor (p) +{ + // Make sure that before the constructor has finished, you've set the + // editor's size to whatever you need it to be. + setSize (400, 300); +} + +UnityGainNChannelAudioProcessorEditor::~UnityGainNChannelAudioProcessorEditor() +{ +} + +//============================================================================== +void UnityGainNChannelAudioProcessorEditor::paint (juce::Graphics& g) +{ + // (Our component is opaque, so we must completely fill the background with a solid colour) + g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); + + g.setColour (juce::Colours::white); + g.setFont (15.0f); + g.drawFittedText ("Hello World!", getLocalBounds(), juce::Justification::centred, 1); +} + +void UnityGainNChannelAudioProcessorEditor::resized() +{ + // This is generally where you'll want to lay out the positions of any + // subcomponents in your editor.. +} diff --git a/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.h b/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.h new file mode 100644 index 00000000..0cded6a8 --- /dev/null +++ b/tests/plugins/effect/UnityGainNChannel/Source/PluginEditor.h @@ -0,0 +1,33 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin editor. + + ============================================================================== +*/ + +#pragma once + +#include +#include "PluginProcessor.h" + +//============================================================================== +/** +*/ +class UnityGainNChannelAudioProcessorEditor : public juce::AudioProcessorEditor +{ +public: + UnityGainNChannelAudioProcessorEditor (UnityGainNChannelAudioProcessor&); + ~UnityGainNChannelAudioProcessorEditor() override; + + //============================================================================== + void paint (juce::Graphics&) override; + void resized() override; + +private: + // This reference is provided as a quick way for your editor to + // access the processor object that created it. + UnityGainNChannelAudioProcessor& audioProcessor; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UnityGainNChannelAudioProcessorEditor) +}; diff --git a/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.cpp b/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.cpp new file mode 100644 index 00000000..03954cce --- /dev/null +++ b/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.cpp @@ -0,0 +1,168 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin processor. + + ============================================================================== +*/ + +#include "PluginProcessor.h" +#include "PluginEditor.h" + +//============================================================================== +UnityGainNChannelAudioProcessor::UnityGainNChannelAudioProcessor() +#ifndef JucePlugin_PreferredChannelConfigurations + : AudioProcessor (BusesProperties() + #if ! JucePlugin_IsMidiEffect + #if ! JucePlugin_IsSynth + .withInput ("Input", juce::AudioChannelSet::discreteChannels(32), true) + #endif + .withOutput ("Output", juce::AudioChannelSet::discreteChannels(32), true) + #endif + ) +#endif +{ +} + +UnityGainNChannelAudioProcessor::~UnityGainNChannelAudioProcessor() +{ +} + +//============================================================================== +const juce::String UnityGainNChannelAudioProcessor::getName() const +{ + return JucePlugin_Name; +} + +bool UnityGainNChannelAudioProcessor::acceptsMidi() const +{ + #if JucePlugin_WantsMidiInput + return true; + #else + return false; + #endif +} + +bool UnityGainNChannelAudioProcessor::producesMidi() const +{ + #if JucePlugin_ProducesMidiOutput + return true; + #else + return false; + #endif +} + +bool UnityGainNChannelAudioProcessor::isMidiEffect() const +{ + #if JucePlugin_IsMidiEffect + return true; + #else + return false; + #endif +} + +double UnityGainNChannelAudioProcessor::getTailLengthSeconds() const +{ + return 0.0; +} + +int UnityGainNChannelAudioProcessor::getNumPrograms() +{ + return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, + // so this should be at least 1, even if you're not really implementing programs. +} + +int UnityGainNChannelAudioProcessor::getCurrentProgram() +{ + return 0; +} + +void UnityGainNChannelAudioProcessor::setCurrentProgram (int index) +{ +} + +const juce::String UnityGainNChannelAudioProcessor::getProgramName (int index) +{ + return {}; +} + +void UnityGainNChannelAudioProcessor::changeProgramName (int index, const juce::String& newName) +{ +} + +//============================================================================== +void UnityGainNChannelAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + // Use this method as the place to do any pre-playback + // initialisation that you need.. +} + +void UnityGainNChannelAudioProcessor::releaseResources() +{ + // When playback stops, you can use this as an opportunity to free up any + // spare memory, etc. +} + +#ifndef JucePlugin_PreferredChannelConfigurations +bool UnityGainNChannelAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + #if JucePlugin_IsMidiEffect + juce::ignoreUnused (layouts); + return true; + #else + + // This checks if the input layout matches the output layout + #if ! JucePlugin_IsSynth + if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) + return false; + #endif + + return true; + #endif +} +#endif + +void UnityGainNChannelAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) +{ + juce::ScopedNoDenormals noDenormals; + + for (auto i = 0; i < buffer.getNumSamples(); ++i) + { + for (auto j = 0; j < buffer.getNumChannels(); ++j) + { + *buffer.getWritePointer(j, i) = *buffer.getReadPointer(j, i); + } + } +} + +//============================================================================== +bool UnityGainNChannelAudioProcessor::hasEditor() const +{ + return true; // (change this to false if you choose to not supply an editor) +} + +juce::AudioProcessorEditor* UnityGainNChannelAudioProcessor::createEditor() +{ + return new UnityGainNChannelAudioProcessorEditor (*this); +} + +//============================================================================== +void UnityGainNChannelAudioProcessor::getStateInformation (juce::MemoryBlock& destData) +{ + // You should use this method to store your parameters in the memory block. + // You could do that either as raw data, or use the XML or ValueTree classes + // as intermediaries to make it easy to save and load complex data. +} + +void UnityGainNChannelAudioProcessor::setStateInformation (const void* data, int sizeInBytes) +{ + // You should use this method to restore your parameters from this memory block, + // whose contents will have been created by the getStateInformation() call. +} + +//============================================================================== +// This creates new instances of the plugin.. +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new UnityGainNChannelAudioProcessor(); +} diff --git a/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.h b/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.h new file mode 100644 index 00000000..13f039c5 --- /dev/null +++ b/tests/plugins/effect/UnityGainNChannel/Source/PluginProcessor.h @@ -0,0 +1,62 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin processor. + + ============================================================================== +*/ + +#pragma once + +#include + +//============================================================================== +/** +*/ +class UnityGainNChannelAudioProcessor : public juce::AudioProcessor + #if JucePlugin_Enable_ARA + , public juce::AudioProcessorARAExtension + #endif +{ +public: + //============================================================================== + UnityGainNChannelAudioProcessor(); + ~UnityGainNChannelAudioProcessor() override; + + //============================================================================== + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + + #ifndef JucePlugin_PreferredChannelConfigurations + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; + #endif + + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + //============================================================================== + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override; + + //============================================================================== + const juce::String getName() const override; + + bool acceptsMidi() const override; + bool producesMidi() const override; + bool isMidiEffect() const override; + double getTailLengthSeconds() const override; + + //============================================================================== + int getNumPrograms() override; + int getCurrentProgram() override; + void setCurrentProgram (int index) override; + const juce::String getProgramName (int index) override; + void changeProgramName (int index, const juce::String& newName) override; + + //============================================================================== + void getStateInformation (juce::MemoryBlock& destData) override; + void setStateInformation (const void* data, int sizeInBytes) override; + +private: + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UnityGainNChannelAudioProcessor) +}; diff --git a/tests/plugins/effect/UnityGainNChannel/UnityGainNChannel.jucer b/tests/plugins/effect/UnityGainNChannel/UnityGainNChannel.jucer new file mode 100644 index 00000000..67bb75a8 --- /dev/null +++ b/tests/plugins/effect/UnityGainNChannel/UnityGainNChannel.jucer @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/plugins/effect/Windows/UnityGain32Channel.vst3 b/tests/plugins/effect/Windows/UnityGain32Channel.vst3 new file mode 100644 index 00000000..9510f803 Binary files /dev/null and b/tests/plugins/effect/Windows/UnityGain32Channel.vst3 differ diff --git a/tests/test_external_plugins.py b/tests/test_external_plugins.py index 88ae643f..43dcf3f7 100644 --- a/tests/test_external_plugins.py +++ b/tests/test_external_plugins.py @@ -663,6 +663,10 @@ def test_string_parameter_valdation(plugin_filename: str, parameter_name: str): @pytest.mark.parametrize("plugin_filename", AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT) def test_plugin_parameters_persist_between_calls(plugin_filename: str): + + if plugin_filename == "UnityGain32Channel.vst3": + pytest.skip("No parameters in this plugin.") + plugin = load_test_plugin(plugin_filename) sr = 44100 noise = np.random.rand(2, sr) @@ -728,6 +732,10 @@ def test_plugin_state_cleared_between_invocations_by_default(plugin_filename: st @pytest.mark.parametrize("plugin_filename", AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT) def test_plugin_state_not_cleared_between_invocations_if_reset_is_false(plugin_filename: str): + + if plugin_filename == "UnityGain32Channel.vst3": + pytest.skip("No internal state stored in this plugin.") + plugin = load_test_plugin(plugin_filename, disable_caching=True) sr = 44100 @@ -922,3 +930,21 @@ def test_get_plugin_names_from_container(plugin_filename: str): raise ValueError("Plugin does not seem to be a .vst3 or .component.") assert len(names) > 1 + + +@pytest.mark.parametrize("plugin_filename", ["UnityGain32Channel.vst3"]) +def test_poly_channel_plugins(plugin_filename): + + plugin_path = find_plugin_path(plugin_filename) + plugin = pedalboard.load_plugin(plugin_path) + + sr = 44100 + noise = np.random.rand(sr, 32) + + outputs = plugin( + input_array=noise, + sample_rate=sr) + + assert outputs.shape == noise.shape + + assert np.all(np.isclose(noise, outputs)) diff --git a/tests/test_memory_leaks.py b/tests/test_memory_leaks.py index adfd57bf..35728702 100644 --- a/tests/test_memory_leaks.py +++ b/tests/test_memory_leaks.py @@ -23,9 +23,12 @@ def get_first_parameter_from(plugin): - param_name, param = next( - iter([(name, param) for name, param in plugin.parameters.items() if param.type is float]) - ) + try: + param_name, param = next( + iter([(name, param) for name, param in plugin.parameters.items() if param.type is float]) + ) + except StopIteration: + pytest.skip("No float parameter in plugin.") value = getattr(plugin, param_name) return param_name, param, value @@ -33,6 +36,7 @@ def get_first_parameter_from(plugin): @pytest.mark.parametrize("plugin_path", AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT) def test_plugin_can_be_garbage_collected(plugin_path: str): # Load a VST3 or Audio Unit plugin from disk: + plugin = load_test_plugin(plugin_path, disable_caching=True) _plugin_ref = weakref.ref(plugin)