Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Allow processing square buffers." #361

Merged
merged 1 commit into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
627 changes: 241 additions & 386 deletions docs/reference/pedalboard.html

Large diffs are not rendered by default.

134 changes: 67 additions & 67 deletions docs/reference/pedalboard.io.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/searchindex.js

Large diffs are not rendered by default.

24 changes: 17 additions & 7 deletions pedalboard/BufferUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,15 @@ detectChannelLayout(const py::array_t<T, py::array::c_style> inputArray,
} else if (inputInfo.shape[1] == *channelCountHint) {
return ChannelLayout::Interleaved;
} else {
// The hint was not used; fall through to the next case.
throw std::runtime_error(
"Unable to determine channel layout from shape: (" +
std::to_string(inputInfo.shape[0]) + ", " +
std::to_string(inputInfo.shape[1]) + ").");
}
}

// Try to auto-detect the channel layout from the shape
if (inputInfo.shape[0] == 0 && inputInfo.shape[1] > 0) {
// Zero channels doesn't make sense; but zero samples does.
return ChannelLayout::Interleaved;
} else if (inputInfo.shape[1] == 0 && inputInfo.shape[0] > 0) {
return ChannelLayout::NotInterleaved;
} else if (inputInfo.shape[1] < inputInfo.shape[0]) {
if (inputInfo.shape[1] < inputInfo.shape[0]) {
return ChannelLayout::Interleaved;
} else if (inputInfo.shape[0] < inputInfo.shape[1]) {
return ChannelLayout::NotInterleaved;
Expand Down Expand Up @@ -115,6 +113,12 @@ juce::AudioBuffer<T> copyPyArrayIntoJuceBuffer(
std::to_string(inputInfo.ndim) + ").");
}

if (numChannels == 0) {
throw std::runtime_error("No channels passed!");
} else if (numChannels > 2) {
throw std::runtime_error("More than two channels received!");
}

juce::AudioBuffer<T> ioBuffer(numChannels, numSamples);

// Depending on the input channel layout, we need to copy data
Expand Down Expand Up @@ -191,6 +195,12 @@ const juce::AudioBuffer<T> convertPyArrayIntoJuceBuffer(
std::to_string(inputInfo.ndim) + ").");
}

if (numChannels == 0) {
throw std::runtime_error("No channels passed!");
} else if (numChannels > 2) {
throw std::runtime_error("More than two channels received!");
}

T **channelPointers = (T **)alloca(numChannels * sizeof(T *));
for (int c = 0; c < numChannels; c++) {
channelPointers[c] = static_cast<T *>(inputInfo.ptr) + (c * numSamples);
Expand Down
42 changes: 0 additions & 42 deletions pedalboard/Plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

#pragma once

#include "BufferUtils.h"
#include "JuceHeader.h"
#include <mutex>

Expand Down Expand Up @@ -54,15 +53,6 @@ class Plugin {
*/
virtual void reset() = 0;

/**
* Reset this plugin's memory of the last channel layout and/or last channel
* count. This should usually not be called directly.
*/
void resetLastChannelLayout() {
lastSpec = {0};
lastChannelLayout = {};
};

/**
* Get the number of samples of latency introduced by this plugin.
* This is the number of samples that must be provided to the plugin
Expand Down Expand Up @@ -92,39 +82,7 @@ class Plugin {
// plugins to avoid deadlocking.
std::mutex mutex;

template <typename T>
ChannelLayout parseAndCacheChannelLayout(
const py::array_t<T, py::array::c_style> inputArray,
std::optional<int> channelCountHint = {}) {

if (!channelCountHint && lastSpec.numChannels != 0) {
channelCountHint = {lastSpec.numChannels};
}

if (lastChannelLayout) {
try {
lastChannelLayout = detectChannelLayout(inputArray, channelCountHint);
} catch (...) {
// Use the last cached layout.
}
} else {
// We have no cached layout; detect it now and raise if necessary:
try {
lastChannelLayout = detectChannelLayout(inputArray, channelCountHint);
} catch (const std::exception &e) {
throw std::runtime_error(
std::string(e.what()) +
" Provide a non-square array first to allow Pedalboard to "
"determine which dimension corresponds with the number of channels "
"and which dimension corresponds with the number of samples.");
}
}

return *lastChannelLayout;
}

protected:
juce::dsp::ProcessSpec lastSpec = {0};
std::optional<ChannelLayout> lastChannelLayout = {};
};
} // namespace Pedalboard
12 changes: 4 additions & 8 deletions pedalboard/plugin_templates/PrimeWithSilence.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ class PrimeWithSilence
}

virtual void reset() override {
JucePlugin<juce::dsp::DelayLine<
SampleType, juce::dsp::DelayLineInterpolationTypes::None>>::reset();
this->getDSP().reset();
this->getDSP().setMaximumDelayInSamples(silenceLengthSamples);
this->getDSP().setDelay(silenceLengthSamples);
Expand Down Expand Up @@ -75,13 +73,11 @@ class PrimeWithSilence
T &getNestedPlugin() { return plugin; }

void setSilenceLengthSamples(int newSilenceLengthSamples) {
if (silenceLengthSamples != newSilenceLengthSamples) {
this->getDSP().setMaximumDelayInSamples(newSilenceLengthSamples);
this->getDSP().setDelay(newSilenceLengthSamples);
silenceLengthSamples = newSilenceLengthSamples;
this->getDSP().setMaximumDelayInSamples(newSilenceLengthSamples);
this->getDSP().setDelay(newSilenceLengthSamples);
silenceLengthSamples = newSilenceLengthSamples;

reset();
}
reset();
}

int getSilenceLengthSamples() const { return silenceLengthSamples; }
Expand Down
2 changes: 0 additions & 2 deletions pedalboard/plugins/AddLatency.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ class AddLatency : public JucePlugin<juce::dsp::DelayLine<
virtual ~AddLatency(){};

virtual void reset() override {
JucePlugin<juce::dsp::DelayLine<
float, juce::dsp::DelayLineInterpolationTypes::None>>::reset();
getDSP().reset();
samplesProvided = 0;
}
Expand Down
6 changes: 1 addition & 5 deletions pedalboard/plugins/Delay.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ class Delay : public JucePlugin<juce::dsp::DelayLine<
this->getDSP().setDelay((int)(delaySeconds * spec.sampleRate));
}

virtual void reset() override {
JucePlugin<juce::dsp::DelayLine<
SampleType, juce::dsp::DelayLineInterpolationTypes::None>>::reset();
this->getDSP().reset();
}
virtual void reset() override { this->getDSP().reset(); }

virtual int process(
const juce::dsp::ProcessContextReplacing<SampleType> &context) override {
Expand Down
38 changes: 2 additions & 36 deletions pedalboard/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,42 +163,8 @@ py::array_t<float>
processFloat32(const py::array_t<float, py::array::c_style> inputArray,
double sampleRate, std::vector<std::shared_ptr<Plugin>> plugins,
unsigned int bufferSize, bool reset) {

ChannelLayout inputChannelLayout;
if (!plugins.empty()) {
inputChannelLayout = plugins[0]->parseAndCacheChannelLayout(inputArray);
} else {
inputChannelLayout = detectChannelLayout(inputArray);
}

juce::AudioBuffer<float> ioBuffer =
copyPyArrayIntoJuceBuffer(inputArray, {inputChannelLayout});

if (ioBuffer.getNumChannels() == 0) {
unsigned int numChannels = 0;
unsigned int numSamples = ioBuffer.getNumSamples();
// We have no channels to process; just return an empty output array with
// the same shape. Passing zero channels into JUCE breaks some assumptions
// all over the place.
py::array_t<float> outputArray;
if (inputArray.request().ndim == 2) {
switch (inputChannelLayout) {
case ChannelLayout::Interleaved:
outputArray = py::array_t<float>({numSamples, numChannels});
break;
case ChannelLayout::NotInterleaved:
outputArray = py::array_t<float>({numChannels, numSamples});
break;
default:
throw std::runtime_error(
"Internal error: got unexpected channel layout.");
}
} else {
outputArray = py::array_t<float>(0);
}
return outputArray;
}

const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray);
juce::AudioBuffer<float> ioBuffer = copyPyArrayIntoJuceBuffer(inputArray);
int totalOutputLatencySamples;

{
Expand Down
24 changes: 6 additions & 18 deletions pedalboard/python_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,10 @@ or buffer, set ``reset`` to ``False``.
// type inference.
return nullptr;
}))
.def(
"reset",
[](std::shared_ptr<Plugin> self) {
self->reset();
// Only reset the last channel layout if the user explicitly calls
// reset from the Python side:
self->resetLastChannelLayout();
},
"Clear any internal state stored by this plugin (e.g.: reverb "
"tails, delay lines, LFO state, etc). The values of plugin "
"parameters will remain unchanged. ")
.def("reset", &Plugin::reset,
"Clear any internal state stored by this plugin (e.g.: reverb "
"tails, delay lines, LFO state, etc). The values of plugin "
"parameters will remain unchanged. ")
.def(
"process",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
Expand Down Expand Up @@ -161,17 +154,12 @@ processing begins, clearing any state from previous calls to ``process``.
If calling ``process`` multiple times while processing the same audio file
or buffer, set ``reset`` to ``False``.

The layout of the provided ``input_array`` will be automatically detected,
assuming that the smaller dimension corresponds with the number of channels.
If the number of samples and the number of channels are the same, each
:py:class:`Plugin` object will use the last-detected channel layout until
:py:meth:`reset` is explicitly called (as of v0.9.9).

.. note::
The :py:meth:`process` method can also be used via :py:meth:`__call__`;
i.e.: just calling this object like a function (``my_plugin(...)``) will
automatically invoke :py:meth:`process` with the same arguments.
)",

)",
py::arg("input_array"), py::arg("sample_rate"),
py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true)
.def(
Expand Down
8 changes: 2 additions & 6 deletions pedalboard_native/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,12 @@ class Plugin:
If calling ``process`` multiple times while processing the same audio file
or buffer, set ``reset`` to ``False``.

The layout of the provided ``input_array`` will be automatically detected,
assuming that the smaller dimension corresponds with the number of channels.
If the number of samples and the number of channels are the same, each
:py:class:`Plugin` object will use the last-detected channel layout until
:py:meth:`reset` is explicitly called (as of v0.9.9).

.. note::
The :py:meth:`process` method can also be used via :py:meth:`__call__`;
i.e.: just calling this object like a function (``my_plugin(...)``) will
automatically invoke :py:meth:`process` with the same arguments.


"""

def reset(self) -> None:
Expand Down
103 changes: 0 additions & 103 deletions tests/test_native_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,12 @@
import pytest

from pedalboard import (
Bitcrush,
Chorus,
Clipping,
Compressor,
Convolution,
Delay,
Distortion,
Gain,
GSMFullRateCompressor,
HighpassFilter,
HighShelfFilter,
Invert,
LowpassFilter,
LowShelfFilter,
MP3Compressor,
NoiseGate,
PeakFilter,
Phaser,
PitchShift,
Resample,
Reverb,
process,
)
Expand All @@ -49,31 +35,6 @@
IMPULSE_RESPONSE_PATH = os.path.join(os.path.dirname(__file__), "impulse_response.wav")


ALL_BUILTIN_PLUGINS = [
Compressor,
Delay,
Distortion,
Gain,
Invert,
Reverb,
Bitcrush,
Chorus,
Clipping,
# Convolution, # Not instantiable with zero arguments
HighpassFilter,
LowpassFilter,
HighShelfFilter,
LowShelfFilter,
PeakFilter,
NoiseGate,
Phaser,
PitchShift,
MP3Compressor,
GSMFullRateCompressor,
Resample,
]


@pytest.mark.parametrize("shape", [(44100,), (44100, 1), (44100, 2), (1, 44100), (2, 44100)])
def test_no_transforms(shape, sr=44100):
_input = np.random.rand(*shape).astype(np.float32)
Expand Down Expand Up @@ -247,67 +208,3 @@ def test_plugin_state_not_cleared_if_passed_smaller_buffer():
effected_silence_noise_floor = np.amax(np.abs(effected_silence))

assert effected_silence_noise_floor > 0.25


@pytest.mark.parametrize("plugin_class", ALL_BUILTIN_PLUGINS)
def test_process_differently_shaped_empty_buffers(plugin_class):
plugin = plugin_class()
sr = 44100
assert plugin(np.zeros((0, 1), dtype=np.float32), sr).shape == (0, 1)
assert plugin(np.zeros((1, 0), dtype=np.float32), sr).shape == (1, 0)
assert plugin(np.zeros((0,), dtype=np.float32), sr).shape == (0,)


@pytest.mark.parametrize("plugin_class", ALL_BUILTIN_PLUGINS)
def test_process_one_by_one_buffer(plugin_class):
plugin = plugin_class()
sr = 44100
# Processing a single sample at a time should work:
assert plugin(np.zeros((1, 1), dtype=np.float32), sr).shape == (1, 1)
# Processing that same sample as a flat 1D array should work too:
assert plugin(np.zeros((1,), dtype=np.float32), sr).shape == (1,)


@pytest.mark.parametrize("plugin_class", ALL_BUILTIN_PLUGINS)
def test_process_two_by_two_buffer(plugin_class):
plugin = plugin_class()
sr = 44100

# Writing a 2x2 buffer should not work right off the bat, as we
# can't tell which dimension is channels and which dimension is
# samples:
with pytest.raises(RuntimeError) as e:
plugin(np.zeros((2, 2), dtype=np.float32), sr).shape
assert "Provide a non-square array first" in str(e)

# ...but if we write a non-square buffer, it should work:
output = plugin(np.zeros((2, 1024), dtype=np.float32), sr)
assert output.shape[0] == 2
# ...and now square buffers are interpreted as having the same channel layout:
assert plugin(np.zeros((2, 2), dtype=np.float32), sr).shape[0] == 2


@pytest.mark.parametrize("channel_dimension", [0, 1])
@pytest.mark.parametrize("plugin_class", ALL_BUILTIN_PLUGINS)
def test_process_two_by_two_buffer_with_hint(plugin_class, channel_dimension: int):
plugin = plugin_class()
sr = 44100

empty_shape = (2, 0) if channel_dimension == 0 else (0, 2)

# ...if we pass an empty array of the right shape, that shape hint should be saved:
assert plugin(np.zeros(empty_shape, dtype=np.float32), sr).shape == empty_shape
# ...and now square buffers are interpreted as having the same channel layout:
output = plugin(np.zeros((2, 2), dtype=np.float32), sr)
assert output.shape[channel_dimension] == 2

# Some plugins buffer their output, so make sure we eventually do get something out in the right shape:
while output.shape[1 if channel_dimension == 0 else 0] == 0:
output = plugin(np.zeros((2, 2), dtype=np.float32), sr, reset=False)
assert output.shape[channel_dimension] == 2

# ...but not if we call reset():
plugin.reset()
with pytest.raises(RuntimeError) as e:
plugin(np.zeros((2, 2), dtype=np.float32), sr).shape
assert "Provide a non-square array first" in str(e)
Loading
Loading