From 305c7753fdf7481d9dfb13e5a81b4f169deafaa7 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sat, 22 Jan 2022 16:28:05 -0500 Subject: [PATCH 1/5] Rename DelayLine to AddLatency. --- .../plugins/{DelayLine.h => AddLatency.h} | 22 +++++++++---------- pedalboard/python_bindings.cpp | 4 ++-- tests/test_latency_compensation.py | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) rename pedalboard/plugins/{DelayLine.h => AddLatency.h} (77%) diff --git a/pedalboard/plugins/DelayLine.h b/pedalboard/plugins/AddLatency.h similarity index 77% rename from pedalboard/plugins/DelayLine.h rename to pedalboard/plugins/AddLatency.h index 01741794..8d117655 100644 --- a/pedalboard/plugins/DelayLine.h +++ b/pedalboard/plugins/AddLatency.h @@ -21,12 +21,12 @@ namespace Pedalboard { /** * A dummy plugin that buffers audio data internally, used to test Pedalboard's - * automatic delay compensation. + * automatic latency compensation. */ -class DelayLine : public JucePlugin> { +class AddLatency : public JucePlugin> { public: - virtual ~DelayLine(){}; + virtual ~AddLatency(){}; virtual void reset() override { getDSP().reset(); @@ -49,18 +49,18 @@ class DelayLine : public JucePlugin( - m, "DelayLine", +inline void init_add_latency(py::module &m) { + 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 " "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; + auto al = new AddLatency(); + al->getDSP().setMaximumDelayInSamples(samples); + al->getDSP().setDelay(samples); + return al; }), py::arg("samples") = 44100); } diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index f9b6e41b..b267f917 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -32,10 +32,10 @@ namespace py = pybind11; #include "Plugin.h" #include "process.h" +#include "plugins/AddLatency.h" #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" @@ -151,5 +151,5 @@ PYBIND11_MODULE(pedalboard_native, m) { // Internal plugins for testing, debugging, etc: py::module internal = m.def_submodule("_internal"); - init_delay_line(internal); + init_add_latency(internal); }; diff --git a/tests/test_latency_compensation.py b/tests/test_latency_compensation.py index 1af267ac..d65d0fe3 100644 --- a/tests/test_latency_compensation.py +++ b/tests/test_latency_compensation.py @@ -17,7 +17,7 @@ import pytest import numpy as np -from pedalboard_native._internal import DelayLine +from pedalboard_native._internal import AddLatency @pytest.mark.parametrize("sample_rate", [22050, 44100, 48000]) @@ -26,6 +26,6 @@ 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)) + plugin = AddLatency(int(latency_seconds * sample_rate)) output = plugin.process(noise, sample_rate, buffer_size=buffer_size) np.testing.assert_allclose(output, noise) From f221fcf57eb1a2ac31dacc6aeea93878b10c75ac Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sat, 22 Jan 2022 17:58:51 -0500 Subject: [PATCH 2/5] Fix AddLatency rename. --- pedalboard/plugins/AddLatency.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pedalboard/plugins/AddLatency.h b/pedalboard/plugins/AddLatency.h index 8d117655..15df4fcc 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 " From 85a607f47f18656f3c650ff9cf9aa3b998ec198c Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sat, 22 Jan 2022 17:58:32 -0500 Subject: [PATCH 3/5] Add delay plugin with feedback. --- pedalboard/plugins/Delay.h | 125 +++++++++++++++++++++++++++++++++ pedalboard/python_bindings.cpp | 2 + tests/test_native_module.py | 26 ++++++- 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 pedalboard/plugins/Delay.h diff --git a/pedalboard/plugins/Delay.h b/pedalboard/plugins/Delay.h new file mode 100644 index 00000000..e965cf7d --- /dev/null +++ b/pedalboard/plugins/Delay.h @@ -0,0 +1,125 @@ +/* + * 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 { +template +class Delay : public JucePlugin> { +public: + SampleType getDelaySeconds() const { return delaySeconds; } + void setDelaySeconds(const SampleType value) { + if (value < 0.0 || value > MAXIMUM_DELAY_TIME_SECONDS) { + throw std::range_error("Delay (in seconds) must be between 0.0s and " + + std::to_string(MAXIMUM_DELAY_TIME_SECONDS) + "s."); + } + delaySeconds = value; + }; + SampleType getFeedback() const { return feedback; } + void setFeedback(const SampleType value) { + if (value < 0.0 || value > 1.0) { + throw std::range_error("Feedback must be between 0.0 and 1.0."); + } + feedback = value; + }; + SampleType getMix() const { return mix; } + void setMix(const SampleType value) { + if (value < 0.0 || value > 1.0) { + throw std::range_error("Mix must be between 0.0 and 1.0."); + } + mix = value; + }; + + virtual void prepare(const juce::dsp::ProcessSpec &spec) override { + if (this->lastSpec.sampleRate != spec.sampleRate || + this->lastSpec.maximumBlockSize < spec.maximumBlockSize || + spec.numChannels != this->lastSpec.numChannels) { + this->getDSP().setMaximumDelayInSamples( + (int)(MAXIMUM_DELAY_TIME_SECONDS * spec.sampleRate)); + this->getDSP().prepare(spec); + this->lastSpec = spec; + } + + this->getDSP().setDelay((int)(getDelaySeconds() * spec.sampleRate)); + } + + virtual void reset() override { this->getDSP().reset(); } + + virtual int process( + const juce::dsp::ProcessContextReplacing &context) override { + // TODO: More advanced mixing rules than "linear?" + SampleType dryVolume = 1.0f - getMix(); + SampleType wetVolume = getMix(); + + this->getDSP().setDelay((int)(getDelaySeconds() * this->lastSpec.sampleRate)); + + // Pass samples through the delay line with feedback: + for (size_t c = 0; c < context.getInputBlock().getNumChannels(); c++) { + jassert(context.getInputBlock().getChannelPointer(c) == + context.getOutputBlock().getChannelPointer(c)); + SampleType *channelBuffer = context.getOutputBlock().getChannelPointer(c); + + for (size_t i = 0; i < context.getInputBlock().getNumSamples(); i++) { + SampleType delayOutput = this->getDSP().popSample(c); + this->getDSP().pushSample(c, + channelBuffer[i] + (getFeedback() * delayOutput)); + channelBuffer[i] = + (channelBuffer[i] * dryVolume) + (wetVolume * delayOutput); + } + } + return context.getInputBlock().getNumSamples(); + } + +private: + SampleType delaySeconds = 1.0f; + SampleType feedback = 0.0f; + SampleType mix = 1.0f; + static constexpr int MAXIMUM_DELAY_TIME_SECONDS = 30; +}; + +inline void init_delay(py::module &m) { + py::class_, Plugin>( + m, "Delay", + "A digital delay plugin with controllable delay time, feedback " + "percentage, and dry/wet mix.") + .def(py::init([](float delaySeconds, float feedback, float mix) { + auto delay = new Delay(); + delay->setDelaySeconds(delaySeconds); + delay->setFeedback(feedback); + delay->setMix(mix); + return delay; + }), + py::arg("delay_seconds") = 0.5, py::arg("feedback") = 0.0, + py::arg("mix") = 0.5) + .def("__repr__", + [](const Delay &plugin) { + std::ostringstream ss; + ss << ""; + return ss.str(); + }) + .def_property("delay_seconds", &Delay::getDelaySeconds, + &Delay::setDelaySeconds) + .def_property("feedback", &Delay::getFeedback, + &Delay::setFeedback) + .def_property("mix", &Delay::getMix, &Delay::setMix); +} +}; // namespace Pedalboard diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index b267f917..61bf1156 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -36,6 +36,7 @@ namespace py = pybind11; #include "plugins/Chorus.h" #include "plugins/Compressor.h" #include "plugins/Convolution.h" +#include "plugins/Delay.h" #include "plugins/Distortion.h" #include "plugins/Gain.h" #include "plugins/HighpassFilter.h" @@ -136,6 +137,7 @@ PYBIND11_MODULE(pedalboard_native, m) { init_chorus(m); init_compressor(m); init_convolution(m); + init_delay(m); init_distortion(m); init_gain(m); init_highpass(m); diff --git a/tests/test_native_module.py b/tests/test_native_module.py index 4df3863b..14752127 100644 --- a/tests/test_native_module.py +++ b/tests/test_native_module.py @@ -18,7 +18,7 @@ import os import pytest import numpy as np -from pedalboard import process, Distortion, Gain, Compressor, Convolution, Reverb +from pedalboard import process, Delay, Distortion, Gain, Compressor, Convolution, Reverb IMPULSE_RESPONSE_PATH = os.path.join(os.path.dirname(__file__), "impulse_response.wav") @@ -91,6 +91,30 @@ def test_distortion(gain_db, shape, sr=44100): np.testing.assert_allclose(np.tanh(full_scale_noise * gain_scale), result, rtol=4e-7, atol=2e-7) +def test_delay(): + delay_seconds = 2.5 + feedback = 0.0 + mix = 0.5 + duration = 10.0 + sr = 44100 + + full_scale_noise = np.random.rand(int(sr * duration)).astype(np.float32) + result = Delay(delay_seconds, feedback, mix)(full_scale_noise, sr) + + # Manually do what a delay plugin would do: + dry_volume = 1.0 - mix + wet_volume = mix + + delayed_line = np.concatenate([ + np.zeros(int(delay_seconds * sr)), + full_scale_noise + ])[:len(result)] + expected = (dry_volume * full_scale_noise) + (wet_volume * delayed_line) + + np.testing.assert_equal(result.shape, expected.shape) + np.testing.assert_allclose(expected, result, rtol=4e-7, atol=2e-7) + + @pytest.mark.parametrize("reset", (True, False)) def test_plugin_state_not_cleared_between_invocations(reset: bool): """ From bc9810980d06f620617aad3d7de07b584f0df68a Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Sun, 23 Jan 2022 00:52:14 -0500 Subject: [PATCH 4/5] Fix Delay plugin when delay time is 0.0 seconds. --- pedalboard/plugins/Delay.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pedalboard/plugins/Delay.h b/pedalboard/plugins/Delay.h index e965cf7d..b04dbc38 100644 --- a/pedalboard/plugins/Delay.h +++ b/pedalboard/plugins/Delay.h @@ -54,7 +54,7 @@ class Delay : public JucePluginlastSpec = spec; } - this->getDSP().setDelay((int)(getDelaySeconds() * spec.sampleRate)); + this->getDSP().setDelay((int)(delaySeconds * spec.sampleRate)); } virtual void reset() override { this->getDSP().reset(); } @@ -65,7 +65,13 @@ class Delay : public JucePlugingetDSP().setDelay((int)(getDelaySeconds() * this->lastSpec.sampleRate)); + if (delaySeconds == 0.0f) { + // Special case where DelayLine doesn't do anything for us. + // Regardless of the mix or feedback parameters, the input will sound identical. + return context.getInputBlock().getNumSamples(); + } + + this->getDSP().setDelay((int)(delaySeconds * this->lastSpec.sampleRate)); // Pass samples through the delay line with feedback: for (size_t c = 0; c < context.getInputBlock().getNumChannels(); c++) { From 762d6b5f090cc1d1dbe96e6d54cf50a101cd29bd Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Mon, 24 Jan 2022 20:10:48 -0500 Subject: [PATCH 5/5] Formatting. --- pedalboard/plugins/Delay.h | 10 ++++++---- tests/test_native_module.py | 7 +++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pedalboard/plugins/Delay.h b/pedalboard/plugins/Delay.h index b04dbc38..4fe60a12 100644 --- a/pedalboard/plugins/Delay.h +++ b/pedalboard/plugins/Delay.h @@ -19,7 +19,8 @@ namespace Pedalboard { template -class Delay : public JucePlugin> { +class Delay : public JucePlugin> { public: SampleType getDelaySeconds() const { return delaySeconds; } void setDelaySeconds(const SampleType value) { @@ -67,7 +68,8 @@ class Delay : public JucePlugingetDSP().popSample(c); - this->getDSP().pushSample(c, - channelBuffer[i] + (getFeedback() * delayOutput)); + this->getDSP().pushSample(c, channelBuffer[i] + + (getFeedback() * delayOutput)); channelBuffer[i] = (channelBuffer[i] * dryVolume) + (wetVolume * delayOutput); } diff --git a/tests/test_native_module.py b/tests/test_native_module.py index 14752127..43be12ae 100644 --- a/tests/test_native_module.py +++ b/tests/test_native_module.py @@ -105,10 +105,9 @@ def test_delay(): dry_volume = 1.0 - mix wet_volume = mix - delayed_line = np.concatenate([ - np.zeros(int(delay_seconds * sr)), - full_scale_noise - ])[:len(result)] + delayed_line = np.concatenate([np.zeros(int(delay_seconds * sr)), full_scale_noise])[ + : len(result) + ] expected = (dry_volume * full_scale_noise) + (wet_volume * delayed_line) np.testing.assert_equal(result.shape, expected.shape)