Skip to content

Commit

Permalink
Merge pull request #68 from spotify/psobot/nested-plugin-chains
Browse files Browse the repository at this point in the history
Allow Pedalboard instances to be used as plugins.
  • Loading branch information
psobot authored Feb 9, 2022
2 parents 03eba78 + a8ad1f3 commit 57c8351
Show file tree
Hide file tree
Showing 37 changed files with 1,064 additions and 373 deletions.
111 changes: 76 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,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),
Expand All @@ -112,59 +122,90 @@ 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
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.5, 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:
Expand Down
6 changes: 4 additions & 2 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_<ExternalPlugin<juce::VST3PluginFormat>, Plugin>(
py::class_<ExternalPlugin<juce::VST3PluginFormat>, Plugin,
std::shared_ptr<ExternalPlugin<juce::VST3PluginFormat>>>(
m, "_VST3Plugin",
"A wrapper around any Steinberg® VST3 audio effect plugin. Note that "
"plugins must already support the operating system currently in use "
Expand Down Expand Up @@ -842,7 +843,8 @@ inline void init_external_plugins(py::module &m) {
#endif

#if JUCE_PLUGINHOST_AU && JUCE_MAC
py::class_<ExternalPlugin<juce::AudioUnitPluginFormat>, Plugin>(
py::class_<ExternalPlugin<juce::AudioUnitPluginFormat>, Plugin,
std::shared_ptr<ExternalPlugin<juce::AudioUnitPluginFormat>>>(
m, "_AudioUnitPlugin",
"A wrapper around any Apple Audio Unit audio effect plugin. Only "
"available on macOS.",
Expand Down
2 changes: 2 additions & 0 deletions pedalboard/Plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "JuceHeader.h"
#include <mutex>

static constexpr int DEFAULT_BUFFER_SIZE = 8192;

namespace Pedalboard {
/**
* A base class for all Pedalboard plugins, JUCE-derived or external.
Expand Down
128 changes: 128 additions & 0 deletions pedalboard/PluginContainer.h
Original file line number Diff line number Diff line change
@@ -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 <mutex>

#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<std::shared_ptr<Plugin>> plugins)
: plugins(plugins) {}
virtual ~PluginContainer(){};

std::vector<std::shared_ptr<Plugin>> &getPlugins() { return plugins; }

/*
* Get a flat list of all of the plugins contained
* by this plugin, not including itself.
*/
std::vector<std::shared_ptr<Plugin>> getAllPlugins() {
std::vector<std::shared_ptr<Plugin>> flatList;
for (auto plugin : plugins) {
flatList.push_back(plugin);
if (auto *pluginContainer =
dynamic_cast<PluginContainer *>(plugin.get())) {
auto children = pluginContainer->getAllPlugins();
flatList.insert(flatList.end(), children.begin(), children.end());
}
}
return flatList;
}

protected:
std::vector<std::shared_ptr<Plugin>> plugins;
};

inline void init_plugin_container(py::module &m) {
py::class_<PluginContainer, Plugin, std::shared_ptr<PluginContainer>>(
m, "PluginContainer",
"A generic audio processing plugin that contains zero or more other "
"plugins. Not intended for direct use.")
.def(py::init([](std::vector<std::shared_ptr<Plugin>> 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> 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> 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> plugin) {
s.getPlugins().push_back(plugin);
})
.def("remove",
[](PluginContainer &s, std::shared_ptr<Plugin> 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> plugin) {
auto &plugins = s.getPlugins();
return std::find(plugins.begin(), plugins.end(), plugin) !=
plugins.end();
});
}

} // namespace Pedalboard
1 change: 1 addition & 0 deletions pedalboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@


from pedalboard_native import * # noqa: F403, 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

Expand Down
Loading

0 comments on commit 57c8351

Please sign in to comment.