From 5455063b64f387ad9068a77ee716c0a5a1b2bd67 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sun, 23 Jan 2022 00:56:23 -0500 Subject: [PATCH 01/13] Add basic plugin chaining and mixing (parallel chains) functionality. --- pedalboard/ChainPlugin.h | 78 +++++++++++ pedalboard/MixPlugin.h | 155 ++++++++++++++++++++++ pedalboard/pedalboard.py | 5 +- pedalboard/process.h | 232 +++++++++++++++++---------------- pedalboard/python_bindings.cpp | 4 + 5 files changed, 364 insertions(+), 110 deletions(-) create mode 100644 pedalboard/ChainPlugin.h create mode 100644 pedalboard/MixPlugin.h diff --git a/pedalboard/ChainPlugin.h b/pedalboard/ChainPlugin.h new file mode 100644 index 00000000..69e7bba7 --- /dev/null +++ b/pedalboard/ChainPlugin.h @@ -0,0 +1,78 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "JuceHeader.h" +#include + +#include "Plugin.h" +#include "process.h" + +namespace Pedalboard { +/** + * A class that allows nesting a pedalboard within another. + */ +class ChainPlugin : public Plugin { +public: + ChainPlugin(std::vector chain) : chain(chain) {} + virtual ~ChainPlugin(){}; + + virtual void prepare(const juce::dsp::ProcessSpec &spec) { + for (auto plugin : chain) plugin->prepare(spec); + lastSpec = spec; + } + + virtual int + process(const juce::dsp::ProcessContextReplacing &context) { + // assuming process context replacing + auto ioBlock = context.getOutputBlock(); + + float *channels[8] = {}; + for (int i = 0; i < ioBlock.getNumChannels(); i++) { + channels[i] = ioBlock.getChannelPointer(i); + } + + juce::AudioBuffer ioBuffer(channels, ioBlock.getNumChannels(), ioBlock.getNumSamples()); + return ::Pedalboard::process(ioBuffer, lastSpec, chain, false); + } + + virtual void reset() { + for (auto plugin : chain) plugin->reset(); + } + + virtual int getLatencyHint() { + int hint = 0; + for (auto plugin : chain) hint += plugin->getLatencyHint(); + return hint; + } + +protected: + std::vector chain; +}; + +inline void init_chain(py::module &m) { + py::class_( + m, "ChainPlugin", + "Run a pedalboard within a plugin. Meta.") + .def(py::init([](std::vector plugins) { + return new ChainPlugin(plugins); + }), + py::arg("plugins"), py::keep_alive<1, 2>()); +} + +} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/MixPlugin.h b/pedalboard/MixPlugin.h new file mode 100644 index 00000000..dfb33a9a --- /dev/null +++ b/pedalboard/MixPlugin.h @@ -0,0 +1,155 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "JuceHeader.h" +#include + +#include "Plugin.h" + +namespace Pedalboard { +/** + * A class that allows parallel processing of two separate plugin chains. + */ +class MixPlugin : public Plugin { +public: + MixPlugin(std::vector plugins) : plugins(plugins), pluginBuffers(plugins.size()), samplesAvailablePerPlugin(plugins.size()) {} + virtual ~MixPlugin(){}; + + virtual void prepare(const juce::dsp::ProcessSpec &spec) { + for (auto plugin : plugins) plugin->prepare(spec); + + int maximumBufferSize = getLatencyHint() + spec.maximumBlockSize; + for (auto &buffer : pluginBuffers) buffer.setSize(spec.numChannels, maximumBufferSize); + for (int i = 0; i < samplesAvailablePerPlugin.size(); i++) samplesAvailablePerPlugin[i] = 0; + lastSpec = spec; + } + + virtual int + process(const juce::dsp::ProcessContextReplacing &context) { + auto ioBlock = context.getOutputBlock(); + + for (int i = 0; i < plugins.size(); i++) { + Plugin *plugin = plugins[i]; + juce::AudioBuffer &buffer = pluginBuffers[i]; + + int startInBuffer = samplesAvailablePerPlugin[i]; + int endInBuffer = startInBuffer + ioBlock.getNumSamples(); + // If we don't have enough space, reallocate. (Reluctantly. This is the "audio thread!") + if (endInBuffer > buffer.getNumSamples()) { + printf("Want to write from %d to %d, but buffer is only %d long - reallocating.", startInBuffer, endInBuffer, buffer.getNumSamples()); + buffer.setSize(buffer.getNumChannels(), endInBuffer); + } + + // Copy the audio input into each of these buffers: + context.getInputBlock().copyTo(buffer, 0, samplesAvailablePerPlugin[i]); + + float *channelPointers[8] = {}; + for (int c = 0; c < buffer.getNumChannels(); c++) { + channelPointers[c] = buffer.getWritePointer(c, startInBuffer); + } + + auto subBlock = juce::dsp::AudioBlock( + channelPointers, + buffer.getNumChannels(), + ioBlock.getNumSamples() + ); + + juce::dsp::ProcessContextReplacing subContext(subBlock); + int samplesRendered = plugin->process(subContext); + samplesAvailablePerPlugin[i] += samplesRendered; + + if (samplesRendered < subBlock.getNumSamples()) { + // Left-align the results in the buffer, as we'll need all + // of the plugins' outputs to be aligned: + for (int c = 0; c < pluginBuffers[i].getNumChannels(); c++) { + std::memmove( + channelPointers[c], + channelPointers[c] + (subBlock.getNumSamples() - samplesRendered), + sizeof(float) * samplesRendered + ); + } + } + } + + // Figure out the maximum number of samples we can return, + // which is the min across all buffers: + int maxSamplesAvailable = ioBlock.getNumSamples(); + for (int i = 0; i < plugins.size(); i++) { + maxSamplesAvailable = std::min(samplesAvailablePerPlugin[i], maxSamplesAvailable); + } + + // Now that each plugin has rendered into its own buffer, mix the output: + ioBlock.clear(); + if (maxSamplesAvailable) { + int leftEdge = ioBlock.getNumSamples() - maxSamplesAvailable; + auto subBlock = ioBlock.getSubBlock(leftEdge); + + for (auto &pluginBuffer : pluginBuffers) { + // Right-align exactly `maxSamplesAvailable` samples from each buffer: + juce::dsp::AudioBlock pluginBufferAsBlock(pluginBuffer); + + // Add as many samples as we can (which is maxSamplesAvailable, + // because subBlock is only that size): + subBlock.add(pluginBufferAsBlock); + } + } + + // Delete the samples we just returned from each buffer and shift the remaining content left: + for (int i = 0; i < plugins.size(); i++) { + int samplesToDelete = maxSamplesAvailable; + int samplesRemaining = pluginBuffers[i].getNumSamples() - samplesAvailablePerPlugin[i]; + for (int c = 0; c < pluginBuffers[i].getNumChannels(); c++) { + float *channelBuffer = pluginBuffers[i].getWritePointer(c); + + // Shift the remaining samples to the start of the buffer: + std::memmove(channelBuffer, channelBuffer + samplesToDelete, sizeof(float) * samplesRemaining); + } + samplesAvailablePerPlugin[i] -= samplesToDelete; + } + + return maxSamplesAvailable; + } + + virtual void reset() { + for (auto plugin : plugins) plugin->reset(); + for (auto buffer : pluginBuffers) buffer.clear(); + } + + virtual int getLatencyHint() { + int maxHint = 0; + for (auto plugin : plugins) maxHint = std::max(maxHint, plugin->getLatencyHint()); + return maxHint; + } + +protected: + std::vector> pluginBuffers; + std::vector plugins; + std::vector samplesAvailablePerPlugin; +}; + +inline void init_mix(py::module &m) { + py::class_( + m, "MixPlugin", + "Mix multiple plugins' output together, processing each in parallel.") + .def(py::init([](std::vector plugins) { + return new MixPlugin(plugins); + }), + py::arg("plugins"), py::keep_alive<1, 2>()); +} +} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index d7b792ee..f2c2628d 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -31,7 +31,10 @@ class Pedalboard(collections.abc.MutableSequence): A container for a chain of plugins, to use for processing audio. """ - def __init__(self, plugins: List[Optional[Plugin]], sample_rate: Optional[float] = None): + def __init__(self, plugins: List[Optional[Plugin]] = None, sample_rate: Optional[float] = None): + if not plugins: + plugins = [] + for plugin in plugins: if plugin is not None: if not isinstance(plugin, Plugin): diff --git a/pedalboard/process.h b/pedalboard/process.h index 58731dfc..dd7d1855 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -203,6 +203,124 @@ py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, return outputArray; } +inline int process(juce::AudioBuffer &ioBuffer, + juce::dsp::ProcessSpec spec, + const std::vector &plugins, + bool isProbablyLastProcessCall) { + int totalOutputLatencySamples = 0; + int expectedOutputLatency = 0; + + for (auto *plugin : plugins) { + if (plugin == nullptr) + continue; + expectedOutputLatency += plugin->getLatencyHint(); + } + + int intendedOutputBufferSize = ioBuffer.getNumSamples(); + + if (expectedOutputLatency > 0 && isProbablyLastProcessCall) { + // This is a hint - it's possible that the plugin(s) latency values + // will change and we'll have to reallocate again later on. + ioBuffer.setSize(ioBuffer.getNumChannels(), + ioBuffer.getNumSamples() + expectedOutputLatency, + /* keepExistingContent= */ true, + /* clearExtraSpace= */ true); + } + + // Actually run the plugins over the ioBuffer, in small chunks, to minimize + // memory usage: + int startOfOutputInBuffer = 0; + int lastSampleInBuffer = 0; + + for (auto *plugin : plugins) { + if (plugin == nullptr) + continue; + + int pluginSamplesReceived = 0; + + for (unsigned int blockStart = startOfOutputInBuffer; + blockStart < (unsigned int)intendedOutputBufferSize; + blockStart += spec.maximumBlockSize) { + unsigned int blockEnd = + std::min(blockStart + spec.maximumBlockSize, + static_cast(intendedOutputBufferSize)); + unsigned int blockSize = blockEnd - blockStart; + + auto ioBlock = juce::dsp::AudioBlock( + ioBuffer.getArrayOfWritePointers(), ioBuffer.getNumChannels(), + blockStart, blockSize); + juce::dsp::ProcessContextReplacing context(ioBlock); + + int outputSamples = plugin->process(context); + if (outputSamples < 0) { + throw std::runtime_error( + "A plugin returned a negative number of output samples! " + "This is an internal Pedalboard error and should be reported."); + } + pluginSamplesReceived += outputSamples; + + int missingSamples = blockSize - outputSamples; + if (missingSamples > 0 && pluginSamplesReceived > 0) { + // This can only happen if the plugin we're using is returning us more + // than one chunk of audio that's not completely full, which can + // happen sometimes. In this case, we would end up with gaps in the + // audio output: + // empty empty full part + // [______|______|AAAAAA|__BBBB] + // end of most recently rendered block-->-^ + // We need to consolidate those gaps by moving them forward in time. + // To do so, we take the section from the earliest known output to the + // start of this block, and right-align it to the left side of the + // current block's content: + // empty empty part full + // [______|______|__AAAA|AABBBB] + // end of most recently rendered block-->-^ + for (int c = 0; c < ioBuffer.getNumChannels(); c++) { + // Only move the samples received before this latest block was + // rendered, as audio is right-aligned within blocks by convention. + int samplesToMove = pluginSamplesReceived - outputSamples; + float *outputStart = + ioBuffer.getWritePointer(c) + totalOutputLatencySamples; + float *expectedOutputEnd = + ioBuffer.getWritePointer(c) + blockEnd - outputSamples; + float *expectedOutputStart = expectedOutputEnd - samplesToMove; + + std::memmove((char *)expectedOutputStart, (char *)outputStart, + sizeof(float) * samplesToMove); + } + } + + lastSampleInBuffer = + std::max(lastSampleInBuffer, (int)(blockStart + outputSamples)); + startOfOutputInBuffer += missingSamples; + totalOutputLatencySamples += missingSamples; + + if (missingSamples && isProbablyLastProcessCall) { + // Resize the IO buffer to give us a bit more room + // on the end, so we can continue to write delayed output. + // Only do this if we think this is the last time process is called. + intendedOutputBufferSize += missingSamples; + + // If we need to reallocate, then we reallocate. + if (intendedOutputBufferSize > ioBuffer.getNumSamples()) { + ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, + /* keepExistingContent= */ true, + /* clearExtraSpace= */ true); + } + } + } + } + + // Trim the output buffer down to size; this operation should be + // allocation-free. + jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); + ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, + /* keepExistingContent= */ true, + /* clearExtraSpace= */ true, + /* avoidReallocating= */ true); + return intendedOutputBufferSize - totalOutputLatencySamples; +} + /** * Process a given audio buffer through a list of * Pedalboard plugins at a given sample rate. @@ -215,7 +333,7 @@ process(const py::array_t inputArray, unsigned int bufferSize, bool reset) { const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); juce::AudioBuffer ioBuffer = copyPyArrayIntoJuceBuffer(inputArray); - int totalOutputLatencySamples = 0; + int totalOutputLatencySamples; { py::gil_scoped_release release; @@ -274,123 +392,19 @@ process(const py::array_t inputArray, spec.maximumBlockSize = static_cast(bufferSize); spec.numChannels = static_cast(ioBuffer.getNumChannels()); - int expectedOutputLatency = 0; - for (auto *plugin : plugins) { if (plugin == nullptr) continue; plugin->prepare(spec); - expectedOutputLatency += plugin->getLatencyHint(); } - int intendedOutputBufferSize = ioBuffer.getNumSamples(); - - if (expectedOutputLatency > 0) { - // This is a hint - it's possible that the plugin(s) latency values - // will change and we'll have to reallocate again later on. - ioBuffer.setSize(ioBuffer.getNumChannels(), - ioBuffer.getNumSamples() + expectedOutputLatency, - /* keepExistingContent= */ true, - /* clearExtraSpace= */ true); - } - - // Actually run the plugins over the ioBuffer, in small chunks, to minimize - // memory usage: - int startOfOutputInBuffer = 0; - int lastSampleInBuffer = 0; - - for (auto *plugin : plugins) { - if (plugin == nullptr) - continue; - - int pluginSamplesReceived = 0; - - for (unsigned int blockStart = startOfOutputInBuffer; - blockStart < (unsigned int)intendedOutputBufferSize; - blockStart += bufferSize) { - unsigned int blockEnd = - std::min(blockStart + bufferSize, - static_cast(intendedOutputBufferSize)); - unsigned int blockSize = blockEnd - blockStart; - - auto ioBlock = juce::dsp::AudioBlock( - ioBuffer.getArrayOfWritePointers(), ioBuffer.getNumChannels(), - blockStart, blockSize); - juce::dsp::ProcessContextReplacing context(ioBlock); - - int outputSamples = plugin->process(context); - if (outputSamples < 0) { - throw std::runtime_error( - "A plugin returned a negative number of output samples! " - "This is an internal Pedalboard error and should be reported."); - } - pluginSamplesReceived += outputSamples; - - int missingSamples = blockSize - outputSamples; - if (missingSamples > 0 && pluginSamplesReceived > 0) { - // This can only happen if the plugin we're using is returning us more - // than one chunk of audio that's not completely full, which can - // happen sometimes. In this case, we would end up with gaps in the - // audio output: - // empty empty full part - // [______|______|AAAAAA|__BBBB] - // end of most recently rendered block-->-^ - // We need to consolidate those gaps by moving them forward in time. - // To do so, we take the section from the earliest known output to the - // start of this block, and right-align it to the left side of the - // current block's content: - // empty empty part full - // [______|______|__AAAA|AABBBB] - // end of most recently rendered block-->-^ - for (int c = 0; c < ioBuffer.getNumChannels(); c++) { - // Only move the samples received before this latest block was - // rendered, as audio is right-aligned within blocks by convention. - int samplesToMove = pluginSamplesReceived - outputSamples; - float *outputStart = - ioBuffer.getWritePointer(c) + totalOutputLatencySamples; - float *expectedOutputEnd = - ioBuffer.getWritePointer(c) + blockEnd - outputSamples; - float *expectedOutputStart = expectedOutputEnd - samplesToMove; - - std::memmove((char *)expectedOutputStart, (char *)outputStart, - sizeof(float) * samplesToMove); - } - } - - lastSampleInBuffer = - std::max(lastSampleInBuffer, (int)(blockStart + outputSamples)); - startOfOutputInBuffer += missingSamples; - totalOutputLatencySamples += missingSamples; - - if (missingSamples && reset) { - // Resize the IO buffer to give us a bit more room - // on the end, so we can continue to write delayed output. - // Only do this if reset=True was passed, as we can use that - // as a proxy for the user's intent to call `process` again. - intendedOutputBufferSize += missingSamples; - - // If we need to reallocate, then we reallocate. - if (intendedOutputBufferSize > ioBuffer.getNumSamples()) { - ioBuffer.setSize(ioBuffer.getNumChannels(), - intendedOutputBufferSize, - /* keepExistingContent= */ true, - /* clearExtraSpace= */ true); - } - } - } - } - - // Trim the output buffer down to size; this operation should be - // allocation-free. - jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); - ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, - /* keepExistingContent= */ true, - /* clearExtraSpace= */ true, - /* avoidReallocating= */ true); + // Actually run the process method of all plugins. + int samplesReturned = process(ioBuffer, spec, plugins, reset); + totalOutputLatencySamples = ioBuffer.getNumSamples() - samplesReturned; } return copyJuceBufferIntoPyArray(ioBuffer, inputChannelLayout, totalOutputLatencySamples, inputArray.request().ndim); -}; +} } // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index 7a16bd0f..270d2681 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -29,9 +29,11 @@ namespace py = pybind11; #include "ExternalPlugin.h" #include "JucePlugin.h" +#include "MixPlugin.h" #include "Plugin.h" #include "process.h" +#include "ChainPlugin.h" #include "plugins/AddLatency.h" #include "plugins/Chorus.h" #include "plugins/Compressor.h" @@ -136,6 +138,7 @@ PYBIND11_MODULE(pedalboard_native, m) { py::arg("reset") = true); plugin.attr("__call__") = plugin.attr("process"); + init_chain(m); init_chorus(m); init_compressor(m); init_convolution(m); @@ -148,6 +151,7 @@ PYBIND11_MODULE(pedalboard_native, m) { init_limiter(m); init_lowpass(m); init_mp3_compressor(m); + init_mix(m); init_noisegate(m); init_phaser(m); init_pitch_shift(m); From 4a7e8110c5fa150587d88c36977ea419700c09a2 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sun, 23 Jan 2022 15:32:51 -0500 Subject: [PATCH 02/13] Working Chain/Mix implementation with correct latency compensation. Could be cleaner, though. --- pedalboard/ChainPlugin.h | 78 -------- pedalboard/Plugin.h | 6 + pedalboard/__init__.py | 1 + pedalboard/pedalboard.py | 104 ++++++++--- pedalboard/plugins/Chain.h | 106 +++++++++++ pedalboard/{MixPlugin.h => plugins/Mix.h} | 120 ++++++++----- pedalboard/process.h | 15 +- pedalboard/python_bindings.cpp | 12 +- tests/test_mix.py | 206 ++++++++++++++++++++++ 9 files changed, 498 insertions(+), 150 deletions(-) delete mode 100644 pedalboard/ChainPlugin.h create mode 100644 pedalboard/plugins/Chain.h rename pedalboard/{MixPlugin.h => plugins/Mix.h} (50%) create mode 100644 tests/test_mix.py diff --git a/pedalboard/ChainPlugin.h b/pedalboard/ChainPlugin.h deleted file mode 100644 index 69e7bba7..00000000 --- a/pedalboard/ChainPlugin.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * pedalboard - * Copyright 2021 Spotify AB - * - * Licensed under the GNU Public License, Version 3.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.gnu.org/licenses/gpl-3.0.html - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "JuceHeader.h" -#include - -#include "Plugin.h" -#include "process.h" - -namespace Pedalboard { -/** - * A class that allows nesting a pedalboard within another. - */ -class ChainPlugin : public Plugin { -public: - ChainPlugin(std::vector chain) : chain(chain) {} - virtual ~ChainPlugin(){}; - - virtual void prepare(const juce::dsp::ProcessSpec &spec) { - for (auto plugin : chain) plugin->prepare(spec); - lastSpec = spec; - } - - virtual int - process(const juce::dsp::ProcessContextReplacing &context) { - // assuming process context replacing - auto ioBlock = context.getOutputBlock(); - - float *channels[8] = {}; - for (int i = 0; i < ioBlock.getNumChannels(); i++) { - channels[i] = ioBlock.getChannelPointer(i); - } - - juce::AudioBuffer ioBuffer(channels, ioBlock.getNumChannels(), ioBlock.getNumSamples()); - return ::Pedalboard::process(ioBuffer, lastSpec, chain, false); - } - - virtual void reset() { - for (auto plugin : chain) plugin->reset(); - } - - virtual int getLatencyHint() { - int hint = 0; - for (auto plugin : chain) hint += plugin->getLatencyHint(); - return hint; - } - -protected: - std::vector chain; -}; - -inline void init_chain(py::module &m) { - py::class_( - m, "ChainPlugin", - "Run a pedalboard within a plugin. Meta.") - .def(py::init([](std::vector plugins) { - return new ChainPlugin(plugins); - }), - py::arg("plugins"), py::keep_alive<1, 2>()); -} - -} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/Plugin.h b/pedalboard/Plugin.h index 3dff016f..5b3d2069 100644 --- a/pedalboard/Plugin.h +++ b/pedalboard/Plugin.h @@ -70,6 +70,12 @@ class Plugin { */ virtual int getLatencyHint() { return 0; } + /* + * Some plugins can host/contain other plugins (i.e.: Mix, Chain). + * This method can be used to traverse the plugin tree when necessary. + */ + virtual std::vector getNestedPlugins() const { return {}; } + // A mutex to gate access to this plugin, as its internals may not be // thread-safe. Note: use std::lock or std::scoped_lock when locking multiple // plugins to avoid deadlocking. diff --git a/pedalboard/__init__.py b/pedalboard/__init__.py index 36404dc5..598a67bf 100644 --- a/pedalboard/__init__.py +++ b/pedalboard/__init__.py @@ -16,6 +16,7 @@ from pedalboard_native import * # noqa: F403, F401 +from pedalboard_native.utils import * # noqa: F401 from .pedalboard import Pedalboard, AVAILABLE_PLUGIN_CLASSES, load_plugin # noqa: F401 from .version import __version__ # noqa: F401 diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index f2c2628d..263589e3 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -14,16 +14,85 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import collections import platform import weakref from functools import update_wrapper from contextlib import contextmanager -from typing import List, Optional, Dict, Union, Tuple, Iterable +from typing import List, Optional, Dict, Union, Tuple, Set, Iterable, Union import numpy as np from pedalboard_native import Plugin, process, _AudioProcessorParameter +from pedalboard_native.utils import Mix, Chain + + +# A concrete type for all ways to define a Pedalboard with zero or more plugins: +PedalboardDefinition = Union[ + # The standard Pedalboard is a list of plugins, + # although nested lists will be unpacked. + List[Union[Plugin, 'PedalboardDefinition']], + # Tuples can be used in place of lists when necessary, + # if putting a chain inside of a mix plugin. + Tuple[Union[Plugin, 'PedalboardDefinition']], + # Pedalboards can be nested, and the contained + # pedalboard's plugins will be treated as a list: + 'Pedalboard', + # Passing a set of plugins will result in them being processed in parallel + # (i.e: they will all accept the same input and their outputs will be mixed) + Set[Union[Plugin, 'PedalboardDefinition']], +] + + +def _coalesce_plugin_definitions( + _input: Optional[PedalboardDefinition], level: int = 0 +) -> List[Plugin]: + """ + Given a PedalboardDefinition, return a concrete list of plugins that can be executed. + Basically: remove the syntactic sugar and add the appropriate Mix() and Chain() plugins. + """ + if isinstance(_input, Plugin): + return _input + elif hasattr(_input, "plugins"): + return _coalesce_plugin_definitions(_input.plugins, level + 1) + elif isinstance(_input, List) or isinstance(_input, Tuple): + plugins = [ + _coalesce_plugin_definitions(element, level + 1) + for element in _input + if element is not None + ] + if level > 0: + return Chain(plugins) + else: + return plugins + elif isinstance(_input, Set): + return Mix( + [ + _coalesce_plugin_definitions(element, level + 1) + for element in _input + if element is not None + ] + ) + else: + raise TypeError( + "Pedalboard(...) expected a list (or set) of plugins (or lists or sets of plugins)," + " but found an element of type: {}".format(type(_input)) + ) + + +def _flatten_all_plugins(_input: Optional[PedalboardDefinition]) -> List[Plugin]: + """ + Given a PedalboardDefinition, return a concrete list of plugins that can be executed. + Basically: remove the syntactic sugar and add the appropriate Mix() and Chain() plugins. + """ + if hasattr(_input, "plugins"): + return _input.plugins + elif isinstance(_input, Plugin): + return [_input] + elif isinstance(_input, List) or isinstance(_input, List) or isinstance(_input, Tuple): + return sum([_flatten_all_plugins(element) for element in _input if element is not None], []) + return [] class Pedalboard(collections.abc.MutableSequence): @@ -31,25 +100,20 @@ class Pedalboard(collections.abc.MutableSequence): A container for a chain of plugins, to use for processing audio. """ - def __init__(self, plugins: List[Optional[Plugin]] = None, sample_rate: Optional[float] = None): - if not plugins: - plugins = [] - - for plugin in plugins: - if plugin is not None: - if not isinstance(plugin, Plugin): - raise TypeError( - "An object of type {} cannot be included in a {}.".format( - type(plugin), self.__class__.__name__ - ) - ) - if plugins.count(plugin) > 1: - raise ValueError( - "The same plugin object ({}) was included multiple times in a {}. Please" - " create unique instances if the same effect is required multiple times in" - " series.".format(plugin, self.__class__.__name__) - ) - self.plugins = plugins + def __init__( + self, plugins: Optional[PedalboardDefinition] = None, sample_rate: Optional[float] = None + ): + all_plugins = _flatten_all_plugins(plugins) + for plugin in all_plugins: + if plugin is not None and all_plugins.count(plugin) > 1: + raise ValueError( + "The same plugin object ({}) was included multiple times in a {}. Please" + " create unique instances if the same effect is required multiple times." + .format(plugin, self.__class__.__name__) + ) + self.plugins = _coalesce_plugin_definitions(plugins) + if not isinstance(self.plugins, list): + self.plugins = [self.plugins] if sample_rate is not None and not isinstance(sample_rate, (int, float)): raise TypeError("sample_rate must be None, an integer, or a floating-point number.") diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h new file mode 100644 index 00000000..90e04d5c --- /dev/null +++ b/pedalboard/plugins/Chain.h @@ -0,0 +1,106 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../JuceHeader.h" +#include + +#include "../Plugin.h" +#include "../process.h" + +namespace Pedalboard { +/** + * A class that allows nesting a list of plugins within another. + */ +class Chain : public Plugin { +public: + Chain(std::vector plugins) : plugins(plugins) {} + virtual ~Chain(){}; + + virtual void prepare(const juce::dsp::ProcessSpec &spec) { + for (auto plugin : plugins) + plugin->prepare(spec); + lastSpec = spec; + } + + virtual int + process(const juce::dsp::ProcessContextReplacing &context) { + // assuming process context replacing + auto ioBlock = context.getOutputBlock(); + + float **channels = + (float **)alloca(ioBlock.getNumChannels() * sizeof(float *)); + for (int i = 0; i < ioBlock.getNumChannels(); i++) { + channels[i] = ioBlock.getChannelPointer(i); + } + + juce::AudioBuffer ioBuffer(channels, ioBlock.getNumChannels(), + ioBlock.getNumSamples()); + return ::Pedalboard::process(ioBuffer, lastSpec, plugins, false); + } + + virtual void reset() { + for (auto plugin : plugins) + plugin->reset(); + } + + virtual int getLatencyHint() { + int hint = 0; + for (auto plugin : plugins) + hint += plugin->getLatencyHint(); + return hint; + } + + virtual std::vector getNestedPlugins() const override { return plugins; } + +protected: + std::vector plugins; +}; + +inline void init_chain(py::module &m) { + py::class_(m, "Chain", + "Run zero or more plugins as a plugin. Useful when " + "used with the Mix plugin.") + .def(py::init([](std::vector plugins) { + return new Chain(plugins); + }), + py::arg("plugins"), py::keep_alive<1, 2>()) + .def("__repr__", + [](const Chain &plugin) { + std::ostringstream ss; + ss << ""; + return ss.str(); + }) + .def_property_readonly("plugins", &Chain::getNestedPlugins); +} + +} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/MixPlugin.h b/pedalboard/plugins/Mix.h similarity index 50% rename from pedalboard/MixPlugin.h rename to pedalboard/plugins/Mix.h index dfb33a9a..ab9dd829 100644 --- a/pedalboard/MixPlugin.h +++ b/pedalboard/plugins/Mix.h @@ -17,26 +17,32 @@ #pragma once -#include "JuceHeader.h" +#include "../JuceHeader.h" #include -#include "Plugin.h" +#include "../Plugin.h" namespace Pedalboard { /** - * A class that allows parallel processing of two separate plugin chains. + * A class that allows parallel processing of zero or more separate plugin + * chains. */ -class MixPlugin : public Plugin { +class Mix : public Plugin { public: - MixPlugin(std::vector plugins) : plugins(plugins), pluginBuffers(plugins.size()), samplesAvailablePerPlugin(plugins.size()) {} - virtual ~MixPlugin(){}; + Mix(std::vector plugins) + : plugins(plugins), pluginBuffers(plugins.size()), + samplesAvailablePerPlugin(plugins.size()) {} + virtual ~Mix(){}; virtual void prepare(const juce::dsp::ProcessSpec &spec) { - for (auto plugin : plugins) plugin->prepare(spec); + for (auto plugin : plugins) + plugin->prepare(spec); int maximumBufferSize = getLatencyHint() + spec.maximumBlockSize; - for (auto &buffer : pluginBuffers) buffer.setSize(spec.numChannels, maximumBufferSize); - for (int i = 0; i < samplesAvailablePerPlugin.size(); i++) samplesAvailablePerPlugin[i] = 0; + for (auto &buffer : pluginBuffers) + buffer.setSize(spec.numChannels, maximumBufferSize); + for (int i = 0; i < samplesAvailablePerPlugin.size(); i++) + samplesAvailablePerPlugin[i] = 0; lastSpec = spec; } @@ -50,25 +56,23 @@ class MixPlugin : public Plugin { int startInBuffer = samplesAvailablePerPlugin[i]; int endInBuffer = startInBuffer + ioBlock.getNumSamples(); - // If we don't have enough space, reallocate. (Reluctantly. This is the "audio thread!") + // If we don't have enough space, reallocate. (Reluctantly. This is the + // "audio thread!") if (endInBuffer > buffer.getNumSamples()) { - printf("Want to write from %d to %d, but buffer is only %d long - reallocating.", startInBuffer, endInBuffer, buffer.getNumSamples()); buffer.setSize(buffer.getNumChannels(), endInBuffer); } // Copy the audio input into each of these buffers: context.getInputBlock().copyTo(buffer, 0, samplesAvailablePerPlugin[i]); - float *channelPointers[8] = {}; + float **channelPointers = + (float **)alloca(ioBlock.getNumChannels() * sizeof(float *)); for (int c = 0; c < buffer.getNumChannels(); c++) { channelPointers[c] = buffer.getWritePointer(c, startInBuffer); } auto subBlock = juce::dsp::AudioBlock( - channelPointers, - buffer.getNumChannels(), - ioBlock.getNumSamples() - ); + channelPointers, buffer.getNumChannels(), ioBlock.getNumSamples()); juce::dsp::ProcessContextReplacing subContext(subBlock); int samplesRendered = plugin->process(subContext); @@ -78,11 +82,10 @@ class MixPlugin : public Plugin { // Left-align the results in the buffer, as we'll need all // of the plugins' outputs to be aligned: for (int c = 0; c < pluginBuffers[i].getNumChannels(); c++) { - std::memmove( - channelPointers[c], - channelPointers[c] + (subBlock.getNumSamples() - samplesRendered), - sizeof(float) * samplesRendered - ); + std::memmove(channelPointers[c], + channelPointers[c] + + (subBlock.getNumSamples() - samplesRendered), + sizeof(float) * samplesRendered); } } } @@ -91,14 +94,15 @@ class MixPlugin : public Plugin { // which is the min across all buffers: int maxSamplesAvailable = ioBlock.getNumSamples(); for (int i = 0; i < plugins.size(); i++) { - maxSamplesAvailable = std::min(samplesAvailablePerPlugin[i], maxSamplesAvailable); + maxSamplesAvailable = + std::min(samplesAvailablePerPlugin[i], maxSamplesAvailable); } // Now that each plugin has rendered into its own buffer, mix the output: ioBlock.clear(); if (maxSamplesAvailable) { int leftEdge = ioBlock.getNumSamples() - maxSamplesAvailable; - auto subBlock = ioBlock.getSubBlock(leftEdge); + auto subBlock = ioBlock.getSubBlock(leftEdge, maxSamplesAvailable); for (auto &pluginBuffer : pluginBuffers) { // Right-align exactly `maxSamplesAvailable` samples from each buffer: @@ -110,33 +114,44 @@ class MixPlugin : public Plugin { } } - // Delete the samples we just returned from each buffer and shift the remaining content left: - for (int i = 0; i < plugins.size(); i++) { - int samplesToDelete = maxSamplesAvailable; - int samplesRemaining = pluginBuffers[i].getNumSamples() - samplesAvailablePerPlugin[i]; - for (int c = 0; c < pluginBuffers[i].getNumChannels(); c++) { - float *channelBuffer = pluginBuffers[i].getWritePointer(c); + // Delete the samples we just returned from each buffer and shift the + // remaining content left: + int samplesToDelete = maxSamplesAvailable; + if (samplesToDelete) { + for (int i = 0; i < plugins.size(); i++) { + int samplesRemaining = samplesAvailablePerPlugin[i] - samplesToDelete; + for (int c = 0; c < pluginBuffers[i].getNumChannels(); c++) { + float *channelBuffer = pluginBuffers[i].getWritePointer(c); - // Shift the remaining samples to the start of the buffer: - std::memmove(channelBuffer, channelBuffer + samplesToDelete, sizeof(float) * samplesRemaining); + // Shift the remaining samples to the start of the buffer: + std::memmove(channelBuffer, channelBuffer + samplesToDelete, + sizeof(float) * samplesRemaining); + } + samplesAvailablePerPlugin[i] -= samplesToDelete; } - samplesAvailablePerPlugin[i] -= samplesToDelete; } return maxSamplesAvailable; } virtual void reset() { - for (auto plugin : plugins) plugin->reset(); - for (auto buffer : pluginBuffers) buffer.clear(); + for (auto plugin : plugins) + plugin->reset(); + for (auto buffer : pluginBuffers) + buffer.clear(); } virtual int getLatencyHint() { int maxHint = 0; - for (auto plugin : plugins) maxHint = std::max(maxHint, plugin->getLatencyHint()); + for (auto plugin : plugins) + maxHint = std::max(maxHint, plugin->getLatencyHint()); return maxHint; } + virtual std::vector getNestedPlugins() const override { + return plugins; + } + protected: std::vector> pluginBuffers; std::vector plugins; @@ -144,12 +159,33 @@ class MixPlugin : public Plugin { }; inline void init_mix(py::module &m) { - py::class_( - m, "MixPlugin", - "Mix multiple plugins' output together, processing each in parallel.") - .def(py::init([](std::vector plugins) { - return new MixPlugin(plugins); - }), - py::arg("plugins"), py::keep_alive<1, 2>()); + py::class_( + m, "Mix", + "A utility plugin that allows running other plugins in parallel. All " + "plugins provided will be mixed equally.") + .def(py::init( + [](std::vector plugins) { return new Mix(plugins); }), + py::arg("plugins"), py::keep_alive<1, 2>()) + .def("__repr__", + [](const Mix &plugin) { + std::ostringstream ss; + ss << ""; + return ss.str(); + }) + .def_property_readonly("plugins", &Mix::getNestedPlugins); } } // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/process.h b/pedalboard/process.h index dd7d1855..69aac96e 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -321,6 +321,15 @@ inline int process(juce::AudioBuffer &ioBuffer, return intendedOutputBufferSize - totalOutputLatencySamples; } +void flattenPluginTree(Plugin *plugin, std::vector &output) { + if (std::find(output.begin(), output.end(), plugin) == output.end()) + output.push_back(plugin); + + for (auto *nestedPlugin : plugin->getNestedPlugins()) { + flattenPluginTree(nestedPlugin, output); + } +} + /** * Process a given audio buffer through a list of * Pedalboard plugins at a given sample rate. @@ -356,11 +365,7 @@ process(const py::array_t inputArray, for (auto *plugin : plugins) { if (plugin == nullptr) continue; - - if (std::find(uniquePluginsSortedByPointer.begin(), - uniquePluginsSortedByPointer.end(), - plugin) == uniquePluginsSortedByPointer.end()) - uniquePluginsSortedByPointer.push_back(plugin); + flattenPluginTree(plugin, uniquePluginsSortedByPointer); } if (uniquePluginsSortedByPointer.size() < countOfPluginsIgnoringNull) { diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index 270d2681..14f03c96 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -29,12 +29,11 @@ namespace py = pybind11; #include "ExternalPlugin.h" #include "JucePlugin.h" -#include "MixPlugin.h" #include "Plugin.h" #include "process.h" -#include "ChainPlugin.h" #include "plugins/AddLatency.h" +#include "plugins/Chain.h" #include "plugins/Chorus.h" #include "plugins/Compressor.h" #include "plugins/Convolution.h" @@ -46,7 +45,7 @@ namespace py = pybind11; #include "plugins/LadderFilter.h" #include "plugins/Limiter.h" #include "plugins/LowpassFilter.h" -#include "plugins/MP3Compressor.h" +#include "plugins/Mix.h" #include "plugins/NoiseGate.h" #include "plugins/Phaser.h" #include "plugins/PitchShift.h" @@ -138,7 +137,6 @@ PYBIND11_MODULE(pedalboard_native, m) { py::arg("reset") = true); plugin.attr("__call__") = plugin.attr("process"); - init_chain(m); init_chorus(m); init_compressor(m); init_convolution(m); @@ -151,7 +149,6 @@ PYBIND11_MODULE(pedalboard_native, m) { init_limiter(m); init_lowpass(m); init_mp3_compressor(m); - init_mix(m); init_noisegate(m); init_phaser(m); init_pitch_shift(m); @@ -159,6 +156,11 @@ PYBIND11_MODULE(pedalboard_native, m) { init_external_plugins(m); + // Plugins that don't perform any audio effects, but that add other utilities: + py::module utils = m.def_submodule("utils"); + init_mix(utils); + init_chain(utils); + // Internal plugins for testing, debugging, etc: py::module internal = m.def_submodule("_internal"); init_add_latency(internal); diff --git a/tests/test_mix.py b/tests/test_mix.py new file mode 100644 index 00000000..43ad6ab4 --- /dev/null +++ b/tests/test_mix.py @@ -0,0 +1,206 @@ +#! /usr/bin/env python +# +# Copyright 2021 Spotify AB +# +# Licensed under the GNU Public License, Version 3.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gnu.org/licenses/gpl-3.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest +import numpy as np +from pedalboard import Pedalboard, Compressor, Delay, Distortion, Gain, Mix, Chain, PitchShift, Reverb +from pedalboard_native._internal import AddLatency + + +NUM_SECONDS = 4 + + +def test_chain_syntactic_sugar(): + sr = 44100 + _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) + + # Nested chains should work fine when passed as lists: + pb = Pedalboard([Gain(6), [Gain(-6), Gain(1)], Gain(-1)]) + output = pb(_input, sr) + + assert isinstance(pb[0], Gain) + assert pb[0].gain_db == 6 + + assert isinstance(pb[1], Chain) + assert pb[1].plugins[0].gain_db == -6 + assert pb[1].plugins[1].gain_db == 1 + + assert isinstance(pb[2], Gain) + assert pb[2].gain_db == -1 + + np.testing.assert_allclose(_input, output, rtol=0.01) + + +def test_mix_syntactic_sugar(): + sr = 44100 + _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) + + # Nested mixes should work fine when passed as sets: + pb = Pedalboard([Gain(6), {Gain(-6), Gain(-6)}, Gain(-6)]) + output = pb(_input, sr) + + assert isinstance(pb[0], Gain) + assert pb[0].gain_db == 6 + + assert isinstance(pb[1], Mix) + assert set([plugin.gain_db for plugin in pb[1].plugins]) == set([-6, -6]) + + assert isinstance(pb[2], Gain) + assert pb[2].gain_db == -6 + + np.testing.assert_allclose(_input, output, rtol=0.01) + + +def test_deep_nesting(): + sr = 44100 + _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) + + pb = Pedalboard( + [ + # This parallel chain should boost by 6dB, but due to + # there being 2 outputs, the result will be +12dB. + {tuple([Gain(1) for _ in range(6)]), tuple([Gain(1) for _ in range(6)])}, + # This parallel chain should cut by -24dB, but due to + # there being 2 outputs, the result will be -18dB. + {tuple([Gain(-1) for _ in range(24)]), tuple([Gain(-1) for _ in range(24)])}, + ] + ) + output = pb(_input, sr) + np.testing.assert_allclose(_input * 0.5, output, rtol=0.01) + + +def test_nesting_pedalboards(): + sr = 44100 + _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) + + pb = Pedalboard( + [ + # This parallel chain should boost by 6dB, but due to + # there being 2 outputs, the result will be +12dB. + {Pedalboard([Gain(1) for _ in range(6)]), Pedalboard([Gain(1) for _ in range(6)])}, + # This parallel chain should cut by -24dB, but due to + # there being 2 outputs, the result will be -18dB. + {Pedalboard([Gain(-1) for _ in range(24)]), Pedalboard([Gain(-1) for _ in range(24)])}, + ] + ) + output = pb(_input, sr) + np.testing.assert_allclose(_input * 0.5, output, rtol=0.01) + + +def test_chain_latency_compensation(): + sr = 44100 + _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) + + pb = Pedalboard( + [ + # This parallel chain should boost by 6dB, but due to + # there being 2 outputs, the result will be +12dB. + {tuple([Gain(1) for _ in range(6)]), tuple([Gain(1) for _ in range(6)])}, + # This parallel chain should cut by -24dB, but due to + # there being 2 outputs, the result will be -18dB. + {tuple([Gain(-1) for _ in range(24)]), tuple([Gain(-1) for _ in range(24)])}, + ] + ) + output = pb(_input, sr) + np.testing.assert_allclose(_input * 0.5, output, rtol=0.01) + + +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [128, 8192, 22050, 65536]) +@pytest.mark.parametrize("latency_a_seconds", [0.25, 1, NUM_SECONDS / 2]) +@pytest.mark.parametrize("latency_b_seconds", [0.25, 1, NUM_SECONDS / 2]) +def test_mix_latency_compensation(sample_rate, buffer_size, latency_a_seconds, latency_b_seconds): + noise = np.random.rand(int(NUM_SECONDS * sample_rate)) + pb = Pedalboard( + [ + { + AddLatency(int(latency_a_seconds * sample_rate)), + AddLatency(int(latency_b_seconds * sample_rate)), + }, + ] + ) + output = pb(noise, sample_rate, buffer_size=buffer_size) + + # * 2 here as the mix plugin mixes each plugin at 100% by default + np.testing.assert_allclose(output, noise * 2, rtol=0.01) + + +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [128, 8192, 65536]) +@pytest.mark.parametrize("latency_a_seconds", [0.25, 1, 2, 10]) +@pytest.mark.parametrize("latency_b_seconds", [0.25, 1, 2, 10]) +def test_chain_latency_compensation(sample_rate, buffer_size, latency_a_seconds, latency_b_seconds): + noise = np.random.rand(int(NUM_SECONDS * sample_rate)) + pb = Pedalboard( + [ + [ + AddLatency(int(latency_a_seconds * sample_rate)), + AddLatency(int(latency_b_seconds * sample_rate)), + ], + [ + AddLatency(int(latency_a_seconds * sample_rate)), + AddLatency(int(latency_b_seconds * sample_rate)), + ], + ] + ) + output = pb(noise, sample_rate, buffer_size=buffer_size) + np.testing.assert_allclose(output, noise, rtol=0.01) + + +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [128, 8192, 65536]) +def test_readme_example_does_not_crash(sample_rate, buffer_size): + noise = np.random.rand(int(NUM_SECONDS * sample_rate)) + + passthrough = Gain(gain_db=0) + + delay_and_pitch_shift = Pedalboard( + [ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=7), + Gain(gain_db=-3), + ] + ) + + delay_longer_and_more_pitch_shift = Pedalboard( + [ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=12), + Gain(gain_db=-6), + ] + ) + + original_plus_delayed_harmonies = Pedalboard( + {passthrough, delay_and_pitch_shift, delay_longer_and_more_pitch_shift} + ) + # TODO: Allow passing a Pedalboard into Mix (which will require Pedalboard to be a subclass of Plugin) + original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) + + # or mix and match in more complex ways: + original_plus_delayed_harmonies = Pedalboard([ + # Put a compressor at the front of the chain: + Compressor(), + # Split the chain and mix three different effects equally: + { + (passthrough, Distortion(drive_db=36)), + (delay_and_pitch_shift, Reverb(room_size=1)), + delay_longer_and_more_pitch_shift + }, + # Add a reverb on the final mix: + Reverb() + ]) + original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) \ No newline at end of file From d7d0748e1f9e38fa9444fccca04dd4542256ba4e Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 00:44:38 -0500 Subject: [PATCH 03/13] Change inheritance hierarchy to make Pedalboard instances Plugins. --- README.md | 113 +++++++++++----- pedalboard/ExternalPlugin.h | 6 +- pedalboard/Plugin.h | 8 +- pedalboard/PluginContainer.h | 128 +++++++++++++++++++ pedalboard/pedalboard.py | 111 +--------------- pedalboard/plugins/AddLatency.h | 2 +- pedalboard/plugins/Chain.h | 68 +++++++--- pedalboard/plugins/Chorus.h | 2 +- pedalboard/plugins/Compressor.h | 2 +- pedalboard/plugins/Convolution.h | 2 +- pedalboard/plugins/Delay.h | 2 +- pedalboard/plugins/Distortion.h | 2 +- pedalboard/plugins/Gain.h | 2 +- pedalboard/plugins/HighpassFilter.h | 2 +- pedalboard/plugins/LadderFilter.h | 2 +- pedalboard/plugins/Limiter.h | 2 +- pedalboard/plugins/LowpassFilter.h | 2 +- pedalboard/plugins/Mix.h | 68 +++++----- pedalboard/plugins/NoiseGate.h | 2 +- pedalboard/plugins/Phaser.h | 2 +- pedalboard/plugins/PitchShift.h | 2 +- pedalboard/plugins/Reverb.h | 2 +- pedalboard/process.h | 78 ++++++----- pedalboard/python_bindings.cpp | 19 +-- tests/test_external_plugins.py | 8 +- tests/test_locking.py | 6 +- tests/{test_mix.py => test_mix_and_chain.py} | 94 +++++++++++--- tests/test_python_interface.py | 32 +---- 28 files changed, 450 insertions(+), 319 deletions(-) create mode 100644 pedalboard/PluginContainer.h rename tests/{test_mix.py => test_mix_and_chain.py} (73%) diff --git a/README.md b/README.md index d3fee27a..dc43da49 100644 --- a/README.md +++ b/README.md @@ -90,25 +90,35 @@ to the next in an undesired fashion, try: ## Examples -A very basic example of how to use `pedalboard`'s built-in plugins: +### Quick Start ```python import soundfile as sf -from pedalboard import ( - Pedalboard, - Convolution, - Compressor, - Chorus, - Gain, - Reverb, - Limiter, - LadderFilter, - Phaser, -) +from pedalboard import Pedalboard, Chorus, Reverb +# Read in an audio file: audio, sample_rate = sf.read('some-file.wav') # Make a Pedalboard object, containing multiple plugins: +board = Pedalboard([Chorus(), Reverb(room_size=0.25)]) + +# Run the audio through this pedalboard! +effected = board(audio, s) + +# Write the audio back as a wav file: +sf.write('./processed-output.wav', effected, sample_rate) +``` + +### Making a guitar-style pedalboard + +```python +import soundfile as sf +# Don't do import *! (It just makes this example smaller) +from pedalboard import * + +audio, sample_rate = sf.read('./guitar-input.wav') + +# Make a pretty interesting sounding guitar pedalboard: board = Pedalboard([ Compressor(threshold_db=-50, ratio=25), Gain(gain_db=30), @@ -117,46 +127,38 @@ board = Pedalboard([ Phaser(), Convolution("./guitar_amp.wav", 1.0), Reverb(room_size=0.25), -], sample_rate=sample_rate) +]) # Pedalboard objects behave like lists, so you can add plugins: board.append(Compressor(threshold_db=-25, ratio=10)) board.append(Gain(gain_db=10)) board.append(Limiter()) +# ... or change parameters easily: +board[0].threshold_db = -40 + # Run the audio through this pedalboard! -effected = board(audio) +effected = board(audio, sample_rate) # Write the audio back as a wav file: -with sf.SoundFile('./processed-output-stereo.wav', 'w', samplerate=sample_rate, channels=len(effected.shape)) as f: - f.write(effected) - +sf.write('./guitar-output.wav', effected, sample_rate) ``` -### Loading a VST3® plugin and manipulating its parameters +### Using VST3® or Audio Unit plugins ```python import soundfile as sf from pedalboard import Pedalboard, Reverb, load_plugin -# Load a VST3 package from a known path on disk: +# Load a VST3 or Audio Unit plugin from a known path on disk: vst = load_plugin("./VSTs/RoughRider3.vst3") print(vst.parameters.keys()) # dict_keys([ -# 'sc_hpf_hz', -# 'input_lvl_db', -# 'sensitivity_db', -# 'ratio', -# 'attack_ms', -# 'release_ms', -# 'makeup_db', -# 'mix', -# 'output_lvl_db', -# 'sc_active', -# 'full_bandwidth', -# 'bypass', -# 'program', +# 'sc_hpf_hz', 'input_lvl_db', 'sensitivity_db', +# 'ratio', 'attack_ms', 'release_ms', 'makeup_db', +# 'mix', 'output_lvl_db', 'sc_active', +# 'full_bandwidth', 'bypass', 'program', # ]) # Set the "ratio" parameter to 15 @@ -164,12 +166,53 @@ vst.ratio = 15 # Use this VST to process some audio: audio, sample_rate = sf.read('some-file.wav') -effected = vst(audio, sample_rate=sample_rate) +effected = vst(audio, sample_rate) # ...or put this VST into a chain with other plugins: -board = Pedalboard([vst, Reverb()], sample_rate=sample_rate) +board = Pedalboard([vst, Reverb()]) # ...and run that pedalboard with the same VST instance! -effected = board(audio) +effected = board(audio, sample_rate) +``` + +### Creating parallel effects chains + +This example creates a delayed pitch-shift effect by running +multiple Pedalboards in parallel on the same audio. `Pedalboard` +objects are themselves `Plugin` objects, so you can nest them +as much as you like: + +```python +import soundfile as sf +from pedalboard import \ + Pedalboard, Compressor, Delay, Distortion, \ + Gain, PitchShift, Reverb, Mix + +passthrough = Gain(gain_db=0) + +delay_and_pitch_shift = Pedalboard([ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=7), + Gain(gain_db=-3), +]) + +delay_longer_and_more_pitch_shift = Pedalboard([ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=12), + Gain(gain_db=-6), +]) + +board = Pedalboard([ + # Put a compressor at the front of the chain: + Compressor(), + # Run all of these pedalboards simultaneously with the Mix plugin: + Mix([ + passthrough, + delay_and_pitch_shift, + delay_longer_and_more_pitch_shift, + ]), + # Add a reverb on the final mix: + Reverb() +]) ``` For more examples, see: diff --git a/pedalboard/ExternalPlugin.h b/pedalboard/ExternalPlugin.h index 5344bb70..c8e5cd32 100644 --- a/pedalboard/ExternalPlugin.h +++ b/pedalboard/ExternalPlugin.h @@ -801,7 +801,8 @@ inline void init_external_plugins(py::module &m) { "Returns the current value of the parameter as a string."); #if JUCE_PLUGINHOST_VST3 && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) - py::class_, Plugin>( + py::class_, Plugin, + std::shared_ptr>>( m, "_VST3Plugin", "A wrapper around any Steinberg® VST3 audio effect plugin. Note that " "plugins must already support the operating system currently in use " @@ -842,7 +843,8 @@ inline void init_external_plugins(py::module &m) { #endif #if JUCE_PLUGINHOST_AU && JUCE_MAC - py::class_, Plugin>( + py::class_, Plugin, + std::shared_ptr>>( m, "_AudioUnitPlugin", "A wrapper around any Apple Audio Unit audio effect plugin. Only " "available on macOS.", diff --git a/pedalboard/Plugin.h b/pedalboard/Plugin.h index 5b3d2069..9ad7d7dd 100644 --- a/pedalboard/Plugin.h +++ b/pedalboard/Plugin.h @@ -20,6 +20,8 @@ #include "JuceHeader.h" #include +static constexpr int DEFAULT_BUFFER_SIZE = 8192; + namespace Pedalboard { /** * A base class for all Pedalboard plugins, JUCE-derived or external. @@ -70,12 +72,6 @@ class Plugin { */ virtual int getLatencyHint() { return 0; } - /* - * Some plugins can host/contain other plugins (i.e.: Mix, Chain). - * This method can be used to traverse the plugin tree when necessary. - */ - virtual std::vector getNestedPlugins() const { return {}; } - // A mutex to gate access to this plugin, as its internals may not be // thread-safe. Note: use std::lock or std::scoped_lock when locking multiple // plugins to avoid deadlocking. diff --git a/pedalboard/PluginContainer.h b/pedalboard/PluginContainer.h new file mode 100644 index 00000000..fcc750a9 --- /dev/null +++ b/pedalboard/PluginContainer.h @@ -0,0 +1,128 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "JuceHeader.h" +#include + +#include "Plugin.h" + +namespace Pedalboard { + +/** + * A class for all Pedalboard plugins that contain one or more other plugins. + */ +class PluginContainer : public Plugin { +public: + PluginContainer(std::vector> plugins) + : plugins(plugins) {} + virtual ~PluginContainer(){}; + + std::vector> &getPlugins() { return plugins; } + + /* + * Get a flat list of all of the plugins contained + * by this plugin, not including itself. + */ + std::vector> getAllPlugins() { + std::vector> flatList; + for (auto plugin : plugins) { + flatList.push_back(plugin); + if (auto *pluginContainer = + dynamic_cast(plugin.get())) { + auto children = pluginContainer->getAllPlugins(); + flatList.insert(flatList.end(), children.begin(), children.end()); + } + } + return flatList; + } + +protected: + std::vector> plugins; +}; + +inline void init_plugin_container(py::module &m) { + py::class_>( + m, "PluginContainer", + "A generic audio processing plugin that contains zero or more other " + "plugins. Not intended for direct use.") + .def(py::init([](std::vector> plugins) { + throw py::type_error( + "PluginContainer is an abstract base class - don't instantiate " + "this directly, use its subclasses instead."); + // This will never be hit, but is required to provide a non-void + // type to return from this lambda or else the compiler can't do + // type inference. + return nullptr; + })) + // Implement the Sequence protocol: + .def("__getitem__", + [](PluginContainer &s, size_t i) { + if (i >= s.getPlugins().size()) + throw py::index_error("index out of range"); + return s.getPlugins()[i]; + }) + .def("__setitem__", + [](PluginContainer &s, size_t i, std::shared_ptr plugin) { + if (i >= s.getPlugins().size()) + throw py::index_error("index out of range"); + s.getPlugins()[i] = plugin; + }) + .def("__delitem__", + [](PluginContainer &s, size_t i) { + if (i >= s.getPlugins().size()) + throw py::index_error("index out of range"); + auto &plugins = s.getPlugins(); + plugins.erase(plugins.begin() + i); + }) + .def("__len__", [](PluginContainer &s) { return s.getPlugins().size(); }) + .def("insert", + [](PluginContainer &s, int i, std::shared_ptr plugin) { + if (i > s.getPlugins().size()) + throw py::index_error("index out of range"); + auto &plugins = s.getPlugins(); + plugins.insert(plugins.begin() + i, plugin); + }) + .def("append", + [](PluginContainer &s, std::shared_ptr plugin) { + s.getPlugins().push_back(plugin); + }) + .def("remove", + [](PluginContainer &s, std::shared_ptr plugin) { + auto &plugins = s.getPlugins(); + auto position = std::find(plugins.begin(), plugins.end(), plugin); + if (position == plugins.end()) + throw py::value_error("remove(x): x not in list"); + plugins.erase(position); + }) + .def( + "__iter__", + [](PluginContainer &s) { + return py::make_iterator(s.getPlugins().begin(), + s.getPlugins().end()); + }, + py::keep_alive<0, 1>()) + .def("__contains__", + [](PluginContainer &s, std::shared_ptr plugin) { + auto &plugins = s.getPlugins(); + return std::find(plugins.begin(), plugins.end(), plugin) != + plugins.end(); + }); +} + +} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index 263589e3..e31bfa76 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -95,116 +95,19 @@ def _flatten_all_plugins(_input: Optional[PedalboardDefinition]) -> List[Plugin] return [] -class Pedalboard(collections.abc.MutableSequence): +class Pedalboard(Chain): """ A container for a chain of plugins, to use for processing audio. """ - def __init__( - self, plugins: Optional[PedalboardDefinition] = None, sample_rate: Optional[float] = None - ): - all_plugins = _flatten_all_plugins(plugins) - for plugin in all_plugins: - if plugin is not None and all_plugins.count(plugin) > 1: - raise ValueError( - "The same plugin object ({}) was included multiple times in a {}. Please" - " create unique instances if the same effect is required multiple times." - .format(plugin, self.__class__.__name__) - ) - self.plugins = _coalesce_plugin_definitions(plugins) - if not isinstance(self.plugins, list): - self.plugins = [self.plugins] - - if sample_rate is not None and not isinstance(sample_rate, (int, float)): - raise TypeError("sample_rate must be None, an integer, or a floating-point number.") - self.sample_rate = sample_rate + def __init__(self, plugins: Optional[PedalboardDefinition] = None): + plugins = _coalesce_plugin_definitions(plugins) + if not isinstance(plugins, list): + plugins = [plugins] + super().__init__(plugins) def __repr__(self) -> str: - return "<{} plugins={} sample_rate={}>".format( - self.__class__.__name__, repr(self.plugins), repr(self.sample_rate) - ) - - def __len__(self) -> int: - return len(self.plugins) - - def __delitem__(self, index: int) -> None: - self.plugins.__delitem__(index) - - def insert(self, index: int, value: Optional[Plugin]) -> None: - if value is not None: - if not isinstance(value, Plugin): - raise TypeError( - "An object of type {} cannot be inserted into a {}.".format( - type(value), self.__class__.__name__ - ) - ) - if value in self.plugins: - raise ValueError( - "The provided plugin object ({}) already exists in this {}. Please" - " create unique instances if the same effect is required multiple times in" - " series.".format(value, self.__class__.__name__) - ) - self.plugins.insert(index, value) - - def __setitem__(self, index: int, value: Optional[Plugin]) -> None: - if value is not None: - if not isinstance(value, Plugin): - raise TypeError( - "An object of type {} cannot be added into a {}.".format( - type(value), self.__class__.__name__ - ) - ) - if self.plugins.count(value) == 1 and self.plugins.index(value) != index: - raise ValueError( - "The provided plugin object ({}) already exists in this {} at index {}. Please" - " create unique instances if the same effect is required multiple times in" - " series.".format(value, self.__class__.__name__, self.plugins.index(value)) - ) - self.plugins.__setitem__(index, value) - - def __getitem__(self, index: int) -> Optional[Plugin]: - return self.plugins.__getitem__(index) - - def reset(self): - """ - Clear any internal state (e.g.: reverb tails) kept by all of the plugins in this - Pedalboard. The values of plugin parameters will remain unchanged. For most plugins, - this is a fast operation; for some, this will cause a full re-instantiation of the plugin. - """ - for plugin in self.plugins: - plugin.reset() - - def process( - self, - audio: np.ndarray, - sample_rate: Optional[float] = None, - buffer_size: Optional[int] = None, - reset: bool = True, - ) -> np.ndarray: - if sample_rate is not None and not isinstance(sample_rate, (int, float)): - raise TypeError("sample_rate must be None, an integer, or a floating-point number.") - if buffer_size is not None: - if not isinstance(buffer_size, (int, float)): - raise TypeError("buffer_size must be None, an integer, or a floating-point number.") - buffer_size = int(buffer_size) - - effective_sample_rate = sample_rate or self.sample_rate - if effective_sample_rate is None: - raise ValueError( - ( - "No sample rate available. `sample_rate` must be provided to either the {}" - " constructor or as an argument to `process`." - ).format(self.__class__.__name__) - ) - - # pyBind11 makes a copy of self.plugins when passing it into process. - kwargs = {"sample_rate": effective_sample_rate, "plugins": self.plugins, "reset": reset} - if buffer_size: - kwargs["buffer_size"] = buffer_size - return process(audio, **kwargs) - - # Alias process to __call__, so that people can call Pedalboards like functions. - __call__ = process + return "<{} plugins={}>".format(self.__class__.__name__, list(self)) FLOAT_SUFFIXES_TO_IGNORE = set(["x", "%", "*", ",", ".", "hz"]) diff --git a/pedalboard/plugins/AddLatency.h b/pedalboard/plugins/AddLatency.h index e2101c53..3a438e95 100644 --- a/pedalboard/plugins/AddLatency.h +++ b/pedalboard/plugins/AddLatency.h @@ -50,7 +50,7 @@ class AddLatency : public JucePlugin( + py::class_>( m, "AddLatency", "A dummy plugin that delays input audio for the given number of samples " "before passing it back to the output. Used internally to test " diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h index 90e04d5c..33c10807 100644 --- a/pedalboard/plugins/Chain.h +++ b/pedalboard/plugins/Chain.h @@ -20,16 +20,17 @@ #include "../JuceHeader.h" #include -#include "../Plugin.h" +#include "../PluginContainer.h" #include "../process.h" namespace Pedalboard { /** * A class that allows nesting a list of plugins within another. */ -class Chain : public Plugin { +class Chain : public PluginContainer { public: - Chain(std::vector plugins) : plugins(plugins) {} + Chain(std::vector> plugins) + : PluginContainer(plugins) {} virtual ~Chain(){}; virtual void prepare(const juce::dsp::ProcessSpec &spec) { @@ -65,34 +66,30 @@ class Chain : public Plugin { hint += plugin->getLatencyHint(); return hint; } - - virtual std::vector getNestedPlugins() const override { return plugins; } - -protected: - std::vector plugins; }; inline void init_chain(py::module &m) { - py::class_(m, "Chain", - "Run zero or more plugins as a plugin. Useful when " - "used with the Mix plugin.") - .def(py::init([](std::vector plugins) { + py::class_>( + m, "Chain", + "Run zero or more plugins as a plugin. Useful when " + "used with the Mix plugin.") + .def(py::init([](std::vector> plugins) { return new Chain(plugins); }), - py::arg("plugins"), py::keep_alive<1, 2>()) + py::arg("plugins")) .def("__repr__", - [](const Chain &plugin) { + [](Chain &plugin) { std::ostringstream ss; - ss << ""; return ss.str(); }) - .def_property_readonly("plugins", &Chain::getNestedPlugins); + // If calling process() directly on Chain, pass the plugins immediately to + // process() itself, as that will result in slightly faster performance. + .def( + "process", + [](std::shared_ptr self, + const py::array_t inputArray, + double sampleRate, unsigned int bufferSize, bool reset) { + return process(inputArray, sampleRate, self->getPlugins(), bufferSize, reset); + }, + "Run a 32-bit floating point audio buffer through this plugin." + "(Note: if calling this multiple times with multiple plugins, " + "consider using pedalboard.process(...) instead.)", + py::arg("input_array"), py::arg("sample_rate"), + py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true) + + .def( + "process", + [](std::shared_ptr self, + const py::array_t inputArray, + double sampleRate, unsigned int bufferSize, bool reset) { + const py::array_t float32InputArray = + inputArray.attr("astype")("float32"); + return process(float32InputArray, sampleRate, self->getPlugins(), bufferSize, + reset); + }, + "Run a 64-bit floating point audio buffer through this plugin." + "(Note: if calling this multiple times with multiple plugins, " + "consider using pedalboard.process(...) instead.) The buffer " + "will be converted to 32-bit for processing.", + py::arg("input_array"), py::arg("sample_rate"), + py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, + py::arg("reset") = true); } } // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/plugins/Chorus.h b/pedalboard/plugins/Chorus.h index bee74387..9ccd8f5c 100644 --- a/pedalboard/plugins/Chorus.h +++ b/pedalboard/plugins/Chorus.h @@ -37,7 +37,7 @@ class Chorus : public JucePlugin> { }; inline void init_chorus(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Chorus", "A basic chorus effect. This audio effect can be controlled via the " "speed and depth of the LFO controlling the frequency response, a mix " diff --git a/pedalboard/plugins/Compressor.h b/pedalboard/plugins/Compressor.h index 41ccb633..46aec967 100644 --- a/pedalboard/plugins/Compressor.h +++ b/pedalboard/plugins/Compressor.h @@ -36,7 +36,7 @@ class Compressor : public JucePlugin> { }; inline void init_compressor(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Compressor", "A dynamic range compressor, used to amplify quiet sounds and reduce the " "volume of loud sounds.") diff --git a/pedalboard/plugins/Convolution.h b/pedalboard/plugins/Convolution.h index b9994d19..2f3daefd 100644 --- a/pedalboard/plugins/Convolution.h +++ b/pedalboard/plugins/Convolution.h @@ -78,7 +78,7 @@ class ConvolutionWithMix { }; inline void init_convolution(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Convolution", "An audio convolution, suitable for things like speaker simulation or " "reverb modeling.") diff --git a/pedalboard/plugins/Delay.h b/pedalboard/plugins/Delay.h index 6b25162b..f8010d73 100644 --- a/pedalboard/plugins/Delay.h +++ b/pedalboard/plugins/Delay.h @@ -100,7 +100,7 @@ class Delay : public JucePlugin, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Delay", "A digital delay plugin with controllable delay time, feedback " "percentage, and dry/wet mix.") diff --git a/pedalboard/plugins/Distortion.h b/pedalboard/plugins/Distortion.h index 9f22bc90..fb9b5a68 100644 --- a/pedalboard/plugins/Distortion.h +++ b/pedalboard/plugins/Distortion.h @@ -48,7 +48,7 @@ class Distortion }; inline void init_distortion(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Distortion", "Apply soft distortion with a tanh waveshaper.") .def(py::init([](float drive_db) { auto plugin = std::make_unique>(); diff --git a/pedalboard/plugins/Gain.h b/pedalboard/plugins/Gain.h index 079fdff3..13fb864c 100644 --- a/pedalboard/plugins/Gain.h +++ b/pedalboard/plugins/Gain.h @@ -29,7 +29,7 @@ class Gain : public JucePlugin> { }; inline void init_gain(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Gain", "Increase or decrease the volume of a signal by applying a gain value " "(in decibels). No distortion or other effects are applied.") diff --git a/pedalboard/plugins/HighpassFilter.h b/pedalboard/plugins/HighpassFilter.h index 6065f2e7..d6298b9b 100644 --- a/pedalboard/plugins/HighpassFilter.h +++ b/pedalboard/plugins/HighpassFilter.h @@ -41,7 +41,7 @@ class HighpassFilter : public JucePlugin> { }; inline void init_highpass(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "HighpassFilter", "Apply a first-order high-pass filter with a roll-off of 6dB/octave. " "The cutoff frequency will be attenuated by -3dB (i.e.: 0.707x as " diff --git a/pedalboard/plugins/LadderFilter.h b/pedalboard/plugins/LadderFilter.h index 8ac610a7..ccc773a0 100644 --- a/pedalboard/plugins/LadderFilter.h +++ b/pedalboard/plugins/LadderFilter.h @@ -58,7 +58,7 @@ class LadderFilter : public JucePlugin> { }; inline void init_ladderfilter(py::module &m) { - py::class_, Plugin> ladderFilter( + py::class_, Plugin, std::shared_ptr>> ladderFilter( m, "LadderFilter", "Multi-mode audio filter based on the classic Moog synthesizer ladder " "filter."); diff --git a/pedalboard/plugins/Limiter.h b/pedalboard/plugins/Limiter.h index e7229125..d4047c10 100644 --- a/pedalboard/plugins/Limiter.h +++ b/pedalboard/plugins/Limiter.h @@ -30,7 +30,7 @@ class Limiter : public JucePlugin> { }; inline void init_limiter(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Limiter", "A simple limiter with standard threshold and release time controls, " "featuring two compressors and a hard clipper at 0 dB.") diff --git a/pedalboard/plugins/LowpassFilter.h b/pedalboard/plugins/LowpassFilter.h index d22afa14..090ccc51 100644 --- a/pedalboard/plugins/LowpassFilter.h +++ b/pedalboard/plugins/LowpassFilter.h @@ -41,7 +41,7 @@ class LowpassFilter : public JucePlugin> { }; inline void init_lowpass(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "LowpassFilter", "Apply a first-order low-pass filter with a roll-off of 6dB/octave. " "The cutoff frequency will be attenuated by -3dB (i.e.: 0.707x as " diff --git a/pedalboard/plugins/Mix.h b/pedalboard/plugins/Mix.h index ab9dd829..40e828be 100644 --- a/pedalboard/plugins/Mix.h +++ b/pedalboard/plugins/Mix.h @@ -20,17 +20,17 @@ #include "../JuceHeader.h" #include -#include "../Plugin.h" +#include "../PluginContainer.h" namespace Pedalboard { /** * A class that allows parallel processing of zero or more separate plugin * chains. */ -class Mix : public Plugin { +class Mix : public PluginContainer { public: - Mix(std::vector plugins) - : plugins(plugins), pluginBuffers(plugins.size()), + Mix(std::vector> plugins) + : PluginContainer(plugins), pluginBuffers(plugins.size()), samplesAvailablePerPlugin(plugins.size()) {} virtual ~Mix(){}; @@ -51,7 +51,7 @@ class Mix : public Plugin { auto ioBlock = context.getOutputBlock(); for (int i = 0; i < plugins.size(); i++) { - Plugin *plugin = plugins[i]; + std::shared_ptr plugin = plugins[i]; juce::AudioBuffer &buffer = pluginBuffers[i]; int startInBuffer = samplesAvailablePerPlugin[i]; @@ -66,7 +66,7 @@ class Mix : public Plugin { context.getInputBlock().copyTo(buffer, 0, samplesAvailablePerPlugin[i]); float **channelPointers = - (float **)alloca(ioBlock.getNumChannels() * sizeof(float *)); + (float **)alloca(ioBlock.getNumChannels() * sizeof(float *)); for (int c = 0; c < buffer.getNumChannels(); c++) { channelPointers[c] = buffer.getWritePointer(c, startInBuffer); } @@ -125,7 +125,7 @@ class Mix : public Plugin { // Shift the remaining samples to the start of the buffer: std::memmove(channelBuffer, channelBuffer + samplesToDelete, - sizeof(float) * samplesRemaining); + sizeof(float) * samplesRemaining); } samplesAvailablePerPlugin[i] -= samplesToDelete; } @@ -148,44 +148,38 @@ class Mix : public Plugin { return maxHint; } - virtual std::vector getNestedPlugins() const override { - return plugins; - } - protected: std::vector> pluginBuffers; - std::vector plugins; std::vector samplesAvailablePerPlugin; }; inline void init_mix(py::module &m) { - py::class_( + py::class_>( m, "Mix", "A utility plugin that allows running other plugins in parallel. All " "plugins provided will be mixed equally.") - .def(py::init( - [](std::vector plugins) { return new Mix(plugins); }), - py::arg("plugins"), py::keep_alive<1, 2>()) - .def("__repr__", - [](const Mix &plugin) { - std::ostringstream ss; - ss << ""; - return ss.str(); - }) - .def_property_readonly("plugins", &Mix::getNestedPlugins); + .def(py::init([](std::vector> plugins) { + return new Mix(plugins); + }), + py::arg("plugins")) + .def("__repr__", [](Mix &plugin) { + std::ostringstream ss; + ss << ""; + return ss.str(); + }); } } // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/plugins/NoiseGate.h b/pedalboard/plugins/NoiseGate.h index e8492737..da1bb4e5 100644 --- a/pedalboard/plugins/NoiseGate.h +++ b/pedalboard/plugins/NoiseGate.h @@ -33,7 +33,7 @@ class NoiseGate : public JucePlugin> { inline void init_noisegate(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "NoiseGate", "A simple noise gate with standard threshold, ratio, attack time and " "release time controls. Can be used as an expander if the ratio is low.") diff --git a/pedalboard/plugins/Phaser.h b/pedalboard/plugins/Phaser.h index c65226fb..c9181a32 100644 --- a/pedalboard/plugins/Phaser.h +++ b/pedalboard/plugins/Phaser.h @@ -34,7 +34,7 @@ class Phaser : public JucePlugin> { inline void init_phaser(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Phaser", "A 6 stage phaser that modulates first order all-pass filters to create " "sweeping notches in the magnitude frequency response. This audio effect " diff --git a/pedalboard/plugins/PitchShift.h b/pedalboard/plugins/PitchShift.h index ed97e0de..74e71915 100644 --- a/pedalboard/plugins/PitchShift.h +++ b/pedalboard/plugins/PitchShift.h @@ -61,7 +61,7 @@ class PitchShift : public RubberbandPlugin }; inline void init_pitch_shift(py::module &m) { - py::class_( + py::class_>( m, "PitchShift", "Shift pitch without affecting audio duration.") .def(py::init([](double scale) { auto plugin = std::make_unique(); diff --git a/pedalboard/plugins/Reverb.h b/pedalboard/plugins/Reverb.h index be735be7..8d517225 100644 --- a/pedalboard/plugins/Reverb.h +++ b/pedalboard/plugins/Reverb.h @@ -77,7 +77,7 @@ class Reverb : public JucePlugin { }; inline void init_reverb(py::module &m) { - py::class_( + py::class_>( m, "Reverb", "Performs a simple reverb effect on a stream of audio data. This is a " "simple stereo reverb, based on the technique and tunings used in " diff --git a/pedalboard/process.h b/pedalboard/process.h index 69aac96e..4058ebaa 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -37,7 +37,7 @@ enum class ChannelLayout { template py::array_t process(const py::array_t inputArray, - double sampleRate, const std::vector &plugins, + double sampleRate, const std::vector> plugins, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); @@ -50,9 +50,9 @@ process(const py::array_t inputArray, template py::array_t processSingle(const py::array_t inputArray, - double sampleRate, Plugin &plugin, unsigned int bufferSize, - bool reset) { - std::vector plugins{&plugin}; + double sampleRate, std::shared_ptr plugin, + unsigned int bufferSize, bool reset) { + std::vector> plugins{plugin}; return process(inputArray, sampleRate, plugins, bufferSize, reset); } @@ -205,13 +205,13 @@ py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, inline int process(juce::AudioBuffer &ioBuffer, juce::dsp::ProcessSpec spec, - const std::vector &plugins, + const std::vector> &plugins, bool isProbablyLastProcessCall) { int totalOutputLatencySamples = 0; int expectedOutputLatency = 0; - for (auto *plugin : plugins) { - if (plugin == nullptr) + for (auto plugin : plugins) { + if (!plugin) continue; expectedOutputLatency += plugin->getLatencyHint(); } @@ -232,8 +232,8 @@ inline int process(juce::AudioBuffer &ioBuffer, int startOfOutputInBuffer = 0; int lastSampleInBuffer = 0; - for (auto *plugin : plugins) { - if (plugin == nullptr) + for (auto plugin : plugins) { + if (!plugin) continue; int pluginSamplesReceived = 0; @@ -321,15 +321,6 @@ inline int process(juce::AudioBuffer &ioBuffer, return intendedOutputBufferSize - totalOutputLatencySamples; } -void flattenPluginTree(Plugin *plugin, std::vector &output) { - if (std::find(output.begin(), output.end(), plugin) == output.end()) - output.push_back(plugin); - - for (auto *nestedPlugin : plugin->getNestedPlugins()) { - flattenPluginTree(nestedPlugin, output); - } -} - /** * Process a given audio buffer through a list of * Pedalboard plugins at a given sample rate. @@ -338,7 +329,7 @@ void flattenPluginTree(Plugin *plugin, std::vector &output) { template <> py::array_t process(const py::array_t inputArray, - double sampleRate, const std::vector &plugins, + double sampleRate, std::vector> plugins, unsigned int bufferSize, bool reset) { const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); juce::AudioBuffer ioBuffer = copyPyArrayIntoJuceBuffer(inputArray); @@ -349,44 +340,49 @@ process(const py::array_t inputArray, bufferSize = std::min(bufferSize, (unsigned int)ioBuffer.getNumSamples()); - unsigned int countOfPluginsIgnoringNull = 0; - for (auto *plugin : plugins) { - if (plugin == nullptr) - continue; - countOfPluginsIgnoringNull++; - } - // We'd pass multiple arguments to scoped_lock here, but we don't know how // many plugins have been passed at compile time - so instead, we do our own // deadlock-avoiding multiple-lock algorithm here. By locking each plugin // only in order of its pointers, we're guaranteed to avoid deadlocks with // other threads that may be running this same code on the same plugins. - std::vector uniquePluginsSortedByPointer; - for (auto *plugin : plugins) { - if (plugin == nullptr) + std::vector> allPlugins; + for (auto plugin : plugins) { + if (!plugin) continue; - flattenPluginTree(plugin, uniquePluginsSortedByPointer); + allPlugins.push_back(plugin); + if (auto pluginContainer = + dynamic_cast(plugin.get())) { + auto children = pluginContainer->getAllPlugins(); + allPlugins.insert(allPlugins.end(), children.begin(), children.end()); + } } - if (uniquePluginsSortedByPointer.size() < countOfPluginsIgnoringNull) { + std::sort(allPlugins.begin(), allPlugins.end(), + [](const std::shared_ptr lhs, + const std::shared_ptr rhs) { + return lhs.get() < rhs.get(); + }); + + bool containsDuplicates = + std::adjacent_find(allPlugins.begin(), allPlugins.end()) != + allPlugins.end(); + + if (containsDuplicates) { throw std::runtime_error( "The same plugin instance is being used multiple times in the same " - "chain of plugins, which would cause undefined results."); + "chain of plugins, which would cause undefined results. Please " + "ensure that no duplicate plugins are present before calling."); } - std::sort(uniquePluginsSortedByPointer.begin(), - uniquePluginsSortedByPointer.end(), - [](const Plugin *lhs, const Plugin *rhs) { return lhs < rhs; }); - std::vector>> pluginLocks; - for (auto *plugin : uniquePluginsSortedByPointer) { + for (auto plugin : allPlugins) { pluginLocks.push_back( std::make_unique>(plugin->mutex)); } if (reset) { - for (auto *plugin : plugins) { - if (plugin == nullptr) + for (auto plugin : plugins) { + if (!plugin) continue; plugin->reset(); } @@ -397,8 +393,8 @@ process(const py::array_t inputArray, spec.maximumBlockSize = static_cast(bufferSize); spec.numChannels = static_cast(ioBuffer.getNumChannels()); - for (auto *plugin : plugins) { - if (plugin == nullptr) + for (auto plugin : plugins) { + if (!plugin) continue; plugin->prepare(spec); } diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index 14f03c96..a14b5d22 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -30,6 +30,7 @@ namespace py = pybind11; #include "ExternalPlugin.h" #include "JucePlugin.h" #include "Plugin.h" +#include "PluginContainer.h" #include "process.h" #include "plugins/AddLatency.h" @@ -53,8 +54,6 @@ namespace py = pybind11; using namespace Pedalboard; -static constexpr int DEFAULT_BUFFER_SIZE = 8192; - PYBIND11_MODULE(pedalboard_native, m) { m.def("process", process, "Run a 32-bit floating point audio buffer through a list of Pedalboard " @@ -84,9 +83,10 @@ PYBIND11_MODULE(pedalboard_native, m) { py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); auto plugin = - py::class_(m, "Plugin", - "A generic audio processing plugin. Base class of all " - "Pedalboard plugins.") + py::class_>( + m, "Plugin", + "A generic audio processing plugin. Base class of all " + "Pedalboard plugins.") .def(py::init([]() { throw py::type_error( "Plugin is an abstract base class - don't instantiate this " @@ -104,7 +104,7 @@ PYBIND11_MODULE(pedalboard_native, m) { "cause a full re-instantiation of the plugin.") .def( "process", - [](Plugin *self, + [](std::shared_ptr self, const py::array_t inputArray, double sampleRate, unsigned int bufferSize, bool reset) { return process(inputArray, sampleRate, {self}, bufferSize, @@ -119,7 +119,7 @@ PYBIND11_MODULE(pedalboard_native, m) { .def( "process", - [](Plugin *self, + [](std::shared_ptr self, const py::array_t inputArray, double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = @@ -130,13 +130,14 @@ PYBIND11_MODULE(pedalboard_native, m) { "Run a 64-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " "consider using pedalboard.process(...) instead.) The buffer " - "will be " - "converted to 32-bit for processing.", + "will be converted to 32-bit for processing.", py::arg("input_array"), py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); plugin.attr("__call__") = plugin.attr("process"); + init_plugin_container(m); + init_chorus(m); init_compressor(m); init_convolution(m); diff --git a/tests/test_external_plugins.py b/tests/test_external_plugins.py index 3a5448e6..4b5f66a7 100644 --- a/tests/test_external_plugins.py +++ b/tests/test_external_plugins.py @@ -586,17 +586,17 @@ def test_explicit_reset(plugin_filename: str): @pytest.mark.parametrize("plugin_filename", AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT) def test_explicit_reset_in_pedalboard(plugin_filename: str): sr = 44100 - board = pedalboard.Pedalboard([load_test_plugin(plugin_filename, disable_caching=True)], sr) + board = pedalboard.Pedalboard([load_test_plugin(plugin_filename, disable_caching=True)]) noise = np.random.rand(sr, 2) assert max_volume_of(noise) > 0.95 silence = np.zeros_like(noise) assert max_volume_of(silence) == 0.0 - assert max_volume_of(board(silence, reset=False)) < 0.00001 - assert max_volume_of(board(noise, reset=False)) > 0.00001 + assert max_volume_of(board(silence, reset=False, sample_rate=sr)) < 0.00001 + assert max_volume_of(board(noise, reset=False, sample_rate=sr)) > 0.00001 board.reset() - assert max_volume_of(board(silence, reset=False)) < 0.00001 + assert max_volume_of(board(silence, reset=False, sample_rate=sr)) < 0.00001 @pytest.mark.parametrize("value", (True, False)) diff --git a/tests/test_locking.py b/tests/test_locking.py index 03c7be80..e50c2dd6 100644 --- a/tests/test_locking.py +++ b/tests/test_locking.py @@ -42,19 +42,19 @@ def test_multiple_threads_using_same_plugin_instances(num_concurrent_chains: int # we take locks (if we didn't have logic for it) would be # the pathologically worst-case. plugins.reverse() - pedalboards.append(pedalboard.Pedalboard(list(plugins), sample_rate=sr)) + pedalboards.append(pedalboard.Pedalboard(list(plugins))) for _ in range(0, num_concurrent_chains // 2): # Shuffle the list of plugins so that the order in which # we pass plugins into the method is non-deterministic: random.shuffle(plugins) - pedalboards.append(pedalboard.Pedalboard(list(plugins), sample_rate=sr)) + pedalboards.append(pedalboard.Pedalboard(list(plugins))) futures = [] with ThreadPoolExecutor(max_workers=num_concurrent_chains) as e: noise = np.random.rand(1, sr) for pb in pedalboards: - futures.append(e.submit(pb.process, np.copy(noise))) + futures.append(e.submit(pb.process, np.copy(noise), sample_rate=sr)) # This will throw an exception if we exceed the timeout: processed = [future.result(timeout=2 * num_concurrent_chains) for future in futures] diff --git a/tests/test_mix.py b/tests/test_mix_and_chain.py similarity index 73% rename from tests/test_mix.py rename to tests/test_mix_and_chain.py index 43ad6ab4..3e1c3458 100644 --- a/tests/test_mix.py +++ b/tests/test_mix_and_chain.py @@ -17,7 +17,17 @@ import pytest import numpy as np -from pedalboard import Pedalboard, Compressor, Delay, Distortion, Gain, Mix, Chain, PitchShift, Reverb +from pedalboard import ( + Pedalboard, + Compressor, + Delay, + Distortion, + Gain, + Mix, + Chain, + PitchShift, + Reverb, +) from pedalboard_native._internal import AddLatency @@ -36,8 +46,8 @@ def test_chain_syntactic_sugar(): assert pb[0].gain_db == 6 assert isinstance(pb[1], Chain) - assert pb[1].plugins[0].gain_db == -6 - assert pb[1].plugins[1].gain_db == 1 + assert pb[1][0].gain_db == -6 + assert pb[1][1].gain_db == 1 assert isinstance(pb[2], Gain) assert pb[2].gain_db == -1 @@ -57,7 +67,7 @@ def test_mix_syntactic_sugar(): assert pb[0].gain_db == 6 assert isinstance(pb[1], Mix) - assert set([plugin.gain_db for plugin in pb[1].plugins]) == set([-6, -6]) + assert set([plugin.gain_db for plugin in pb[1]]) == set([-6, -6]) assert isinstance(pb[2], Gain) assert pb[2].gain_db == -6 @@ -191,16 +201,66 @@ def test_readme_example_does_not_crash(sample_rate, buffer_size): original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) # or mix and match in more complex ways: - original_plus_delayed_harmonies = Pedalboard([ - # Put a compressor at the front of the chain: - Compressor(), - # Split the chain and mix three different effects equally: - { - (passthrough, Distortion(drive_db=36)), - (delay_and_pitch_shift, Reverb(room_size=1)), - delay_longer_and_more_pitch_shift - }, - # Add a reverb on the final mix: - Reverb() - ]) - original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) \ No newline at end of file + original_plus_delayed_harmonies = Pedalboard( + [ + # Put a compressor at the front of the chain: + Compressor(), + # Split the chain and mix three different effects equally: + { + (passthrough, Distortion(drive_db=36)), + (delay_and_pitch_shift, Reverb(room_size=1)), + delay_longer_and_more_pitch_shift, + }, + # Add a reverb on the final mix: + Reverb(), + ] + ) + original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) + + +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [128, 8192, 65536]) +def test_pedalboard_is_a_plugin(sample_rate, buffer_size): + noise = np.random.rand(int(NUM_SECONDS * sample_rate)) + + passthrough = Gain(gain_db=0) + + delay_and_pitch_shift = Chain( + [ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=7), + Gain(gain_db=-3), + ] + ) + + delay_longer_and_more_pitch_shift = Chain( + [ + Delay(delay_seconds=0.25, mix=1.0), + PitchShift(semitones=12), + Gain(gain_db=-6), + ] + ) + + original_plus_delayed_harmonies = Pedalboard( + [Mix([passthrough, delay_and_pitch_shift, delay_longer_and_more_pitch_shift])] + ) + original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) + + # or mix and match in more complex ways: + original_plus_delayed_harmonies = Pedalboard( + [ + # Put a compressor at the front of the chain: + Compressor(), + # Split the chain and mix three different effects equally: + Mix( + [ + Chain([passthrough, Distortion(drive_db=36)]), + Chain([delay_and_pitch_shift, Reverb(room_size=1)]), + delay_longer_and_more_pitch_shift, + ] + ), + # Add a reverb on the final mix: + Reverb(), + ] + ) + original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) diff --git a/tests/test_python_interface.py b/tests/test_python_interface.py index f4fe2674..82ae34cc 100644 --- a/tests/test_python_interface.py +++ b/tests/test_python_interface.py @@ -24,7 +24,7 @@ def test_no_transforms(shape, sr=44100): _input = np.random.rand(*shape).astype(np.float32) - output = Pedalboard([], sr).process(_input) + output = Pedalboard([]).process(_input, sr) assert _input.shape == output.shape assert np.allclose(_input, output, rtol=0.0001) @@ -32,12 +32,10 @@ def test_no_transforms(shape, sr=44100): def test_fail_on_invalid_plugin(): with pytest.raises(TypeError): - Pedalboard(["I want a reverb please"], 44100) + Pedalboard(["I want a reverb please"]) def test_fail_on_invalid_sample_rate(): - with pytest.raises(TypeError): - Pedalboard([], "fourty four one hundred") with pytest.raises(TypeError): Pedalboard([]).process([], "fourty four one hundred") @@ -50,23 +48,21 @@ def test_fail_on_invalid_buffer_size(): def test_repr(): sr = 44100 gain = Gain(-6) - value = repr(Pedalboard([gain], sr)) + value = repr(Pedalboard([gain])) # Allow flexibility; all we care about is that these values exist in the repr. assert "Pedalboard" in value - assert str(sr) in value assert "plugins=" in value assert repr(gain) in value - assert "sample_rate=" in value def test_is_list_like(): sr = 44100 gain = Gain(-6) - assert len(Pedalboard([gain], sr)) == 1 - assert len(Pedalboard([gain, Gain(-6)], sr)) == 2 + assert len(Pedalboard([gain])) == 1 + assert len(Pedalboard([gain, Gain(-6)])) == 2 - pb = Pedalboard([gain], sr) + pb = Pedalboard([gain]) assert len(pb) == 1 # Allow adding elements, like a list: @@ -89,19 +85,3 @@ def test_is_list_like(): with pytest.raises(TypeError): pb[0] = "not a plugin" - - -def test_process_validates_sample_rate(): - sr = 44100 - full_scale_noise = np.random.rand(sr, 1).astype(np.float32) - - # Sample rate can be provided in the constructor: - pb = Pedalboard([Gain(-6)], sr).process(full_scale_noise) - - # ...or in the `process` call: - pb = Pedalboard([Gain(-6)]).process(full_scale_noise, sr) - - # But if not passed in either, an exception will be raised: - pb = Pedalboard([Gain(-6)]) - with pytest.raises(ValueError): - pb.process(full_scale_noise) From 444c6c834015369ffae06808597fa8f929336ab6 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 10:18:59 -0500 Subject: [PATCH 04/13] Fix TensorFlow test. --- tests/test_tensorflow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_tensorflow.py b/tests/test_tensorflow.py index 3e8e09b5..c84e80ed 100644 --- a/tests/test_tensorflow.py +++ b/tests/test_tensorflow.py @@ -32,12 +32,13 @@ def test_can_be_called_in_tensorflow_data_pipeline(): import tensorflow as tf sr = 48000 - plugins = pedalboard.Pedalboard([pedalboard.Gain(), pedalboard.Reverb()], sample_rate=sr) + plugins = pedalboard.Pedalboard([pedalboard.Gain(), pedalboard.Reverb()]) noise = np.random.rand(sr) ds = tf.data.Dataset.from_tensor_slices([noise]).map( - lambda audio: tf.numpy_function(plugins.process, [audio], tf.float32) + lambda audio: tf.numpy_function(plugins.process, [audio, sr], tf.float32 + ) ) model = tf.keras.models.Sequential( @@ -47,4 +48,4 @@ def test_can_be_called_in_tensorflow_data_pipeline(): model.compile(loss="mse") model.fit(ds.map(lambda effected: (effected, 1)).batch(1), epochs=10) - assert np.allclose(plugins.process(noise), np.array([tensor.numpy() for tensor in ds])) + assert np.allclose(plugins(noise, sr), np.array([tensor.numpy() for tensor in ds])) From d9db0e88b93a25a5838b62094956835007cfff4f Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 20:30:55 -0500 Subject: [PATCH 05/13] Remove syntactic sugar experiment. --- pedalboard/pedalboard.py | 76 ++----------------------------------- pedalboard/plugins/Chain.h | 1 + pedalboard/plugins/Mix.h | 1 + tests/test_mix_and_chain.py | 71 ++++++++++++++++++++++------------ 4 files changed, 51 insertions(+), 98 deletions(-) diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index e31bfa76..8ee60706 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -28,86 +28,16 @@ from pedalboard_native.utils import Mix, Chain -# A concrete type for all ways to define a Pedalboard with zero or more plugins: -PedalboardDefinition = Union[ - # The standard Pedalboard is a list of plugins, - # although nested lists will be unpacked. - List[Union[Plugin, 'PedalboardDefinition']], - # Tuples can be used in place of lists when necessary, - # if putting a chain inside of a mix plugin. - Tuple[Union[Plugin, 'PedalboardDefinition']], - # Pedalboards can be nested, and the contained - # pedalboard's plugins will be treated as a list: - 'Pedalboard', - # Passing a set of plugins will result in them being processed in parallel - # (i.e: they will all accept the same input and their outputs will be mixed) - Set[Union[Plugin, 'PedalboardDefinition']], -] - - -def _coalesce_plugin_definitions( - _input: Optional[PedalboardDefinition], level: int = 0 -) -> List[Plugin]: - """ - Given a PedalboardDefinition, return a concrete list of plugins that can be executed. - Basically: remove the syntactic sugar and add the appropriate Mix() and Chain() plugins. - """ - if isinstance(_input, Plugin): - return _input - elif hasattr(_input, "plugins"): - return _coalesce_plugin_definitions(_input.plugins, level + 1) - elif isinstance(_input, List) or isinstance(_input, Tuple): - plugins = [ - _coalesce_plugin_definitions(element, level + 1) - for element in _input - if element is not None - ] - if level > 0: - return Chain(plugins) - else: - return plugins - elif isinstance(_input, Set): - return Mix( - [ - _coalesce_plugin_definitions(element, level + 1) - for element in _input - if element is not None - ] - ) - else: - raise TypeError( - "Pedalboard(...) expected a list (or set) of plugins (or lists or sets of plugins)," - " but found an element of type: {}".format(type(_input)) - ) - - -def _flatten_all_plugins(_input: Optional[PedalboardDefinition]) -> List[Plugin]: - """ - Given a PedalboardDefinition, return a concrete list of plugins that can be executed. - Basically: remove the syntactic sugar and add the appropriate Mix() and Chain() plugins. - """ - if hasattr(_input, "plugins"): - return _input.plugins - elif isinstance(_input, Plugin): - return [_input] - elif isinstance(_input, List) or isinstance(_input, List) or isinstance(_input, Tuple): - return sum([_flatten_all_plugins(element) for element in _input if element is not None], []) - return [] - - class Pedalboard(Chain): """ A container for a chain of plugins, to use for processing audio. """ - def __init__(self, plugins: Optional[PedalboardDefinition] = None): - plugins = _coalesce_plugin_definitions(plugins) - if not isinstance(plugins, list): - plugins = [plugins] - super().__init__(plugins) + def __init__(self, plugins: Optional[List[Plugin]] = None): + super().__init__(plugins or []) def __repr__(self) -> str: - return "<{} plugins={}>".format(self.__class__.__name__, list(self)) + return "<{} with {} plugins: {}>".format(self.__class__.__name__, len(self), list(self)) FLOAT_SUFFIXES_TO_IGNORE = set(["x", "%", "*", ",", ".", "hz"]) diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h index 33c10807..871172bd 100644 --- a/pedalboard/plugins/Chain.h +++ b/pedalboard/plugins/Chain.h @@ -77,6 +77,7 @@ inline void init_chain(py::module &m) { return new Chain(plugins); }), py::arg("plugins")) + .def(py::init([]() { return new Chain({}); })) .def("__repr__", [](Chain &plugin) { std::ostringstream ss; diff --git a/pedalboard/plugins/Mix.h b/pedalboard/plugins/Mix.h index 40e828be..9efc830d 100644 --- a/pedalboard/plugins/Mix.h +++ b/pedalboard/plugins/Mix.h @@ -162,6 +162,7 @@ inline void init_mix(py::module &m) { return new Mix(plugins); }), py::arg("plugins")) + .def(py::init([]() { return new Mix({}); })) .def("__repr__", [](Mix &plugin) { std::ostringstream ss; ss << " Date: Wed, 26 Jan 2022 12:02:25 -0500 Subject: [PATCH 06/13] clang-format --- pedalboard/plugins/Chain.h | 7 ++++--- pedalboard/plugins/Convolution.h | 3 ++- pedalboard/plugins/HighpassFilter.h | 3 ++- pedalboard/plugins/LadderFilter.h | 8 ++++---- pedalboard/plugins/LowpassFilter.h | 3 ++- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h index 871172bd..ec1add10 100644 --- a/pedalboard/plugins/Chain.h +++ b/pedalboard/plugins/Chain.h @@ -105,7 +105,8 @@ inline void init_chain(py::module &m) { [](std::shared_ptr self, const py::array_t inputArray, double sampleRate, unsigned int bufferSize, bool reset) { - return process(inputArray, sampleRate, self->getPlugins(), bufferSize, reset); + return process(inputArray, sampleRate, self->getPlugins(), + bufferSize, reset); }, "Run a 32-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " @@ -120,8 +121,8 @@ inline void init_chain(py::module &m) { double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); - return process(float32InputArray, sampleRate, self->getPlugins(), bufferSize, - reset); + return process(float32InputArray, sampleRate, self->getPlugins(), + bufferSize, reset); }, "Run a 64-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " diff --git a/pedalboard/plugins/Convolution.h b/pedalboard/plugins/Convolution.h index 2f3daefd..12d654be 100644 --- a/pedalboard/plugins/Convolution.h +++ b/pedalboard/plugins/Convolution.h @@ -78,7 +78,8 @@ class ConvolutionWithMix { }; inline void init_convolution(py::module &m) { - py::class_, Plugin, std::shared_ptr>>( + py::class_, Plugin, + std::shared_ptr>>( m, "Convolution", "An audio convolution, suitable for things like speaker simulation or " "reverb modeling.") diff --git a/pedalboard/plugins/HighpassFilter.h b/pedalboard/plugins/HighpassFilter.h index d6298b9b..98f964b3 100644 --- a/pedalboard/plugins/HighpassFilter.h +++ b/pedalboard/plugins/HighpassFilter.h @@ -41,7 +41,8 @@ class HighpassFilter : public JucePlugin> { }; inline void init_highpass(py::module &m) { - py::class_, Plugin, std::shared_ptr>>( + py::class_, Plugin, + std::shared_ptr>>( m, "HighpassFilter", "Apply a first-order high-pass filter with a roll-off of 6dB/octave. " "The cutoff frequency will be attenuated by -3dB (i.e.: 0.707x as " diff --git a/pedalboard/plugins/LadderFilter.h b/pedalboard/plugins/LadderFilter.h index ccc773a0..e8b703c2 100644 --- a/pedalboard/plugins/LadderFilter.h +++ b/pedalboard/plugins/LadderFilter.h @@ -58,10 +58,10 @@ class LadderFilter : public JucePlugin> { }; inline void init_ladderfilter(py::module &m) { - py::class_, Plugin, std::shared_ptr>> ladderFilter( - m, "LadderFilter", - "Multi-mode audio filter based on the classic Moog synthesizer ladder " - "filter."); + py::class_, Plugin, std::shared_ptr>> + ladderFilter(m, "LadderFilter", + "Multi-mode audio filter based on the classic Moog " + "synthesizer ladder filter."); py::enum_(ladderFilter, "Mode") .value("LPF12", juce::dsp::LadderFilterMode::LPF12, diff --git a/pedalboard/plugins/LowpassFilter.h b/pedalboard/plugins/LowpassFilter.h index 090ccc51..71863fea 100644 --- a/pedalboard/plugins/LowpassFilter.h +++ b/pedalboard/plugins/LowpassFilter.h @@ -41,7 +41,8 @@ class LowpassFilter : public JucePlugin> { }; inline void init_lowpass(py::module &m) { - py::class_, Plugin, std::shared_ptr>>( + py::class_, Plugin, + std::shared_ptr>>( m, "LowpassFilter", "Apply a first-order low-pass filter with a roll-off of 6dB/octave. " "The cutoff frequency will be attenuated by -3dB (i.e.: 0.707x as " From 0d65125e08479ab82e34ea750b742ccdf4add5d5 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Wed, 26 Jan 2022 12:04:27 -0500 Subject: [PATCH 07/13] Python formatting. --- pedalboard/pedalboard.py | 10 +++------- tests/test_mix_and_chain.py | 33 +++++++++++++++++++-------------- tests/test_python_interface.py | 2 -- tests/test_tensorflow.py | 3 +-- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index 8ee60706..c231ad7d 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -14,18 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import collections import platform import weakref from functools import update_wrapper from contextlib import contextmanager -from typing import List, Optional, Dict, Union, Tuple, Set, Iterable, Union +from typing import List, Optional, Dict, Tuple, Iterable, Union -import numpy as np - -from pedalboard_native import Plugin, process, _AudioProcessorParameter -from pedalboard_native.utils import Mix, Chain +from pedalboard_native import Plugin, _AudioProcessorParameter +from pedalboard_native.utils import Chain class Pedalboard(Chain): diff --git a/tests/test_mix_and_chain.py b/tests/test_mix_and_chain.py index c87585f6..4f20e8ac 100644 --- a/tests/test_mix_and_chain.py +++ b/tests/test_mix_and_chain.py @@ -167,14 +167,18 @@ def test_chain_latency_compensation(sample_rate, buffer_size, latency_a_seconds, noise = np.random.rand(int(NUM_SECONDS * sample_rate)) pb = Pedalboard( [ - Chain([ - AddLatency(int(latency_a_seconds * sample_rate)), - AddLatency(int(latency_b_seconds * sample_rate)), - ]), - Chain([ - AddLatency(int(latency_a_seconds * sample_rate)), - AddLatency(int(latency_b_seconds * sample_rate)), - ]), + Chain( + [ + AddLatency(int(latency_a_seconds * sample_rate)), + AddLatency(int(latency_b_seconds * sample_rate)), + ] + ), + Chain( + [ + AddLatency(int(latency_a_seconds * sample_rate)), + AddLatency(int(latency_b_seconds * sample_rate)), + ] + ), ] ) output = pb(noise, sample_rate, buffer_size=buffer_size) @@ -216,11 +220,13 @@ def test_readme_example_does_not_crash(sample_rate, buffer_size): # Put a compressor at the front of the chain: Compressor(), # Split the chain and mix three different effects equally: - Mix([ - Pedalboard([passthrough, Distortion(drive_db=36)]), - Pedalboard([delay_and_pitch_shift, Reverb(room_size=1)]), - delay_longer_and_more_pitch_shift, - ]), + Mix( + [ + Pedalboard([passthrough, Distortion(drive_db=36)]), + Pedalboard([delay_and_pitch_shift, Reverb(room_size=1)]), + delay_longer_and_more_pitch_shift, + ] + ), # Add a reverb on the final mix: Reverb(), ] @@ -281,7 +287,6 @@ def test_empty_list_is_valid_constructor_arg(cls): assert len(cls([])) == 0 - @pytest.mark.parametrize("cls", [Mix, Chain, Pedalboard]) def test_no_arg_constructor(cls): assert len(cls()) == 0 diff --git a/tests/test_python_interface.py b/tests/test_python_interface.py index 82ae34cc..807fc9c8 100644 --- a/tests/test_python_interface.py +++ b/tests/test_python_interface.py @@ -46,7 +46,6 @@ def test_fail_on_invalid_buffer_size(): def test_repr(): - sr = 44100 gain = Gain(-6) value = repr(Pedalboard([gain])) # Allow flexibility; all we care about is that these values exist in the repr. @@ -56,7 +55,6 @@ def test_repr(): def test_is_list_like(): - sr = 44100 gain = Gain(-6) assert len(Pedalboard([gain])) == 1 diff --git a/tests/test_tensorflow.py b/tests/test_tensorflow.py index c84e80ed..f78009f9 100644 --- a/tests/test_tensorflow.py +++ b/tests/test_tensorflow.py @@ -37,8 +37,7 @@ def test_can_be_called_in_tensorflow_data_pipeline(): noise = np.random.rand(sr) ds = tf.data.Dataset.from_tensor_slices([noise]).map( - lambda audio: tf.numpy_function(plugins.process, [audio, sr], tf.float32 - ) + lambda audio: tf.numpy_function(plugins.process, [audio, sr], tf.float32) ) model = tf.keras.models.Sequential( From 54f0129d403f789c15c6e801b6f7f029332ba9b7 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Wed, 26 Jan 2022 13:09:08 -0500 Subject: [PATCH 08/13] Fix test_repr. --- pedalboard/pedalboard.py | 7 ++++++- tests/test_python_interface.py | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pedalboard/pedalboard.py b/pedalboard/pedalboard.py index c231ad7d..67e0e7dd 100644 --- a/pedalboard/pedalboard.py +++ b/pedalboard/pedalboard.py @@ -33,7 +33,12 @@ def __init__(self, plugins: Optional[List[Plugin]] = None): super().__init__(plugins or []) def __repr__(self) -> str: - return "<{} with {} plugins: {}>".format(self.__class__.__name__, len(self), list(self)) + return "<{} with {} plugin{}: {}>".format( + self.__class__.__name__, + len(self), + "" if len(self) == 1 else "s", + list(self), + ) FLOAT_SUFFIXES_TO_IGNORE = set(["x", "%", "*", ",", ".", "hz"]) diff --git a/tests/test_python_interface.py b/tests/test_python_interface.py index 807fc9c8..1cbecf1d 100644 --- a/tests/test_python_interface.py +++ b/tests/test_python_interface.py @@ -50,9 +50,17 @@ def test_repr(): value = repr(Pedalboard([gain])) # Allow flexibility; all we care about is that these values exist in the repr. assert "Pedalboard" in value - assert "plugins=" in value + assert " 1 " in value assert repr(gain) in value + gain2 = Gain(-6) + value = repr(Pedalboard([gain, gain2])) + # Allow flexibility; all we care about is that these values exist in the repr. + assert "Pedalboard" in value + assert " 2 " in value + assert repr(gain) in value + assert repr(gain2) in value + def test_is_list_like(): gain = Gain(-6) From 83a5bbee6265370491bede3088b3c1b4b0588770 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Wed, 26 Jan 2022 13:11:32 -0500 Subject: [PATCH 09/13] Style. --- README.md | 6 ++---- pedalboard/__init__.py | 2 +- tests/test_mix_and_chain.py | 21 +-------------------- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index dc43da49..33642ced 100644 --- a/README.md +++ b/README.md @@ -183,9 +183,7 @@ as much as you like: ```python import soundfile as sf -from pedalboard import \ - Pedalboard, Compressor, Delay, Distortion, \ - Gain, PitchShift, Reverb, Mix +from pedalboard import Pedalboard, Compressor, Delay, Distortion, Gain, PitchShift, Reverb, Mix passthrough = Gain(gain_db=0) @@ -196,7 +194,7 @@ delay_and_pitch_shift = Pedalboard([ ]) delay_longer_and_more_pitch_shift = Pedalboard([ - Delay(delay_seconds=0.25, mix=1.0), + Delay(delay_seconds=0.5, mix=1.0), PitchShift(semitones=12), Gain(gain_db=-6), ]) diff --git a/pedalboard/__init__.py b/pedalboard/__init__.py index 598a67bf..9b3b8048 100644 --- a/pedalboard/__init__.py +++ b/pedalboard/__init__.py @@ -16,7 +16,7 @@ from pedalboard_native import * # noqa: F403, F401 -from pedalboard_native.utils import * # noqa: F401 +from pedalboard_native.utils import * # noqa: F403, F401 from .pedalboard import Pedalboard, AVAILABLE_PLUGIN_CLASSES, load_plugin # noqa: F401 from .version import __version__ # noqa: F401 diff --git a/tests/test_mix_and_chain.py b/tests/test_mix_and_chain.py index 4f20e8ac..d35c4379 100644 --- a/tests/test_mix_and_chain.py +++ b/tests/test_mix_and_chain.py @@ -119,24 +119,6 @@ def test_nesting_pedalboards(): np.testing.assert_allclose(_input * 0.5, output, rtol=0.01) -def test_chain_latency_compensation(): - sr = 44100 - _input = np.random.rand(int(NUM_SECONDS * sr), 2).astype(np.float32) - - pb = Pedalboard( - [ - # This parallel chain should boost by 6dB, but due to - # there being 2 outputs, the result will be +12dB. - Mix([Chain([Gain(1) for _ in range(6)]), Chain([Gain(1) for _ in range(6)])]), - # This parallel chain should cut by -24dB, but due to - # there being 2 outputs, the result will be -18dB. - Mix([Chain([Gain(-1) for _ in range(24)]), Chain([Gain(-1) for _ in range(24)])]), - ] - ) - output = pb(_input, sr) - np.testing.assert_allclose(_input * 0.5, output, rtol=0.01) - - @pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) @pytest.mark.parametrize("buffer_size", [128, 8192, 22050, 65536]) @pytest.mark.parametrize("latency_a_seconds", [0.25, 1, NUM_SECONDS / 2]) @@ -211,7 +193,6 @@ def test_readme_example_does_not_crash(sample_rate, buffer_size): original_plus_delayed_harmonies = Pedalboard( Mix([passthrough, delay_and_pitch_shift, delay_longer_and_more_pitch_shift]) ) - # TODO: Allow passing a Pedalboard into Mix (which will require Pedalboard to be a subclass of Plugin) original_plus_delayed_harmonies(noise, sample_rate=sample_rate, buffer_size=buffer_size) # or mix and match in more complex ways: @@ -251,7 +232,7 @@ def test_pedalboard_is_a_plugin(sample_rate, buffer_size): delay_longer_and_more_pitch_shift = Chain( [ - Delay(delay_seconds=0.25, mix=1.0), + Delay(delay_seconds=0.5, mix=1.0), PitchShift(semitones=12), Gain(gain_db=-6), ] From 974fdb9beb1697c79532f770e258807884bffe35 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Thu, 3 Feb 2022 18:03:56 -0500 Subject: [PATCH 10/13] Re-introduce plugins lost during rebase. --- pedalboard/plugins/Invert.h | 2 +- pedalboard/plugins/MP3Compressor.h | 2 +- pedalboard/python_bindings.cpp | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pedalboard/plugins/Invert.h b/pedalboard/plugins/Invert.h index f830cae0..2965eb55 100644 --- a/pedalboard/plugins/Invert.h +++ b/pedalboard/plugins/Invert.h @@ -34,7 +34,7 @@ template class Invert : public Plugin { }; inline void init_invert(py::module &m) { - py::class_, Plugin>( + py::class_, Plugin, std::shared_ptr>>( m, "Invert", "Flip the polarity of the signal. This effect is not audible on its own.") .def(py::init([]() { return std::make_unique>(); })) diff --git a/pedalboard/plugins/MP3Compressor.h b/pedalboard/plugins/MP3Compressor.h index 133bb8d6..4fc19e4e 100644 --- a/pedalboard/plugins/MP3Compressor.h +++ b/pedalboard/plugins/MP3Compressor.h @@ -360,7 +360,7 @@ class MP3Compressor : public Plugin { }; inline void init_mp3_compressor(py::module &m) { - py::class_( + py::class_>( m, "MP3Compressor", "Apply an MP3 compressor to the audio to reduce its quality.") .def(py::init([](float vbr_quality) { diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index a14b5d22..691b2586 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -47,6 +47,7 @@ namespace py = pybind11; #include "plugins/Limiter.h" #include "plugins/LowpassFilter.h" #include "plugins/Mix.h" +#include "plugins/MP3Compressor.h" #include "plugins/NoiseGate.h" #include "plugins/Phaser.h" #include "plugins/PitchShift.h" From 25be61d7a3476c1dfa0db82e7096caa80067b008 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Thu, 3 Feb 2022 18:18:20 -0500 Subject: [PATCH 11/13] Clang-format. --- pedalboard/python_bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index 691b2586..a2953c89 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -46,8 +46,8 @@ namespace py = pybind11; #include "plugins/LadderFilter.h" #include "plugins/Limiter.h" #include "plugins/LowpassFilter.h" -#include "plugins/Mix.h" #include "plugins/MP3Compressor.h" +#include "plugins/Mix.h" #include "plugins/NoiseGate.h" #include "plugins/Phaser.h" #include "plugins/PitchShift.h" From 6d7a81feb374bacd50e7452cce1c873295b35af1 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Tue, 8 Feb 2022 00:11:32 -0500 Subject: [PATCH 12/13] Fix merge conflicts. --- pedalboard/plugin_templates/FixedBlockSize.h | 4 +++- pedalboard/plugin_templates/ForceMono.h | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pedalboard/plugin_templates/FixedBlockSize.h b/pedalboard/plugin_templates/FixedBlockSize.h index d5afa9f0..16ca0159 100644 --- a/pedalboard/plugin_templates/FixedBlockSize.h +++ b/pedalboard/plugin_templates/FixedBlockSize.h @@ -304,7 +304,9 @@ class FixedSizeBlockTestPlugin : public FixedBlockSize { }; inline void init_fixed_size_block_test_plugin(py::module &m) { - py::class_(m, "FixedSizeBlockTestPlugin") + py::class_>( + m, "FixedSizeBlockTestPlugin") .def(py::init([](int expectedBlockSize) { auto plugin = new FixedSizeBlockTestPlugin(); plugin->setExpectedBlockSize(expectedBlockSize); diff --git a/pedalboard/plugin_templates/ForceMono.h b/pedalboard/plugin_templates/ForceMono.h index 4fa185e3..70ba24aa 100644 --- a/pedalboard/plugin_templates/ForceMono.h +++ b/pedalboard/plugin_templates/ForceMono.h @@ -109,7 +109,8 @@ class ExpectsMono : public AddLatency { using ForceMonoTestPlugin = ForceMono; inline void init_force_mono_test_plugin(py::module &m) { - py::class_(m, "ForceMonoTestPlugin") + py::class_>( + m, "ForceMonoTestPlugin") .def(py::init([]() { return std::make_unique(); })) .def("__repr__", [](const ForceMonoTestPlugin &plugin) { std::ostringstream ss; From a8ad1f379f705a047966a7e7f0fb6f7b8547fd67 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Tue, 8 Feb 2022 08:57:11 -0500 Subject: [PATCH 13/13] Apply shared_ptr changes to new plugins. --- pedalboard/plugin_templates/Resample.h | 16 ++++++++++------ pedalboard/plugins/GSMFullRateCompressor.h | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pedalboard/plugin_templates/Resample.h b/pedalboard/plugin_templates/Resample.h index 4de98949..96ce5f48 100644 --- a/pedalboard/plugin_templates/Resample.h +++ b/pedalboard/plugin_templates/Resample.h @@ -433,11 +433,13 @@ class Resample : public Plugin { }; inline void init_resample(py::module &m) { - py::class_, float>, Plugin> resample( - m, "Resample", - "A plugin that downsamples the input audio to the given sample rate, " - "then upsamples it back to the original sample rate. Various quality " - "settings will produce audible distortion and aliasing effects."); + py::class_, float>, Plugin, + std::shared_ptr, float>>> + resample( + m, "Resample", + "A plugin that downsamples the input audio to the given sample rate, " + "then upsamples it back to the original sample rate. Various quality " + "settings will produce audible distortion and aliasing effects."); py::enum_(resample, "Quality") .value("ZeroOrderHold", ResamplingQuality::ZeroOrderHold, @@ -509,7 +511,9 @@ inline void init_resample(py::module &m) { * signal. */ inline void init_resample_with_latency(py::module &m) { - py::class_, Plugin>(m, "ResampleWithLatency") + py::class_, Plugin, + std::shared_ptr>>( + m, "ResampleWithLatency") .def(py::init([](float targetSampleRate, int internalLatency, ResamplingQuality quality) { auto plugin = std::make_unique>(); diff --git a/pedalboard/plugins/GSMFullRateCompressor.h b/pedalboard/plugins/GSMFullRateCompressor.h index 2c43c0f8..5bf54b89 100644 --- a/pedalboard/plugins/GSMFullRateCompressor.h +++ b/pedalboard/plugins/GSMFullRateCompressor.h @@ -146,7 +146,8 @@ using GSMFullRateCompressor = ForceMono>; inline void init_gsm_full_rate_compressor(py::module &m) { - py::class_( + py::class_>( m, "GSMFullRateCompressor", "Apply an GSM Full Rate compressor to emulate the sound of a GSM Full " "Rate (\"2G\") cellular "