From 0b7e09d6748febddc3de7e2ce1b762461b2f9589 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 21 Jan 2022 18:05:36 -0500 Subject: [PATCH 1/8] Add latency compensation support. --- pedalboard/ExternalPlugin.h | 23 ++- pedalboard/JucePlugin.h | 7 +- pedalboard/Plugin.h | 37 +++- pedalboard/plugins/DelayLine.h | 69 +++++++ pedalboard/process.h | 293 +++++++++++++++++++++-------- pedalboard/python_bindings.cpp | 5 + tests/test_external_plugins.py | 32 ++++ tests/test_latency_compensation.py | 31 +++ 8 files changed, 415 insertions(+), 82 deletions(-) create mode 100644 pedalboard/plugins/DelayLine.h create mode 100644 tests/test_latency_compensation.py diff --git a/pedalboard/ExternalPlugin.h b/pedalboard/ExternalPlugin.h index ea2762d6..76908577 100644 --- a/pedalboard/ExternalPlugin.h +++ b/pedalboard/ExternalPlugin.h @@ -514,6 +514,7 @@ template class ExternalPlugin : public Plugin { // Force prepare() to be called again later by invalidating lastSpec: lastSpec.maximumBlockSize = 0; + samplesProvided = 0; } } @@ -543,8 +544,8 @@ template class ExternalPlugin : public Plugin { } } - void - process(const juce::dsp::ProcessContextReplacing &context) override { + int process( + const juce::dsp::ProcessContextReplacing &context) override { if (pluginInstance) { juce::MidiBuffer emptyMidiBuffer; @@ -614,7 +615,17 @@ template class ExternalPlugin : public Plugin { pluginBufferChannelCount, outputBlock.getNumSamples()); pluginInstance->processBlock(audioBuffer, emptyMidiBuffer); + samplesProvided += outputBlock.getNumSamples(); + + // To compensate for any latency added by the plugin, + // only tell Pedalboard to use the last _n_ samples. + long usableSamplesProduced = + samplesProvided - pluginInstance->getLatencySamples(); + return (int)std::min(usableSamplesProduced, + (long)outputBlock.getNumSamples()); } + + return 0; } std::vector getParameters() const { @@ -634,6 +645,12 @@ template class ExternalPlugin : public Plugin { return nullptr; } + virtual int getLatencyHint() override { + if (!pluginInstance) + return 0; + return pluginInstance->getLatencySamples(); + } + private: constexpr static int ExternalLoadSampleRate = 44100, ExternalLoadMaximumBlockSize = 8192; @@ -642,6 +659,8 @@ template class ExternalPlugin : public Plugin { juce::AudioPluginFormatManager pluginFormatManager; std::unique_ptr pluginInstance; + long samplesProvided = 0; + ExternalPluginReloadType reloadType = ExternalPluginReloadType::Unknown; }; diff --git a/pedalboard/JucePlugin.h b/pedalboard/JucePlugin.h index 6fad019c..8c2526aa 100644 --- a/pedalboard/JucePlugin.h +++ b/pedalboard/JucePlugin.h @@ -58,12 +58,13 @@ template class JucePlugin : public Plugin { } } - void process( - const juce::dsp::ProcessContextReplacing &context) override final { + int process( + const juce::dsp::ProcessContextReplacing &context) override { dspBlock.process(context); + return context.getOutputBlock().getNumSamples(); } - void reset() override final { dspBlock.reset(); } + void reset() override { dspBlock.reset(); } DSPType &getDSP() { return dspBlock; }; diff --git a/pedalboard/Plugin.h b/pedalboard/Plugin.h index e9ce6f82..5acc11c4 100644 --- a/pedalboard/Plugin.h +++ b/pedalboard/Plugin.h @@ -28,13 +28,48 @@ class Plugin { public: virtual ~Plugin(){}; + /** + * Prepare the data structures that will be necessary for this plugin to + * process audio at the provided sample rate, maximum block size, and number + * of channels. + */ virtual void prepare(const juce::dsp::ProcessSpec &spec) = 0; - virtual void + /** + * Process a single buffer of audio through this plugin. + * Returns the number of samples that were output. + * + * If less than a whole buffer of audio was output, the samples that + * were produced should be right-aligned in the buffer + * (i.e.: they should come last). + */ + virtual int process(const juce::dsp::ProcessContextReplacing &context) = 0; + /** + * Reset this plugin's state, clearing any internal buffers or delay lines. + */ virtual void reset() = 0; + /** + * Get the number of samples of latency introduced by this plugin. + * This is the number of samples that must be provided to the plugin + * before meaningful output will be returned. + * Pedalboard will automatically compensate for this latency when processing + * by using the return value from the process() call, but this hint can + * make processing more efficient. + * + * This function will only be called after prepare(), so it can take into + * account variables like the current sample rate, maximum block size, and + * other plugin parameters. + * + * Returning a value from getLatencyHint() that is larger than necessary will + * allocate that many extra samples during processing, increasing memory usage. + * Returning a value that is too small will cause memory to be reallocated + * during rendering, impacting rendering speed. + */ + virtual int getLatencyHint() { return 0; } + // 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/plugins/DelayLine.h b/pedalboard/plugins/DelayLine.h new file mode 100644 index 00000000..09025dee --- /dev/null +++ b/pedalboard/plugins/DelayLine.h @@ -0,0 +1,69 @@ +/* + * pedalboard + * Copyright 2022 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. + */ + +#include "../JucePlugin.h" + +namespace Pedalboard { + +/** + * A dummy plugin that buffers audio data internally, used to test Pedalboard's + * automatic delay compensation. + */ +class DelayLine : public JucePlugin> { +public: + virtual ~DelayLine(){}; + + virtual void reset() override { + getDSP().reset(); + samplesProvided = 0; + } + + virtual int + process(const juce::dsp::ProcessContextReplacing &context) override { + getDSP().process(context); + int blockSize = context.getInputBlock().getNumSamples(); + samplesProvided += blockSize; + + return std::min((int)blockSize, + std::max(0, (int)(samplesProvided - getDSP().getDelay()))); + } + + virtual int getLatencyHint() override { + return getDSP().getDelay(); + } + +private: + int samplesProvided = 0; +}; + +inline void init_delay_line(py::module &m) { + py::class_( + m, "DelayLine", + "A dummy plugin that delays input audio for the given number of samples " + "before passing it back to the output. Used internally to test " + "Pedalboard's automatic latency compensation. Probably not useful as a " + "real effect.") + .def(py::init([](int samples) { + auto dl = new DelayLine(); + dl->getDSP().setMaximumDelayInSamples(samples); + dl->getDSP().setDelay(samples); + return dl; + }), + py::arg("samples") = 44100); +} +}; // namespace Pedalboard diff --git a/pedalboard/process.h b/pedalboard/process.h index fb1f3a25..83f13574 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -57,40 +57,51 @@ processSingle(const py::array_t inputArray, reset); } -/** - * Process a given audio buffer through a list of - * Pedalboard plugins at a given sample rate. - * Only supports float processing, not double, at the moment. - */ -template <> -py::array_t -process(const py::array_t inputArray, - double sampleRate, const std::vector &plugins, - unsigned int bufferSize, bool reset) { +template +ChannelLayout +detectChannelLayout(const py::array_t inputArray) { + py::buffer_info inputInfo = inputArray.request(); + + if (inputInfo.ndim == 1) { + return ChannelLayout::Interleaved; + } else if (inputInfo.ndim == 2) { + // Try to auto-detect the channel layout from the shape + if (inputInfo.shape[1] < inputInfo.shape[0]) { + return ChannelLayout::Interleaved; + } else if (inputInfo.shape[0] < inputInfo.shape[1]) { + return ChannelLayout::NotInterleaved; + } else { + throw std::runtime_error( + "Unable to determine channel layout from shape!"); + } + } else { + throw std::runtime_error("Number of input dimensions must be 1 or 2."); + } +} + +template +juce::AudioBuffer +copyPyArrayIntoJuceBuffer(const py::array_t inputArray) { // Numpy/Librosa convention is (num_samples, num_channels) py::buffer_info inputInfo = inputArray.request(); unsigned int numChannels = 0; unsigned int numSamples = 0; - ChannelLayout inputChannelLayout; + ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); if (inputInfo.ndim == 1) { numSamples = inputInfo.shape[0]; numChannels = 1; - inputChannelLayout = ChannelLayout::Interleaved; } else if (inputInfo.ndim == 2) { // Try to auto-detect the channel layout from the shape if (inputInfo.shape[1] < inputInfo.shape[0]) { numSamples = inputInfo.shape[0]; numChannels = inputInfo.shape[1]; - inputChannelLayout = ChannelLayout::Interleaved; } else if (inputInfo.shape[0] < inputInfo.shape[1]) { numSamples = inputInfo.shape[1]; numChannels = inputInfo.shape[0]; - inputChannelLayout = ChannelLayout::NotInterleaved; } else { - throw std::runtime_error( - "Unable to determine channel layout from shape!"); + throw std::runtime_error("Unable to determine shape of audio input!"); } } else { throw std::runtime_error("Number of input dimensions must be 1 or 2."); @@ -102,19 +113,105 @@ process(const py::array_t inputArray, throw std::runtime_error("More than two channels received!"); } - // Cap the buffer size in use to the size of the input data: - bufferSize = std::min(bufferSize, numSamples); + juce::AudioBuffer ioBuffer(numChannels, numSamples); + + // Depending on the input channel layout, we need to copy data + // differently. This loop is duplicated here to move the if statement + // outside of the tight loop, as we don't need to re-check that the input + // channel is still the same on every iteration of the loop. + switch (inputChannelLayout) { + case ChannelLayout::Interleaved: + for (unsigned int i = 0; i < numChannels; i++) { + T *channelBuffer = ioBuffer.getWritePointer(i); + // We're de-interleaving the data here, so we can't use copyFrom. + for (unsigned int j = 0; j < numSamples; j++) { + channelBuffer[j] = static_cast(inputInfo.ptr)[j * numChannels + i]; + } + } + break; + case ChannelLayout::NotInterleaved: + for (unsigned int i = 0; i < numChannels; i++) { + ioBuffer.copyFrom( + i, 0, static_cast(inputInfo.ptr) + (numSamples * i), numSamples); + } + } + + return ioBuffer; +} + +template +py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, + ChannelLayout channelLayout, + int offsetSamples, int ndim = 2) { + unsigned int numChannels = juceBuffer.getNumChannels(); + unsigned int numSamples = juceBuffer.getNumSamples(); + unsigned int outputSampleCount = + std::max((int)numSamples - (int)offsetSamples, 0); + + // TODO: Avoid the need to copy here if offsetSamples is 0! + py::array_t outputArray; + if (ndim == 2) { + switch (channelLayout) { + case ChannelLayout::Interleaved: + outputArray = py::array_t({outputSampleCount, numChannels}); + break; + case ChannelLayout::NotInterleaved: + outputArray = py::array_t({numChannels, outputSampleCount}); + break; + } + } else { + outputArray = py::array_t(outputSampleCount); + } - // JUCE uses separate channel buffers, so the output shape is (num_channels, - // num_samples) - py::array_t outputArray = - inputInfo.ndim == 2 ? py::array_t({numChannels, numSamples}) - : py::array_t(numSamples); py::buffer_info outputInfo = outputArray.request(); + // Depending on the input channel layout, we need to copy data + // differently. This loop is duplicated here to move the if statement + // outside of the tight loop, as we don't need to re-check that the input + // channel is still the same on every iteration of the loop. + T *outputBasePointer = static_cast(outputInfo.ptr); + + switch (channelLayout) { + case ChannelLayout::Interleaved: + for (unsigned int i = 0; i < numChannels; i++) { + const T *channelBuffer = juceBuffer.getReadPointer(i, offsetSamples); + // We're interleaving the data here, so we can't use copyFrom. + for (unsigned int j = 0; j < outputSampleCount; j++) { + outputBasePointer[j * numChannels + i] = channelBuffer[j]; + } + } + break; + case ChannelLayout::NotInterleaved: + for (unsigned int i = 0; i < numChannels; i++) { + const T *channelBuffer = juceBuffer.getReadPointer(i, offsetSamples); + std::copy(channelBuffer, channelBuffer + outputSampleCount, + &outputBasePointer[outputSampleCount * i]); + } + break; + } + + return outputArray; +} + +/** + * Process a given audio buffer through a list of + * Pedalboard plugins at a given sample rate. + * Only supports float processing, not double, at the moment. + */ +template <> +py::array_t +process(const py::array_t inputArray, + double sampleRate, const std::vector &plugins, + unsigned int bufferSize, bool reset) { + const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); + juce::AudioBuffer ioBuffer = copyPyArrayIntoJuceBuffer(inputArray); + int totalOutputLatencySamples = 0; + { py::gil_scoped_release release; + bufferSize = std::min(bufferSize, (unsigned int)ioBuffer.getNumSamples()); + unsigned int countOfPluginsIgnoringNull = 0; for (auto *plugin : plugins) { if (plugin == nullptr) @@ -165,75 +262,119 @@ process(const py::array_t inputArray, juce::dsp::ProcessSpec spec; spec.sampleRate = sampleRate; spec.maximumBlockSize = static_cast(bufferSize); - spec.numChannels = static_cast(numChannels); + spec.numChannels = static_cast(ioBuffer.getNumChannels()); + + int expectedOutputLatency = 0; for (auto *plugin : plugins) { if (plugin == nullptr) continue; plugin->prepare(spec); + expectedOutputLatency += plugin->getLatencyHint(); } - // Manually construct channel pointers to pass to AudioBuffer. - std::vector ioBufferChannelPointers(numChannels); - for (unsigned int i = 0; i < numChannels; i++) { - ioBufferChannelPointers[i] = ((float *)outputInfo.ptr) + (i * numSamples); + 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; - juce::AudioBuffer ioBuffer(ioBufferChannelPointers.data(), - numChannels, numSamples); - - for (unsigned int blockStart = 0; blockStart < numSamples; - blockStart += bufferSize) { - unsigned int blockEnd = std::min(blockStart + bufferSize, - static_cast(numSamples)); - unsigned int blockSize = blockEnd - blockStart; - - // Copy the input audio into the ioBuffer, which will be used for - // processing and will be returned. - - // Depending on the input channel layout, we need to copy data - // differently. This loop is duplicated here to move the if statement - // outside of the tight loop, as we don't need to re-check that the input - // channel is still the same on every iteration of the loop. - switch (inputChannelLayout) { - case ChannelLayout::Interleaved: - for (unsigned int i = 0; i < numChannels; i++) { - // We're de-interleaving the data here, so we can't use std::copy. - for (unsigned int j = blockStart; j < blockEnd; j++) { - ioBufferChannelPointers[i][j] = - static_cast(inputInfo.ptr)[j * numChannels + i]; + int pluginSamplesReceived = 0; + + for (unsigned int blockStart = startOfOutputInBuffer; + blockStart < (unsigned int)ioBuffer.getNumSamples(); + blockStart += bufferSize) { + unsigned int blockEnd = + std::min(blockStart + bufferSize, + static_cast(ioBuffer.getNumSamples())); + 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); + 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); } } - break; - case ChannelLayout::NotInterleaved: - for (unsigned int i = 0; i < numChannels; i++) { - const float *channelBuffer = - static_cast(inputInfo.ptr) + (i * numSamples); - std::copy(channelBuffer + blockStart, channelBuffer + blockEnd, - ioBufferChannelPointers[i] + blockStart); - } - } - auto ioBlock = juce::dsp::AudioBlock( - ioBufferChannelPointers.data(), numChannels, blockStart, blockSize); - juce::dsp::ProcessContextReplacing context(ioBlock); + lastSampleInBuffer = + std::max(lastSampleInBuffer, (int)(blockStart + outputSamples)); + startOfOutputInBuffer += missingSamples; + totalOutputLatencySamples += missingSamples; - // Now all of the pointers in context are pointing to valid input data, - // so let's run the plugins. - for (auto *plugin : plugins) { - if (plugin == nullptr) - continue; - plugin->process(context); + 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 (intendedOutputBufferSize > ioBuffer.getNumSamples()) { + ioBuffer.setSize(ioBuffer.getNumChannels(), + intendedOutputBufferSize, + /* keepExistingContent= */ true, + /* clearExtraSpace= */ true); + } + } } } - } - switch (inputChannelLayout) { - case ChannelLayout::Interleaved: - return outputArray.attr("transpose")(); - case ChannelLayout::NotInterleaved: - default: - return outputArray; + // Trim the output buffer down to size; this operation should be free. + jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); + ioBuffer.setSize(ioBuffer.getNumChannels(), + intendedOutputBufferSize, + /* keepExistingContent= */ true, + /* clearExtraSpace= */ true, + /* avoidReallocating= */ true); } + + 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 ae0ed8a3..f9b6e41b 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -35,6 +35,7 @@ namespace py = pybind11; #include "plugins/Chorus.h" #include "plugins/Compressor.h" #include "plugins/Convolution.h" +#include "plugins/DelayLine.h" #include "plugins/Distortion.h" #include "plugins/Gain.h" #include "plugins/HighpassFilter.h" @@ -147,4 +148,8 @@ PYBIND11_MODULE(pedalboard_native, m) { init_reverb(m); init_external_plugins(m); + + // Internal plugins for testing, debugging, etc: + py::module internal = m.def_submodule("_internal"); + init_delay_line(internal); }; diff --git a/tests/test_external_plugins.py b/tests/test_external_plugins.py index 7f1d0143..6b7bd426 100644 --- a/tests/test_external_plugins.py +++ b/tests/test_external_plugins.py @@ -33,6 +33,7 @@ import pytest import pedalboard import numpy as np +from typing import Optional TEST_PLUGIN_BASE_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "plugins") @@ -144,6 +145,16 @@ def delete_installed_plugins(): pass +def plugin_named(*substrings: str) -> Optional[str]: + """ + Return the first plugin filename that contains all of the + provided substrings from the list of available test plugins. + """ + for plugin_filename in AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT: + if all([s in plugin_filename for s in substrings]): + return plugin_filename + + def max_volume_of(x: np.ndarray) -> float: return np.amax(np.abs(x)) @@ -589,3 +600,24 @@ def test_wrapped_bool_requires_bool(): ) def test_parameter_name_normalization(_input: str, expected: str): assert normalize_python_parameter_name(_input) == expected + + +@pytest.mark.skipif(not plugin_named("CHOWTapeModel"), reason="Missing CHOWTapeModel plugin.") +@pytest.mark.parametrize("buffer_size", [128, 8192, 65536]) +@pytest.mark.parametrize("oversampling", [1, 2, 4, 8, 16]) +def test_external_plugin_latency_compensation(buffer_size: int, oversampling: int): + """ + This test loads CHOWTapeModel (which has non-zero latency due + to an internal oversampler), puts it into Bypass mode, then + ensures that the input matches the output exactly. + """ + num_seconds = 10.0 + sample_rate = 48000 + noise = np.random.rand(int(num_seconds * sample_rate)) + + plugin = load_test_plugin(plugin_named("CHOWTapeModel"), disable_caching=True) + plugin.bypass = True + plugin.oversampling = oversampling + + output = plugin.process(noise, sample_rate, buffer_size=buffer_size) + np.testing.assert_allclose(output, noise, atol=0.05) diff --git a/tests/test_latency_compensation.py b/tests/test_latency_compensation.py new file mode 100644 index 00000000..1af267ac --- /dev/null +++ b/tests/test_latency_compensation.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# +# Copyright 2022 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_native._internal import DelayLine + + +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [128, 8192, 65536]) +@pytest.mark.parametrize("latency_seconds", [0.25, 1, 2, 10]) +def test_latency_compensation(sample_rate, buffer_size, latency_seconds): + num_seconds = 10.0 + noise = np.random.rand(int(num_seconds * sample_rate)) + plugin = DelayLine(int(latency_seconds * sample_rate)) + output = plugin.process(noise, sample_rate, buffer_size=buffer_size) + np.testing.assert_allclose(output, noise) From 54dfe870206cf11b2540f0a476140f8069d91b0e Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 21 Jan 2022 18:06:21 -0500 Subject: [PATCH 2/8] Rewrite Rubber Band integration to support latency compensation. --- pedalboard/RubberbandPlugin.h | 208 ++++++++++++++++++++++++-------- pedalboard/plugins/PitchShift.h | 31 +++-- tests/test_pitch_shift.py | 35 +++++- 3 files changed, 204 insertions(+), 70 deletions(-) diff --git a/pedalboard/RubberbandPlugin.h b/pedalboard/RubberbandPlugin.h index 18fe129e..16caf7ba 100644 --- a/pedalboard/RubberbandPlugin.h +++ b/pedalboard/RubberbandPlugin.h @@ -1,85 +1,191 @@ +/* + * pedalboard + * Copyright 2022 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. + */ + #include "../vendors/rubberband/rubberband/RubberBandStretcher.h" #include "Plugin.h" using namespace RubberBand; namespace Pedalboard { -class RubberbandPlugin : public Plugin + /* -Base class for rubberband plugins. -*/ -{ + * Base class for all Rubber Band-derived plugins. + */ +class RubberbandPlugin : public Plugin { public: virtual ~RubberbandPlugin(){}; - void process( + virtual void prepare(const juce::dsp::ProcessSpec &spec) override { + bool specChanged = lastSpec.sampleRate != spec.sampleRate || + lastSpec.maximumBlockSize < spec.maximumBlockSize || + spec.numChannels != lastSpec.numChannels; + + if (!rbPtr || specChanged) { + auto stretcherOptions = RubberBandStretcher::OptionProcessRealTime | + RubberBandStretcher::OptionThreadingNever | + RubberBandStretcher::OptionChannelsTogether | + RubberBandStretcher::OptionPitchHighQuality; + rbPtr = std::make_unique( + spec.sampleRate, spec.numChannels, stretcherOptions); + rbPtr->setMaxProcessSize(spec.maximumBlockSize); + + lastSpec = spec; + reset(); + } + } + + int process( const juce::dsp::ProcessContextReplacing &context) override final { - if (rbPtr) { - auto inBlock = context.getInputBlock(); - auto outBlock = context.getOutputBlock(); + if (!rbPtr) { + throw std::runtime_error("Rubber Band plugin failed to instantiate."); + } + + auto ioBlock = context.getOutputBlock(); + auto numChannels = ioBlock.getNumChannels(); + + if (numChannels > MAX_CHANNEL_COUNT) { + throw std::runtime_error( + "Pitch shifting or time stretching plugins support a maximum of " + + std::to_string(MAX_CHANNEL_COUNT) + " channels."); + } + + float *ioChannels[MAX_CHANNEL_COUNT] = {}; + + // for (size_t i = 0; i < numChannels; i++) { + // ioChannels[i] = ioBlock.getChannelPointer(i); + // } - auto len = inBlock.getNumSamples(); - auto numChannels = inBlock.getNumChannels(); + // Push all of the input samples we have into RubberBand: + // rbPtr->process(ioChannels, ioBlock.getNumSamples(), false); + // printf("Pushed %d samples into Rubber Band.\n", ioBlock.getNumSamples()); + // int availableSamples = rbPtr->available(); - jassert(len == outBlock.getNumSamples()); - jassert(numChannels == outBlock.getNumChannels()); + int samplesProcessed = 0; + int samplesWritten = 0; + while (samplesProcessed < ioBlock.getNumSamples() || rbPtr->available()) { + int samplesRequired = rbPtr->getSamplesRequired(); + int inputChunkLength = std::min( + (int)(ioBlock.getNumSamples() - samplesProcessed), samplesRequired); - const float **inChannels = - (const float **)alloca(numChannels * sizeof(float *)); - float **outChannels = (float **)alloca(numChannels * sizeof(float *)); + for (size_t i = 0; i < numChannels; i++) { + ioChannels[i] = ioBlock.getChannelPointer(i) + samplesProcessed; + } + + rbPtr->process(ioChannels, inputChunkLength, false); + samplesProcessed += inputChunkLength; + + int samplesAvailable = rbPtr->available(); + int freeSpace = ioBlock.getNumSamples() - samplesWritten; + int outputChunkLength = std::min(freeSpace, samplesAvailable); + + // Avoid overwriting input that hasn't yet been passed to Rubber Band: + if (samplesWritten + outputChunkLength > samplesProcessed) { + outputChunkLength = samplesProcessed - samplesWritten; + } for (size_t i = 0; i < numChannels; i++) { - inChannels[i] = inBlock.getChannelPointer(i); - outChannels[i] = outBlock.getChannelPointer(i); + ioChannels[i] = ioBlock.getChannelPointer(i) + samplesWritten; } + samplesWritten += rbPtr->retrieve(ioChannels, outputChunkLength); + if (samplesWritten == ioBlock.getNumSamples()) + break; + } - // Rubberband expects all channel data with one float array per channel - processSamples(inChannels, outChannels, len, numChannels); + if (samplesWritten > 0 && samplesWritten < ioBlock.getNumSamples()) { + // Right-align the output samples in the buffer: + int offset = ioBlock.getNumSamples() - samplesWritten; + for (size_t i = 0; i < numChannels; i++) { + float *channelBufferSource = ioBlock.getChannelPointer(i); + float *channelBufferDestination = channelBufferSource + offset; + std::memmove((char *)channelBufferDestination, + (char *)channelBufferSource, + sizeof(float) * samplesWritten); + } } + + return samplesWritten; + + // Don't produce any output for this input if RubberBand isn't ready. + // We can do this here because RubberBand buffers audio internally; + // this might not be a safe technique to use with other plugins, as + // their output sample buffers may overflow if passed more than + // maximumBlockSize. + + // if (rbPtr->available() < (int) ioBlock.getNumSamples() + getLatency()) { + // printf("Need %d samples, but Rubber Band only had %d samples + // available.\n", ioBlock.getNumSamples() + getLatency(), + // rbPtr->available()); return 0; + // } else { + // // Pull the next chunk of audio data out of RubberBand: + // int returned = rbPtr->retrieve(ioChannels, ioBlock.getNumSamples()); + // printf("Sent %d samples into Rubber Band, and took %d samples back + // out.\n", ioBlock.getNumSamples(), returned); return returned; + // } + + // // ...but only actually ask Rubberband for at most the number of samples + // we + // // can handle: + // int samplesToPull = ioBlock.getNumSamples(); + // if (samplesToPull > availableSamples) + // samplesToPull = availableSamples; + + // // If we don't have enough samples to fill a full buffer, + // // right-align the samples that we do have (i..e: start with silence). + // int missingSamples = ioBlock.getNumSamples() - availableSamples; + // if (missingSamples > 0) { + // for (size_t c = 0; c < numChannels; c++) { + // // Clear the start of the buffer so that we start + // // the buffer with silence: + // std::fill_n(ioChannels[c], missingSamples, 0.0); + + // // Move the output buffer pointer forward so that + // // RubberBandStretcher::retrieve(...) places its + // // output at the end of the buffer: + // ioChannels[c] += missingSamples; + // } + // } + + // // Pull the next audio data out of Rubberband: + // int pulled = rbPtr->retrieve(ioChannels, samplesToPull); + // printf("Pulled %d samples out of Rubber Band (%d were available).\n", + // pulled, availableSamples); return pulled; } void reset() override final { if (rbPtr) { rbPtr->reset(); } + initialSamplesRequired = 0; } -private: - void processSamples(const float *const *inBlock, float **outBlock, - size_t samples, size_t numChannels) { - // Push all of the input samples into RubberBand: - rbPtr->process(inBlock, samples, false); - - // Figure out how many samples RubberBand is ready to give to us: - int availableSamples = rbPtr->available(); - - // ...but only actually ask Rubberband for at most the number of samples we - // can handle: - int samplesToPull = samples; - if (samplesToPull > availableSamples) - samplesToPull = availableSamples; - - // If we don't have enough samples to fill a full buffer, - // right-align the samples that we do have (i..e: start with silence). - int missingSamples = samples - availableSamples; - if (missingSamples > 0) { - for (size_t c = 0; c < numChannels; c++) { - // Clear the start of the buffer so that we start - // the buffer with silence: - std::fill_n(outBlock[c], missingSamples, 0.0); - - // Move the output buffer pointer forward so that - // RubberBandStretcher::retrieve(...) places its - // output at the end of the buffer: - outBlock[c] += missingSamples; - } - } + virtual int getLatencyHint() override { + if (!rbPtr) + return 0; + + initialSamplesRequired = + std::max(initialSamplesRequired, + (int)(rbPtr->getSamplesRequired() + rbPtr->getLatency() + lastSpec.maximumBlockSize)); - // Pull the next audio data out of Rubberband: - rbPtr->retrieve(outBlock, samplesToPull); + return initialSamplesRequired; } protected: std::unique_ptr rbPtr; + int initialSamplesRequired = 0; + static constexpr int MAX_CHANNEL_COUNT = 8; }; }; // namespace Pedalboard diff --git a/pedalboard/plugins/PitchShift.h b/pedalboard/plugins/PitchShift.h index bd21542b..49f0863a 100644 --- a/pedalboard/plugins/PitchShift.h +++ b/pedalboard/plugins/PitchShift.h @@ -23,20 +23,29 @@ namespace py = pybind11; #include "../RubberbandPlugin.h" namespace Pedalboard { -class PitchShift : public RubberbandPlugin /* Modifies pitch of an audio without affecting duration */ +class PitchShift : public RubberbandPlugin + { private: double _scaleFactor = 1.0; + // Allow pitch shifting by up to 6 octaves up or down: + static constexpr float MIN_SCALE_FACTOR = 0.015625; + static constexpr float MAX_SCALE_FACTOR = 64.0; + public: void setScaleFactor(double scale) { - if (scale <= 0) { - throw std::range_error("Pitch scale must be a value greater than 0.0."); + if (scale < MIN_SCALE_FACTOR || scale > MAX_SCALE_FACTOR) { + throw std::range_error("Scale factor must be a value between " + + std::to_string(MIN_SCALE_FACTOR) + " and " + + std::to_string(MAX_SCALE_FACTOR) + "."); } + _scaleFactor = scale; + if (rbPtr) rbPtr->setPitchScale(_scaleFactor); } @@ -44,20 +53,8 @@ Modifies pitch of an audio without affecting duration double getScaleFactor() { return _scaleFactor; } void prepare(const juce::dsp::ProcessSpec &spec) override final { - bool specChanged = lastSpec.sampleRate != spec.sampleRate || - lastSpec.maximumBlockSize < spec.maximumBlockSize || - spec.numChannels != lastSpec.numChannels; - - if (!rbPtr || specChanged) { - auto stretcherOptions = RubberBandStretcher::OptionProcessRealTime | - RubberBandStretcher::OptionThreadingNever; - rbPtr = std::make_unique( - spec.sampleRate, spec.numChannels, stretcherOptions); - rbPtr->setMaxProcessSize(spec.maximumBlockSize); - rbPtr->setPitchScale(_scaleFactor); - rbPtr->reset(); - lastSpec = spec; - } + RubberbandPlugin::prepare(spec); + rbPtr->setPitchScale(_scaleFactor); } }; diff --git a/tests/test_pitch_shift.py b/tests/test_pitch_shift.py index 3685a36e..e91e477a 100644 --- a/tests/test_pitch_shift.py +++ b/tests/test_pitch_shift.py @@ -17,10 +17,10 @@ import pytest import numpy as np -from pedalboard import PitchShift +from pedalboard import Pedalboard, PitchShift -@pytest.mark.parametrize("scale", [0.5, 1.0, 2.0]) +@pytest.mark.parametrize("scale", [1 / 2, 1.0, 2.0]) @pytest.mark.parametrize("fundamental_hz", [440, 880]) @pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) def test_pitch_shift(scale, fundamental_hz, sample_rate): @@ -31,3 +31,34 @@ def test_pitch_shift(scale, fundamental_hz, sample_rate): output = plugin.process(sine_wave, sample_rate) assert np.all(np.isfinite(output)) + + +@pytest.mark.parametrize("scale", [0.01, 65.0]) +def test_pitch_shift_extremes_throws_errors(scale): + with pytest.raises(ValueError): + PitchShift(scale) + + +@pytest.mark.parametrize("scale", [1 / 64, 1 / 8, 1 / 2, 2, 8, 64]) +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [32, 512, 8192]) +def test_pitch_shift_extremes(scale, sample_rate, buffer_size): + noise = np.random.rand(int(5.0 * sample_rate)) + plugin = PitchShift(scale) + output = plugin.process(noise, sample_rate, buffer_size=buffer_size) + assert np.all(np.isfinite(output)) + + +@pytest.mark.parametrize("scale", [1.0]) +@pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) +@pytest.mark.parametrize("buffer_size", [512, 8192]) +def test_pitch_shift_latency_compensation(scale, sample_rate, buffer_size): + num_seconds = 10.0 + fundamental_hz = 440.0 + samples = np.arange(num_seconds * sample_rate) + sine_wave = np.sin(2 * np.pi * fundamental_hz * samples / sample_rate) + plugin = Pedalboard([PitchShift(scale), PitchShift(1 / scale)]) + output = plugin.process(sine_wave, sample_rate, buffer_size=buffer_size) + np.testing.assert_allclose( + sine_wave[sample_rate:-sample_rate], output[sample_rate:-sample_rate], rtol=0.01, atol=0.01 + ) From 86500d1835b00ec6a33fb50cbd562cd8f0dc293c Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 21 Jan 2022 18:19:46 -0500 Subject: [PATCH 3/8] Revert some Rubberband changes. --- pedalboard/Plugin.h | 8 +- pedalboard/RubberbandPlugin.h | 162 +++++++++++---------------------- pedalboard/plugins/DelayLine.h | 4 +- pedalboard/process.h | 7 +- 4 files changed, 61 insertions(+), 120 deletions(-) diff --git a/pedalboard/Plugin.h b/pedalboard/Plugin.h index 5acc11c4..3dff016f 100644 --- a/pedalboard/Plugin.h +++ b/pedalboard/Plugin.h @@ -62,11 +62,11 @@ class Plugin { * This function will only be called after prepare(), so it can take into * account variables like the current sample rate, maximum block size, and * other plugin parameters. - * + * * Returning a value from getLatencyHint() that is larger than necessary will - * allocate that many extra samples during processing, increasing memory usage. - * Returning a value that is too small will cause memory to be reallocated - * during rendering, impacting rendering speed. + * allocate that many extra samples during processing, increasing memory + * usage. Returning a value that is too small will cause memory to be + * reallocated during rendering, impacting rendering speed. */ virtual int getLatencyHint() { return 0; } diff --git a/pedalboard/RubberbandPlugin.h b/pedalboard/RubberbandPlugin.h index 16caf7ba..05553a21 100644 --- a/pedalboard/RubberbandPlugin.h +++ b/pedalboard/RubberbandPlugin.h @@ -50,142 +50,86 @@ class RubberbandPlugin : public Plugin { int process( const juce::dsp::ProcessContextReplacing &context) override final { - if (!rbPtr) { - throw std::runtime_error("Rubber Band plugin failed to instantiate."); - } - - auto ioBlock = context.getOutputBlock(); - auto numChannels = ioBlock.getNumChannels(); - - if (numChannels > MAX_CHANNEL_COUNT) { - throw std::runtime_error( - "Pitch shifting or time stretching plugins support a maximum of " + - std::to_string(MAX_CHANNEL_COUNT) + " channels."); - } - - float *ioChannels[MAX_CHANNEL_COUNT] = {}; - - // for (size_t i = 0; i < numChannels; i++) { - // ioChannels[i] = ioBlock.getChannelPointer(i); - // } - - // Push all of the input samples we have into RubberBand: - // rbPtr->process(ioChannels, ioBlock.getNumSamples(), false); - // printf("Pushed %d samples into Rubber Band.\n", ioBlock.getNumSamples()); - // int availableSamples = rbPtr->available(); - - int samplesProcessed = 0; - int samplesWritten = 0; - while (samplesProcessed < ioBlock.getNumSamples() || rbPtr->available()) { - int samplesRequired = rbPtr->getSamplesRequired(); - int inputChunkLength = std::min( - (int)(ioBlock.getNumSamples() - samplesProcessed), samplesRequired); - - for (size_t i = 0; i < numChannels; i++) { - ioChannels[i] = ioBlock.getChannelPointer(i) + samplesProcessed; - } + if (rbPtr) { + auto inBlock = context.getInputBlock(); + auto outBlock = context.getOutputBlock(); - rbPtr->process(ioChannels, inputChunkLength, false); - samplesProcessed += inputChunkLength; + auto len = inBlock.getNumSamples(); + auto numChannels = inBlock.getNumChannels(); - int samplesAvailable = rbPtr->available(); - int freeSpace = ioBlock.getNumSamples() - samplesWritten; - int outputChunkLength = std::min(freeSpace, samplesAvailable); + jassert(len == outBlock.getNumSamples()); + jassert(numChannels == outBlock.getNumChannels()); - // Avoid overwriting input that hasn't yet been passed to Rubber Band: - if (samplesWritten + outputChunkLength > samplesProcessed) { - outputChunkLength = samplesProcessed - samplesWritten; - } + const float **inChannels = + (const float **)alloca(numChannels * sizeof(float *)); + float **outChannels = (float **)alloca(numChannels * sizeof(float *)); for (size_t i = 0; i < numChannels; i++) { - ioChannels[i] = ioBlock.getChannelPointer(i) + samplesWritten; + inChannels[i] = inBlock.getChannelPointer(i); + outChannels[i] = outBlock.getChannelPointer(i); } - samplesWritten += rbPtr->retrieve(ioChannels, outputChunkLength); - if (samplesWritten == ioBlock.getNumSamples()) - break; - } - if (samplesWritten > 0 && samplesWritten < ioBlock.getNumSamples()) { - // Right-align the output samples in the buffer: - int offset = ioBlock.getNumSamples() - samplesWritten; - for (size_t i = 0; i < numChannels; i++) { - float *channelBufferSource = ioBlock.getChannelPointer(i); - float *channelBufferDestination = channelBufferSource + offset; - std::memmove((char *)channelBufferDestination, - (char *)channelBufferSource, - sizeof(float) * samplesWritten); - } + // Rubberband expects all channel data with one float array per channel + return processSamples(inChannels, outChannels, len, numChannels); } - - return samplesWritten; - - // Don't produce any output for this input if RubberBand isn't ready. - // We can do this here because RubberBand buffers audio internally; - // this might not be a safe technique to use with other plugins, as - // their output sample buffers may overflow if passed more than - // maximumBlockSize. - - // if (rbPtr->available() < (int) ioBlock.getNumSamples() + getLatency()) { - // printf("Need %d samples, but Rubber Band only had %d samples - // available.\n", ioBlock.getNumSamples() + getLatency(), - // rbPtr->available()); return 0; - // } else { - // // Pull the next chunk of audio data out of RubberBand: - // int returned = rbPtr->retrieve(ioChannels, ioBlock.getNumSamples()); - // printf("Sent %d samples into Rubber Band, and took %d samples back - // out.\n", ioBlock.getNumSamples(), returned); return returned; - // } - - // // ...but only actually ask Rubberband for at most the number of samples - // we - // // can handle: - // int samplesToPull = ioBlock.getNumSamples(); - // if (samplesToPull > availableSamples) - // samplesToPull = availableSamples; - - // // If we don't have enough samples to fill a full buffer, - // // right-align the samples that we do have (i..e: start with silence). - // int missingSamples = ioBlock.getNumSamples() - availableSamples; - // if (missingSamples > 0) { - // for (size_t c = 0; c < numChannels; c++) { - // // Clear the start of the buffer so that we start - // // the buffer with silence: - // std::fill_n(ioChannels[c], missingSamples, 0.0); - - // // Move the output buffer pointer forward so that - // // RubberBandStretcher::retrieve(...) places its - // // output at the end of the buffer: - // ioChannels[c] += missingSamples; - // } - // } - - // // Pull the next audio data out of Rubberband: - // int pulled = rbPtr->retrieve(ioChannels, samplesToPull); - // printf("Pulled %d samples out of Rubber Band (%d were available).\n", - // pulled, availableSamples); return pulled; + return 0; } void reset() override final { if (rbPtr) { rbPtr->reset(); } - initialSamplesRequired = 0; } +private: + int processSamples(const float *const *inBlock, float **outBlock, + size_t samples, size_t numChannels) { + // Push all of the input samples into RubberBand: + rbPtr->process(inBlock, samples, false); + + // Figure out how many samples RubberBand is ready to give to us: + int availableSamples = rbPtr->available(); + + // ...but only actually ask Rubberband for at most the number of samples we + // can handle: + int samplesToPull = samples; + if (samplesToPull > availableSamples) + samplesToPull = availableSamples; + + // If we don't have enough samples to fill a full buffer, + // right-align the samples that we do have (i..e: start with silence). + int missingSamples = samples - availableSamples; + if (missingSamples > 0) { + for (size_t c = 0; c < numChannels; c++) { + // Clear the start of the buffer so that we start + // the buffer with silence: + std::fill_n(outBlock[c], missingSamples, 0.0); + + // Move the output buffer pointer forward so that + // RubberBandStretcher::retrieve(...) places its + // output at the end of the buffer: + outBlock[c] += missingSamples; + } + } + + // Pull the next audio data out of Rubberband: + return rbPtr->retrieve(outBlock, samplesToPull); + } + +protected: virtual int getLatencyHint() override { if (!rbPtr) return 0; initialSamplesRequired = std::max(initialSamplesRequired, - (int)(rbPtr->getSamplesRequired() + rbPtr->getLatency() + lastSpec.maximumBlockSize)); + (int)(rbPtr->getSamplesRequired() + rbPtr->getLatency() + + lastSpec.maximumBlockSize)); return initialSamplesRequired; } -protected: std::unique_ptr rbPtr; int initialSamplesRequired = 0; - static constexpr int MAX_CHANNEL_COUNT = 8; }; }; // namespace Pedalboard diff --git a/pedalboard/plugins/DelayLine.h b/pedalboard/plugins/DelayLine.h index 09025dee..01741794 100644 --- a/pedalboard/plugins/DelayLine.h +++ b/pedalboard/plugins/DelayLine.h @@ -43,9 +43,7 @@ class DelayLine : public JucePlugin(const py::array_t inputArray, /* keepExistingContent= */ true, /* clearExtraSpace= */ true); } - + // Actually run the plugins over the ioBuffer, in small chunks, to minimize // memory usage: int startOfOutputInBuffer = 0; @@ -353,7 +353,7 @@ process(const py::array_t inputArray, // 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 (intendedOutputBufferSize > ioBuffer.getNumSamples()) { ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, @@ -366,8 +366,7 @@ process(const py::array_t inputArray, // Trim the output buffer down to size; this operation should be free. jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); - ioBuffer.setSize(ioBuffer.getNumChannels(), - intendedOutputBufferSize, + ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, /* keepExistingContent= */ true, /* clearExtraSpace= */ true, /* avoidReallocating= */ true); From 55b1bcbd6706791b1d004c675648d3da5f8fd576 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 21 Jan 2022 18:44:52 -0500 Subject: [PATCH 4/8] Fixed some buffer size allocation issues. --- pedalboard/process.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pedalboard/process.h b/pedalboard/process.h index 2ffdfac2..85cda0cb 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -296,11 +296,11 @@ process(const py::array_t inputArray, int pluginSamplesReceived = 0; for (unsigned int blockStart = startOfOutputInBuffer; - blockStart < (unsigned int)ioBuffer.getNumSamples(); + blockStart < (unsigned int)intendedOutputBufferSize; blockStart += bufferSize) { unsigned int blockEnd = std::min(blockStart + bufferSize, - static_cast(ioBuffer.getNumSamples())); + static_cast(intendedOutputBufferSize)); unsigned int blockSize = blockEnd - blockStart; auto ioBlock = juce::dsp::AudioBlock( @@ -354,6 +354,7 @@ process(const py::array_t inputArray, // 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, @@ -364,7 +365,7 @@ process(const py::array_t inputArray, } } - // Trim the output buffer down to size; this operation should be free. + // Trim the output buffer down to size; this operation should be allocation-free. jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, /* keepExistingContent= */ true, From f09c0141caaab9740c95ad5bd50d73493f05f716 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 21 Jan 2022 23:21:08 -0500 Subject: [PATCH 5/8] Fixed c++ formatting. --- pedalboard/process.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pedalboard/process.h b/pedalboard/process.h index 85cda0cb..8692820f 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -365,7 +365,8 @@ process(const py::array_t inputArray, } } - // Trim the output buffer down to size; this operation should be allocation-free. + // Trim the output buffer down to size; this operation should be + // allocation-free. jassert(intendedOutputBufferSize <= ioBuffer.getNumSamples()); ioBuffer.setSize(ioBuffer.getNumChannels(), intendedOutputBufferSize, /* keepExistingContent= */ true, From cab34498acc7c7729bab44aea40271d940725059 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 18:25:39 -0500 Subject: [PATCH 6/8] Add exceptions for default cases in switches. --- pedalboard/process.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pedalboard/process.h b/pedalboard/process.h index 8692820f..45dfd635 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -134,6 +134,9 @@ copyPyArrayIntoJuceBuffer(const py::array_t inputArray) { ioBuffer.copyFrom( i, 0, static_cast(inputInfo.ptr) + (numSamples * i), numSamples); } + break; + default: + throw std::runtime_error("Internal error: got unexpected channel layout."); } return ioBuffer; @@ -158,6 +161,8 @@ py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, case ChannelLayout::NotInterleaved: outputArray = py::array_t({numChannels, outputSampleCount}); break; + default: + throw std::runtime_error("Internal error: got unexpected channel layout."); } } else { outputArray = py::array_t(outputSampleCount); @@ -188,6 +193,8 @@ py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, &outputBasePointer[outputSampleCount * i]); } break; + default: + throw std::runtime_error("Internal error: got unexpected channel layout."); } return outputArray; From 62cf42556d7a397a2e14c91adea64644857c10e0 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 18:31:23 -0500 Subject: [PATCH 7/8] static_cast --- pedalboard/ExternalPlugin.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pedalboard/ExternalPlugin.h b/pedalboard/ExternalPlugin.h index 76908577..6394deea 100644 --- a/pedalboard/ExternalPlugin.h +++ b/pedalboard/ExternalPlugin.h @@ -621,8 +621,8 @@ template class ExternalPlugin : public Plugin { // only tell Pedalboard to use the last _n_ samples. long usableSamplesProduced = samplesProvided - pluginInstance->getLatencySamples(); - return (int)std::min(usableSamplesProduced, - (long)outputBlock.getNumSamples()); + return static_cast( + std::min(usableSamplesProduced, (long)outputBlock.getNumSamples())); } return 0; From 8aa6b9aa0667400133a4df40eb0b5ed42109004c Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 18:34:31 -0500 Subject: [PATCH 8/8] clang-format --- pedalboard/process.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pedalboard/process.h b/pedalboard/process.h index 45dfd635..c34e9aa2 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -162,7 +162,8 @@ py::array_t copyJuceBufferIntoPyArray(const juce::AudioBuffer juceBuffer, outputArray = py::array_t({numChannels, outputSampleCount}); break; default: - throw std::runtime_error("Internal error: got unexpected channel layout."); + throw std::runtime_error( + "Internal error: got unexpected channel layout."); } } else { outputArray = py::array_t(outputSampleCount);