Skip to content
Draft
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
121 changes: 121 additions & 0 deletions pedalboard/ArrayUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* pedalboard
* Copyright 2025 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 <pybind11/numpy.h>
#include <pybind11/pybind11.h>

namespace py = pybind11;

namespace Pedalboard {

/**
* Utility function to convert various array-like objects to py::array.
* Supports:
* - NumPy arrays (pass-through)
* - PyTorch tensors (via .numpy() method)
* - JAX arrays (via .__array__() method)
* - TensorFlow tensors (via .numpy() method)
* - CuPy arrays (via .get() method for CPU copy)
* - Any object with __array__ interface
*/
inline py::array ensureArrayLike(py::object input) {
// If we were already given a numpy array, just return it
if (py::isinstance<py::array>(input)) {
return py::reinterpret_borrow<py::array>(input);
}

// Check if it's a PyTorch tensor (has a .numpy() method)
if (py::hasattr(input, "numpy") && py::hasattr(input, "dtype") &&
py::hasattr(input, "device")) {
// Check if tensor is on CPU
py::object device = input.attr("device");
std::string device_type = py::str(device.attr("type")).cast<std::string>();

if (device_type != "cpu") {
// Move tensor to CPU first
input = input.attr("cpu")();
}

// Call .numpy() to get the numpy array
// This shares memory with the tensor when possible
return input.attr("numpy")().cast<py::array>();
}

// Check if it's a TensorFlow tensor (has .numpy() method but no .device)
if (py::hasattr(input, "numpy") && !py::hasattr(input, "device")) {
try {
return input.attr("numpy")().cast<py::array>();
} catch (...) {
// Fall through to next option
}
}

// Check if it's a CuPy array (has .get() method)
if (py::hasattr(input, "get") && py::hasattr(input, "dtype") &&
py::hasattr(input, "ndim")) {
try {
// .get() copies from GPU to CPU and returns numpy array
return input.attr("get")().cast<py::array>();
} catch (...) {
// Fall through to next option
}
}

// Check if it implements the array protocol (__array__)
if (py::hasattr(input, "__array__")) {
try {
return input.attr("__array__")().cast<py::array>();
} catch (...) {
// Fall through to error
}
}

// Try to convert directly to array as a last resort
// py::array::ensure() will attempt to convert the object to an array
// or return an invalid array handle if conversion fails
py::array result = py::array::ensure(input);

if (!result) {
throw py::type_error(
"Expected an array-like object (numpy array, torch tensor, etc.), "
"but received: " +
py::repr(input).cast<std::string>());
}

return result;
}

/**
* Template version that ensures the array has a specific dtype
*/
template <typename T>
inline py::array_t<T> ensureArrayLikeWithType(py::object input) {
py::array arr = ensureArrayLike(input);

// If the array already has the correct type, return it
if (py::isinstance<py::array_t<T>>(arr)) {
return py::reinterpret_borrow<py::array_t<T>>(arr);
}

// Otherwise, cast to the desired type
// Note: this may create a copy
return arr.cast<py::array_t<T>>();
}

} // namespace Pedalboard
34 changes: 16 additions & 18 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -1578,21 +1578,19 @@ inline void init_external_plugins(py::module &m) {
py::arg("reset") = true)
.def(
"process",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
[](std::shared_ptr<Plugin> self, py::object input,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize,
reset);
return process(input, sampleRate, {self}, bufferSize, reset);
},
EXTERNAL_PLUGIN_PROCESS_DOCSTRING, py::arg("input_array"),
py::arg("sample_rate"),
py::arg("buffer_size") = DEFAULT_BUFFER_SIZE,
py::arg("reset") = true)
.def(
"__call__",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
[](std::shared_ptr<Plugin> self, py::object input,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize,
reset);
return process(input, sampleRate, {self}, bufferSize, reset);
},
"Run an audio or MIDI buffer through this plugin, returning "
"audio. Alias for :py:meth:`process`.",
Expand Down Expand Up @@ -1809,18 +1807,18 @@ example: a Windows VST3 plugin bundle will not load on Linux or macOS.)
SHOW_EDITOR_DOCSTRING, py::arg("close_event") = py::none())
.def(
"process",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
EXTERNAL_PLUGIN_PROCESS_DOCSTRING, py::arg("input_array"),
py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE,
py::arg("reset") = true)
.def(
"__call__",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
"Run an audio or MIDI buffer through this plugin, returning "
"audio. Alias for :py:meth:`process`.",
Expand Down Expand Up @@ -2035,18 +2033,18 @@ see :class:`pedalboard.VST3Plugin`.)
SHOW_EDITOR_DOCSTRING, py::arg("close_event") = py::none())
.def(
"process",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
EXTERNAL_PLUGIN_PROCESS_DOCSTRING, py::arg("input_array"),
py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE,
py::arg("reset") = true)
.def(
"__call__",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
"Run an audio or MIDI buffer through this plugin, returning "
"audio. Alias for :py:meth:`process`.",
Expand Down
12 changes: 10 additions & 2 deletions pedalboard/io/AudioFileInit.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

#include "../ArrayUtils.h"
#include "../JuceHeader.h"
#include "AudioFile.h"

Expand Down Expand Up @@ -278,16 +279,19 @@ inline void init_audio_file(
py::arg("format") = py::none())
.def_static(
"encode",
[](const py::array samples, double sampleRate, std::string format,
[](py::object samples, double sampleRate, std::string format,
int numChannels, int bitDepth,
std::optional<std::variant<std::string, float>> quality) {
// Convert the input to a numpy array (supports torch tensors, etc.)
py::array samplesArray = ensureArrayLike(samples);

juce::MemoryBlock outputBlock;
auto audioFile = std::make_unique<WriteableAudioFile>(
format,
std::make_unique<juce::MemoryOutputStream>(outputBlock, false),
sampleRate, numChannels, bitDepth, quality);

audioFile->write(samples);
audioFile->write(samplesArray);
audioFile->close();

return py::bytes((const char *)outputBlock.getData(),
Expand All @@ -299,6 +303,10 @@ inline void init_audio_file(
R"(
Encode an audio buffer to a Python :class:`bytes` object.

The input audio buffer can be any array-like object, including NumPy arrays,
PyTorch tensors, TensorFlow tensors, JAX arrays, or any other object that
supports the buffer protocol or has a __array__ method.

This function will encode an entire audio buffer at once and return a :class:`bytes`
object representing the bytes of the resulting audio file.

Expand Down
15 changes: 11 additions & 4 deletions pedalboard/io/WriteableAudioFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

#include "../ArrayUtils.h"
#include "../BufferUtils.h"
#include "../JuceHeader.h"
#include "AudioFile.h"
Expand Down Expand Up @@ -445,11 +446,15 @@ class WriteableAudioFile

/**
* A generic type-dispatcher for all writes.
* Accepts various array-like objects including torch tensors.
* pybind11 supports dispatch here, but both pybind11-stubgen
* and Sphinx currently (2022-07-16) struggle with how to render
* docstrings of overloaded functions, so we don't overload.
*/
void write(py::array inputArray) {
void write(py::object input) {
// Convert the input to a numpy array (supports torch tensors, etc.)
py::array inputArray = ensureArrayLike(input);

switch (inputArray.dtype().char_()) {
case 'f':
return write<float>(py::array_t<float>(inputArray.release(), false));
Expand Down Expand Up @@ -1017,13 +1022,15 @@ inline void init_writeable_audio_file(
py::arg("format") = py::none())
.def(
"write",
[](WriteableAudioFile &file, py::array samples) {
[](WriteableAudioFile &file, py::object samples) {
file.write(samples);
},
py::arg("samples").noconvert(),
py::arg("samples"),
"Encode an array of audio data and write "
"it to this file. The number of channels in the array must match the "
"number of channels used to open the file. The array may contain "
"number of channels used to open the file. The audio data may be "
"provided as a NumPy array, PyTorch tensor, TensorFlow tensor, "
"JAX array, or any other array-like object. The array may contain "
"audio in any shape. If the file's bit depth or format does not "
"match the provided data type, the audio will be automatically "
"converted.\n\n"
Expand Down
6 changes: 5 additions & 1 deletion pedalboard/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

#include "ArrayUtils.h"
#include "BufferUtils.h"
#include "Plugin.h"
#include "PluginContainer.h"
Expand Down Expand Up @@ -275,9 +276,12 @@ processFloat32(const py::array_t<float, py::array::c_style> inputArray,
inputArray.request().ndim);
}

py::array_t<float> process(py::array inputArray, double sampleRate,
py::array_t<float> process(py::object input, double sampleRate,
const std::vector<std::shared_ptr<Plugin>> plugins,
unsigned int bufferSize, bool reset) {
// Convert the input to a numpy array (supports torch tensors, etc.)
py::array inputArray = ensureArrayLike(input);

py::array_t<float, py::array::c_style> float32InputArray;
switch (inputArray.dtype().char_()) {
case 'f':
Expand Down
24 changes: 16 additions & 8 deletions pedalboard/python_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,20 @@ PYBIND11_MODULE(pedalboard_native, m, py::mod_gil_not_used()) {

m.def(
"process",
[](const py::array inputArray, double sampleRate,
[](py::object input, double sampleRate,
const std::vector<std::shared_ptr<Plugin>> plugins,
unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, plugins, bufferSize, reset);
return process(input, sampleRate, plugins, bufferSize, reset);
},
R"(
Run a 32-bit or 64-bit floating point audio buffer through a
list of Pedalboard plugins. If the provided buffer uses a 64-bit datatype,
it will be converted to 32-bit for processing.

The input audio buffer can be any array-like object, including NumPy arrays,
PyTorch tensors, TensorFlow tensors, JAX arrays, or any other object that
supports the buffer protocol or has a __array__ method.

The provided ``buffer_size`` argument will be used to control the size of
each chunk of audio provided into the plugins. Higher buffer sizes may speed up
processing at the expense of memory usage.
Expand Down Expand Up @@ -134,15 +138,19 @@ or buffer, set ``reset`` to ``False``.
"parameters will remain unchanged. ")
.def(
"process",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
R"(
Run a 32-bit or 64-bit floating point audio buffer through this plugin.
(If calling this multiple times with multiple plugins, consider creating a
:class:`pedalboard.Pedalboard` object instead.)

The input audio buffer can be any array-like object, including NumPy arrays,
PyTorch tensors, TensorFlow tensors, JAX arrays, or any other object that
supports the buffer protocol or has a __array__ method.

The returned array may contain up to (but not more than) the same number of
samples as were provided. If fewer samples were returned than expected, the
plugin has likely buffered audio inside itself. To receive the remaining
Expand Down Expand Up @@ -176,9 +184,9 @@ If the number of samples and the number of channels are the same, each
py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true)
.def(
"__call__",
[](std::shared_ptr<Plugin> self, const py::array inputArray,
double sampleRate, unsigned int bufferSize, bool reset) {
return process(inputArray, sampleRate, {self}, bufferSize, reset);
[](std::shared_ptr<Plugin> self, py::object input, double sampleRate,
unsigned int bufferSize, bool reset) {
return process(input, sampleRate, {self}, bufferSize, reset);
},
"Run an audio buffer through this plugin. Alias for "
":py:meth:`process`.",
Expand Down
Loading
Loading