Skip to content
Open
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
88 changes: 46 additions & 42 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "Plugin.h"
#include <pybind11/stl.h>

#include "io/PathUtils.h"
#include "juce_overrides/juce_PatchedVST3PluginFormat.h"
#include "process.h"

Expand Down Expand Up @@ -653,19 +654,21 @@ class ExternalPlugin : public AbstractExternalPlugin {
}
};

void loadPresetFile(std::string presetFilePath) {
juce::File presetFile(presetFilePath);
void loadPresetFile(py::object presetFilePath) {
std::string presetFilePathStr = pathToString(presetFilePath);
juce::File presetFile(presetFilePathStr);
juce::MemoryBlock presetData;

if (!presetFile.loadFileAsData(presetData)) {
throw std::runtime_error("Failed to read preset file: " + presetFilePath);
throw std::runtime_error("Failed to read preset file: " +
presetFilePathStr);
}

SetPresetVisitor visitor{presetData};
pluginInstance->getExtensions(visitor);
if (!visitor.didSetPreset) {
throw std::runtime_error("Plugin failed to load data from preset file: " +
presetFilePath);
presetFilePathStr);
}
}

Expand Down Expand Up @@ -1643,23 +1646,23 @@ example: a Windows VST3 plugin bundle will not load on Linux or macOS.)

*Support for running VST3® plugins on background threads introduced in v0.8.8.*
)")
.def(
py::init([](std::string &pathToPluginFile, py::object parameterValues,
std::optional<std::string> pluginName,
float initializationTimeout) {
std::shared_ptr<ExternalPlugin<juce::PatchedVST3PluginFormat>>
plugin = std::make_shared<
ExternalPlugin<juce::PatchedVST3PluginFormat>>(
pathToPluginFile, pluginName, initializationTimeout);
py::cast(plugin).attr("__set_initial_parameter_values__")(
parameterValues);
return plugin;
}),
py::arg("path_to_plugin_file"),
py::arg("parameter_values") = py::none(),
py::arg("plugin_name") = py::none(),
py::arg("initialization_timeout") =
DEFAULT_INITIALIZATION_TIMEOUT_SECONDS)
.def(py::init([](py::object pathToPluginFile, py::object parameterValues,
std::optional<std::string> pluginName,
float initializationTimeout) {
std::string pathStr = pathToString(pathToPluginFile);
std::shared_ptr<ExternalPlugin<juce::PatchedVST3PluginFormat>>
plugin = std::make_shared<
ExternalPlugin<juce::PatchedVST3PluginFormat>>(
pathStr, pluginName, initializationTimeout);
py::cast(plugin).attr("__set_initial_parameter_values__")(
parameterValues);
return plugin;
}),
py::arg("path_to_plugin_file"),
py::arg("parameter_values") = py::none(),
py::arg("plugin_name") = py::none(),
py::arg("initialization_timeout") =
DEFAULT_INITIALIZATION_TIMEOUT_SECONDS)
.def("__repr__",
[](ExternalPlugin<juce::PatchedVST3PluginFormat> &plugin) {
std::ostringstream ss;
Expand Down Expand Up @@ -1693,9 +1696,9 @@ example: a Windows VST3 plugin bundle will not load on Linux or macOS.)
"plugin to crash, taking the entire Python process down with it.")
.def_static(
"get_plugin_names_for_file",
[](std::string filename) {
[](py::object filename) {
return getPluginNamesForFile<juce::PatchedVST3PluginFormat>(
filename);
pathToString(filename));
},
"Return a list of plugin names contained within a given VST3 "
"plugin (i.e.: a \".vst3\"). If the provided file cannot be "
Expand Down Expand Up @@ -1881,23 +1884,23 @@ see :class:`pedalboard.VST3Plugin`.)

*Support for loading AUv3 plugins (* ``.appex`` *bundles) introduced in v0.9.5.*
)")
.def(
py::init([](std::string &pathToPluginFile, py::object parameterValues,
std::optional<std::string> pluginName,
float initializationTimeout) {
std::shared_ptr<ExternalPlugin<juce::AudioUnitPluginFormat>>
plugin = std::make_shared<
ExternalPlugin<juce::AudioUnitPluginFormat>>(
pathToPluginFile, pluginName, initializationTimeout);
py::cast(plugin).attr("__set_initial_parameter_values__")(
parameterValues);
return plugin;
}),
py::arg("path_to_plugin_file"),
py::arg("parameter_values") = py::none(),
py::arg("plugin_name") = py::none(),
py::arg("initialization_timeout") =
DEFAULT_INITIALIZATION_TIMEOUT_SECONDS)
.def(py::init([](py::object pathToPluginFile, py::object parameterValues,
std::optional<std::string> pluginName,
float initializationTimeout) {
std::string pathStr = pathToString(pathToPluginFile);
std::shared_ptr<ExternalPlugin<juce::AudioUnitPluginFormat>>
plugin = std::make_shared<
ExternalPlugin<juce::AudioUnitPluginFormat>>(
pathStr, pluginName, initializationTimeout);
py::cast(plugin).attr("__set_initial_parameter_values__")(
parameterValues);
return plugin;
}),
py::arg("path_to_plugin_file"),
py::arg("parameter_values") = py::none(),
py::arg("plugin_name") = py::none(),
py::arg("initialization_timeout") =
DEFAULT_INITIALIZATION_TIMEOUT_SECONDS)
.def("__repr__",
[](const ExternalPlugin<juce::AudioUnitPluginFormat> &plugin) {
std::ostringstream ss;
Expand All @@ -1909,8 +1912,9 @@ see :class:`pedalboard.VST3Plugin`.)
})
.def_static(
"get_plugin_names_for_file",
[](std::string filename) {
return getPluginNamesForFile<juce::AudioUnitPluginFormat>(filename);
[](py::object filename) {
return getPluginNamesForFile<juce::AudioUnitPluginFormat>(
pathToString(filename));
},
py::arg("filename"),
"Return a list of plugin names contained within a given Audio Unit "
Expand Down
5 changes: 3 additions & 2 deletions pedalboard/_pedalboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import platform
import re
import weakref
Expand Down Expand Up @@ -761,7 +762,7 @@ def __setattr__(self, name: str, value):


def load_plugin(
path_to_plugin_file: str,
path_to_plugin_file: Union[str, "os.PathLike[str]"],
parameter_values: Dict[str, Union[str, int, float, bool]] = {},
plugin_name: Union[str, None] = None,
initialization_timeout: float = 10.0,
Expand All @@ -774,7 +775,7 @@ def load_plugin(
- Audio Units are supported on macOS

Args:
path_to_plugin_file (``str``): The path of a VST3® or Audio Unit plugin file or bundle.
path_to_plugin_file (``str`` or ``os.PathLike``): The path of a VST3® or Audio Unit plugin file or bundle.

parameter_values (``Dict[str, Union[str, int, float, bool]]``):
An optional dictionary of initial values to provide to the plugin
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/io/AudioFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
#include "../juce_overrides/juce_PatchedFLACAudioFormat.h"
#include "../juce_overrides/juce_PatchedMP3AudioFormat.h"
#include "../juce_overrides/juce_PatchedWavAudioFormat.h"
#include "AudioFile.h"
#include "LameMP3AudioFormat.h"
#include "PathUtils.h"

namespace Pedalboard {

Expand Down
103 changes: 66 additions & 37 deletions pedalboard/io/AudioFileInit.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,58 +150,58 @@ inline void init_audio_file(
// instantiate subclasses via __new__.
.def_static(
"__new__",
[](const py::object *, std::string filename, std::string mode) {
if (mode == "r") {
return std::make_shared<ReadableAudioFile>(filename);
} else if (mode == "w") {
throw py::type_error("Opening an audio file for writing requires "
"samplerate and num_channels arguments.");
[](const py::object *, py::object filename, std::string mode,
py::object file_like) {
// Handle both filename and file_like kwargs for backward compat
py::object target;
if (!filename.is_none() && !file_like.is_none()) {
throw py::type_error(
"Cannot specify both 'filename' and 'file_like'");
} else if (!filename.is_none()) {
target = filename;
} else if (!file_like.is_none()) {
target = file_like;
} else {
throw py::type_error("AudioFile instances can only be opened in "
"read mode (\"r\") or write mode (\"w\").");
throw py::type_error(
"Must specify either 'filename' or 'file_like'");
}
},
py::arg("cls"), py::arg("filename"), py::arg("mode") = "r",
"Open an audio file for reading.")
.def_static(
"__new__",
[](const py::object *, py::object filelike, std::string mode) {

if (mode == "r") {
if (!isReadableFileLike(filelike) &&
!tryConvertingToBuffer(filelike)) {
throw py::type_error(
"Expected either a filename, a file-like object (with "
"read, seek, seekable, and tell methods) or a memory view, "
"but received: " +
py::repr(filelike).cast<std::string>());
// Check if this is a path-like object (str or has __fspath__)
if (isPathLike(target)) {
return std::make_shared<ReadableAudioFile>(
pathToString(target));
}

// Otherwise, try to handle as a file-like object or buffer
if (std::optional<py::buffer> buf =
tryConvertingToBuffer(filelike)) {
tryConvertingToBuffer(target)) {
return std::make_shared<ReadableAudioFile>(
std::make_unique<PythonMemoryViewInputStream>(*buf,
filelike));
} else {
target));
} else if (isReadableFileLike(target)) {
return std::make_shared<ReadableAudioFile>(
std::make_unique<PythonInputStream>(filelike));
std::make_unique<PythonInputStream>(target));
} else {
throw py::type_error(
"Expected either a filename, a file-like object (with "
"read, seek, seekable, and tell methods) or a memory view, "
"but received: " +
py::repr(target).cast<std::string>());
}
} else if (mode == "w") {
throw py::type_error(
"Opening an audio file-like object for writing requires "
"samplerate and num_channels arguments.");
throw py::type_error("Opening an audio file for writing requires "
"samplerate and num_channels arguments.");
} else {
throw py::type_error("AudioFile instances can only be opened in "
"read mode (\"r\") or write mode (\"w\").");
}
},
py::arg("cls"), py::arg("file_like"), py::arg("mode") = "r",
"Open a file-like object for reading. The provided object must have "
"``read``, ``seek``, ``tell``, and ``seekable`` methods, and must "
"return binary data (i.e.: ``open(..., \"w\")`` or ``io.BytesIO``, "
"etc.).")
py::arg("cls"), py::arg("filename") = py::none(),
py::arg("mode") = "r", py::kw_only(),
py::arg("file_like") = py::none(), "Open an audio file for reading.")
.def_static(
"__new__",
[](const py::object *, std::string filename, std::string mode,
[](const py::object *, py::object filename, std::string mode,
std::optional<double> sampleRate, int numChannels, int bitDepth,
std::optional<std::variant<std::string, float>> quality) {
if (mode == "r") {
Expand All @@ -217,8 +217,37 @@ inline void init_audio_file(
"argument to be provided.");
}

return std::make_shared<WriteableAudioFile>(
filename, *sampleRate, numChannels, bitDepth, quality);
// If this is a path-like object, open it as a file path.
if (isPathLike(filename)) {
return std::make_shared<WriteableAudioFile>(
pathToString(filename), *sampleRate, numChannels, bitDepth,
quality);
}

// Otherwise, try to handle as a file-like object.
// This can happen because pybind11 overload resolution matches
// py::object for both path-like and file-like inputs.
if (isWriteableFileLike(filename)) {
auto stream = std::make_unique<PythonOutputStream>(filename);
if (!stream->getFilename()) {
throw py::type_error(
"Unable to infer audio file format for writing. "
"Expected either a \".name\" property on the provided "
"file-like object (" +
py::repr(filename).cast<std::string>() +
") or an explicit file format passed as the "
"\"format=\" argument.");
}
return std::make_shared<WriteableAudioFile>(
std::string(""), std::move(stream), *sampleRate,
numChannels, bitDepth, quality);
}

throw py::type_error(
"Expected either a filename (str, bytes, or os.PathLike) "
"or a file-like object (with write, seek, seekable, and "
"tell methods), but received: " +
py::repr(filename).cast<std::string>());
} else {
throw py::type_error("AudioFile instances can only be opened in "
"read mode (\"r\") or write mode (\"w\").");
Expand Down
60 changes: 60 additions & 0 deletions pedalboard/io/PathUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.
*/

#pragma once

#include <pybind11/pybind11.h>

namespace py = pybind11;

namespace Pedalboard {

/**
* Check if a Python object is path-like (str or has __fspath__ method).
*
* NOTE: This function requires the GIL to be held by the caller.
*/
inline bool isPathLike(py::object obj) {
return py::isinstance<py::str>(obj) || py::hasattr(obj, "__fspath__");
}

/**
* Convert a Python path-like object (str, bytes, or os.PathLike) to a
* std::string, without requiring std::filesystem::path.
*
* NOTE: This function requires the GIL to be held by the caller.
*/
inline std::string pathToString(py::object path) {
// If it's already a string, just return it
if (py::isinstance<py::str>(path)) {
return path.cast<std::string>();
}

// Try calling os.fspath() to handle PathLike objects
try {
py::object os = py::module_::import("os");
py::object fspath = os.attr("fspath");
py::object result = fspath(path);
return result.cast<std::string>();
} catch (py::error_already_set &e) {
throw py::type_error(
"expected str, bytes, or os.PathLike object, not " +
std::string(py::str(path.get_type().attr("__name__"))));
}
}

} // namespace Pedalboard
Loading
Loading