Skip to content

Commit

Permalink
Merge pull request #66 from spotify/psobot/delay-plugin
Browse files Browse the repository at this point in the history
Add Delay plugin.
  • Loading branch information
psobot authored Jan 28, 2022
2 parents c1a2ae9 + 762d6b5 commit 1f04c41
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 16 deletions.
22 changes: 11 additions & 11 deletions pedalboard/plugins/DelayLine.h → pedalboard/plugins/AddLatency.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<juce::dsp::DelayLine<
float, juce::dsp::DelayLineInterpolationTypes::None>> {
class AddLatency : public JucePlugin<juce::dsp::DelayLine<
float, juce::dsp::DelayLineInterpolationTypes::None>> {
public:
virtual ~DelayLine(){};
virtual ~AddLatency(){};

virtual void reset() override {
getDSP().reset();
Expand All @@ -49,18 +49,18 @@ class DelayLine : public JucePlugin<juce::dsp::DelayLine<
int samplesProvided = 0;
};

inline void init_delay_line(py::module &m) {
py::class_<DelayLine, Plugin>(
m, "DelayLine",
inline void init_add_latency(py::module &m) {
py::class_<AddLatency, Plugin>(
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);
}
Expand Down
133 changes: 133 additions & 0 deletions pedalboard/plugins/Delay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 <typename SampleType>
class Delay : public JucePlugin<juce::dsp::DelayLine<
SampleType, juce::dsp::DelayLineInterpolationTypes::None>> {
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)(delaySeconds * spec.sampleRate));
}

virtual void reset() override { this->getDSP().reset(); }

virtual int process(
const juce::dsp::ProcessContextReplacing<SampleType> &context) override {
// TODO: More advanced mixing rules than "linear?"
SampleType dryVolume = 1.0f - getMix();
SampleType wetVolume = getMix();

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++) {
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_<Delay<float>, 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<float>();
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<float> &plugin) {
std::ostringstream ss;
ss << "<pedalboard.Delay";
ss << " delay_seconds=" << plugin.getDelaySeconds();
ss << " feedback=" << plugin.getFeedback();
ss << " mix=" << plugin.getMix();
ss << " at " << &plugin;
ss << ">";
return ss.str();
})
.def_property("delay_seconds", &Delay<float>::getDelaySeconds,
&Delay<float>::setDelaySeconds)
.def_property("feedback", &Delay<float>::getFeedback,
&Delay<float>::setFeedback)
.def_property("mix", &Delay<float>::getMix, &Delay<float>::setMix);
}
}; // namespace Pedalboard
6 changes: 4 additions & 2 deletions pedalboard/python_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ 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/Delay.h"
#include "plugins/Distortion.h"
#include "plugins/Gain.h"
#include "plugins/HighpassFilter.h"
Expand Down Expand Up @@ -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);
Expand All @@ -151,5 +153,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);
};
4 changes: 2 additions & 2 deletions tests/test_latency_compensation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)
25 changes: 24 additions & 1 deletion tests/test_native_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -91,6 +91,29 @@ 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):
"""
Expand Down

0 comments on commit 1f04c41

Please sign in to comment.