diff --git a/README.md b/README.md index 63f8dbe5..fdd7fc12 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,15 @@ - `Convolution` - `Compressor` - `Chorus` + - `Distortion` - `Gain` - `HighpassFilter` - - `Reverb` - `LadderFilter` - `Limiter` - `LowpassFilter` - `Phaser` - - Supports VST3 plugins on macOS, Windows, and Linux + - `Reverb` + - Supports VST3® plugins on macOS, Windows, and Linux - Supports Audio Units on macOS - Strong thread-safety, memory usage, and speed guarantees - Releases Python's Global Interpreter Lock (GIL) to allow use of multiple CPU cores @@ -158,3 +159,5 @@ Contributions to `pedalboard` are welcomed! See [CONTRIBUTING.md](https://github `pedalboard`'s logo contains artwork called ["guitar pedals" by Jino from the Noun Project](https://thenounproject.com/term/guitar-pedals/3605562), and the wordmark uses modified glyphs from [Victor Mono](https://github.com/rubjo/victor-mono). + +_VST is a registered trademark of Steinberg Media Technologies GmbH._ \ No newline at end of file diff --git a/pedalboard/plugins/Distortion.h b/pedalboard/plugins/Distortion.h new file mode 100644 index 00000000..2baa0c44 --- /dev/null +++ b/pedalboard/plugins/Distortion.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#include +#include + +namespace py = pybind11; + +#include "../JucePlugin.h" + +namespace Pedalboard { +template +class Distortion + : public JucePlugin, juce::dsp::WaveShaper>> { +public: + void setDriveDecibels(const float f) noexcept { driveDecibels = f; } + float getDriveDecibels() const noexcept { return driveDecibels; } + + virtual void prepare(const juce::dsp::ProcessSpec &spec) override { + JucePlugin, + juce::dsp::WaveShaper>>:: + prepare(spec); + this->getDSP().template get().setGainDecibels( + getDriveDecibels()); + this->getDSP().template get().functionToUse = + [](SampleType x) { return std::tanh(x); }; + } + +private: + SampleType driveDecibels; + + enum { gainIndex, waveshaperIndex }; +}; + +inline void init_distortion(py::module &m) { + py::class_, Plugin>( + m, "Distortion", "Apply soft distortion with a tanh waveshaper.") + .def(py::init([](float drive_db) { + auto plugin = new Distortion(); + plugin->setDriveDecibels(drive_db); + return plugin; + }), + py::arg("drive_db") = 25) + .def("__repr__", + [](const Distortion &plugin) { + std::ostringstream ss; + ss << ""; + return ss.str(); + }) + .def_property("drive_db", &Distortion::getDriveDecibels, + &Distortion::setDriveDecibels); +} +}; // namespace Pedalboard diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index bf4aefb9..3e87d106 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/Distortion.h" #include "plugins/Gain.h" #include "plugins/HighpassFilter.h" #include "plugins/LadderFilter.h" @@ -125,6 +126,7 @@ PYBIND11_MODULE(pedalboard_native, m) { init_compressor(m); init_convolution(m); + init_distortion(m); init_gain(m); init_highpass(m); init_ladderfilter(m); diff --git a/setup.py b/setup.py index 943037eb..de966110 100644 --- a/setup.py +++ b/setup.py @@ -200,7 +200,7 @@ setup( name='pedalboard', - version='0.3.3', + version='0.3.4', author='Peter Sobot', author_email='psobot@spotify.com', description='A Python library for adding effects to audio.', diff --git a/tests/test_locking.py b/tests/test_locking.py index c0947b96..03c7be80 100644 --- a/tests/test_locking.py +++ b/tests/test_locking.py @@ -24,7 +24,7 @@ import pedalboard -@pytest.mark.parametrize("num_concurrent_chains", [10, 24, 50]) +@pytest.mark.parametrize("num_concurrent_chains", [2, 10, 20]) def test_multiple_threads_using_same_plugin_instances(num_concurrent_chains: int): """ Instantiate a large number of stateful plugins, then run audio through them diff --git a/tests/test_native_module.py b/tests/test_native_module.py index 4ecd232b..2abea26a 100644 --- a/tests/test_native_module.py +++ b/tests/test_native_module.py @@ -18,8 +18,8 @@ import os import pytest import numpy as np -from pedalboard import process, Gain, Compressor, Convolution - +from pedalboard import process, Distortion, Gain, Compressor, Convolution +from librosa import db_to_amplitude IMPULSE_RESPONSE_PATH = os.path.join(os.path.dirname(__file__), "impulse_response.wav") @@ -77,3 +77,16 @@ def test_throw_on_inaccessible_convolution_file(): # Should fail: with pytest.raises(RuntimeError): Convolution("missing_impulse_response.wav") + + +@pytest.mark.parametrize("gain_db", [-12, -6, 0, 1.1, 6, 12, 24, 48, 96]) +@pytest.mark.parametrize("shape", [(44100,), (44100, 1), (44100, 2), (1, 4), (2, 4)]) +def test_distortion(gain_db, shape, sr=44100): + full_scale_noise = np.random.rand(*shape).astype(np.float32) + + # Use the Distortion transform with ±0dB, which should change nothing: + result = process(full_scale_noise, sr, [Distortion(gain_db)]) + + np.testing.assert_equal(result.shape, full_scale_noise.shape) + gain_scale = db_to_amplitude(gain_db) + np.testing.assert_allclose(np.tanh(full_scale_noise * gain_scale), result, rtol=4e-7, atol=2e-7)