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

Add VST3 preset_data property to get/set .vstpreset data directly #351

Merged
merged 2 commits into from
Jul 30, 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
91 changes: 73 additions & 18 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -640,33 +640,70 @@ class ExternalPlugin : public AbstractExternalPlugin {
}
}

struct PresetVisitor : public juce::ExtensionsVisitor {
const std::string presetFilePath;
struct SetPresetVisitor : public juce::ExtensionsVisitor {
const juce::MemoryBlock &presetData;
bool didSetPreset;

PresetVisitor(const std::string presetFilePath)
: presetFilePath(presetFilePath) {}
SetPresetVisitor(const juce::MemoryBlock &presetData)
: presetData(presetData), didSetPreset(false) {}

void visitVST3Client(
const juce::ExtensionsVisitor::VST3Client &client) override {
juce::File presetFile(presetFilePath);
juce::MemoryBlock presetData;
this->didSetPreset = client.setPreset(presetData);
}
};

if (!presetFile.loadFileAsData(presetData)) {
throw std::runtime_error("Failed to read preset file: " +
presetFilePath);
}
void loadPresetFile(std::string presetFilePath) {
juce::File presetFile(presetFilePath);
juce::MemoryBlock presetData;

if (!client.setPreset(presetData)) {
throw std::runtime_error(
"Plugin returned an error when loading data from preset file: " +
presetFilePath);
}
if (!presetFile.loadFileAsData(presetData)) {
throw std::runtime_error("Failed to read preset file: " + presetFilePath);
}

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

void setPreset(const void *data, size_t size) {
juce::MemoryBlock presetData(data, size);
SetPresetVisitor visitor{presetData};
pluginInstance->getExtensions(visitor);
if (!visitor.didSetPreset) {
throw std::runtime_error("Failed to set preset data for plugin: " +
pathToPluginFile.toStdString());
}
}

struct GetPresetVisitor : public juce::ExtensionsVisitor {
// This block will get updated with the current preset data when
// visiting VST3 clients.
juce::MemoryBlock &presetData;
bool didGetPreset;

GetPresetVisitor(juce::MemoryBlock &presetData)
: presetData(presetData), didGetPreset(false) {}

void visitVST3Client(
const juce::ExtensionsVisitor::VST3Client &client) override {
this->presetData = client.getPreset();
this->didGetPreset = true;
}
};

void loadPresetData(std::string presetFilePath) {
PresetVisitor visitor{presetFilePath};
void getPreset(juce::MemoryBlock &dest) const {
// Get the plugin state's .vstpreset representation if possible.
GetPresetVisitor visitor(dest);
pluginInstance->getExtensions(visitor);

if (!visitor.didGetPreset) {
throw std::runtime_error("Failed to get preset data for plugin " +
pathToPluginFile.toStdString());
}
}

void reinstantiatePlugin() {
Expand Down Expand Up @@ -1633,9 +1670,27 @@ example: a Windows VST3 plugin bundle will not load on Linux or macOS.)
return ss.str();
})
.def("load_preset",
&ExternalPlugin<juce::PatchedVST3PluginFormat>::loadPresetData,
&ExternalPlugin<juce::PatchedVST3PluginFormat>::loadPresetFile,
"Load a VST3 preset file in .vstpreset format.",
py::arg("preset_file_path"))
.def_property(
"preset_data",
[](const ExternalPlugin<juce::PatchedVST3PluginFormat> &plugin) {
juce::MemoryBlock presetData;
plugin.getPreset(presetData);
return py::bytes((const char *)presetData.getData(),
presetData.getSize());
},
[](ExternalPlugin<juce::PatchedVST3PluginFormat> &plugin,
const py::bytes &presetData) {
py::buffer_info info(py::buffer(presetData).request());
plugin.setPreset(info.ptr, static_cast<size_t>(info.size));
},
"Get or set the current plugin state as bytes in .vstpreset "
"format.\n\n"
".. warning::\n This property can be set to change the "
"plugin's internal state, but providing invalid data may cause the "
"plugin to crash, taking the entire Python process down with it.")
.def_static(
"get_plugin_names_for_file",
[](std::string filename) {
Expand Down
57 changes: 57 additions & 0 deletions tests/test_external_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT + AVAILABLE_INSTRUMENT_PLUGINS_IN_TEST_ENVIRONMENT
)

AVAILABLE_VST3_PLUGINS_IN_TEST_ENVIRONMENT = [
f for f in AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT if "vst3" in f
]

ONE_AVAILABLE_TEST_PLUGIN = (
[AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT[0]] if AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT else []
)
Expand Down Expand Up @@ -302,6 +306,59 @@ def test_preset_parameters(plugin_filename: str, plugin_preset: str):
), f"Expected attribute {name} to be different from default ({default}), but was {actual}"


@pytest.mark.skipif(
not AVAILABLE_VST3_PLUGINS_IN_TEST_ENVIRONMENT,
reason="No VST3 plugin containers installed in test environment!",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reason="No VST3 plugin containers installed in test environment!",
reason="No VST3 plugins installed in test environment!",

)
@pytest.mark.parametrize("plugin_filename", AVAILABLE_VST3_PLUGINS_IN_TEST_ENVIRONMENT)
def test_get_vst3_preset(plugin_filename: str):
plugin = load_test_plugin(plugin_filename)
preset_data: bytes = plugin.preset_data

assert (
preset_data[:4] == b"VST3"
), "Preset data for {plugin_filename} is not in .vstpreset format"
# Check that the class ID (8 bytes into the data) is a 32-character hex string.
cid = preset_data[8:][:32]
assert all(c in b"0123456789ABCDEF" for c in cid), f"CID contains invalid characters: {cid}"


@pytest.mark.skipif(not plugin_named("Magical8BitPlug"), reason="Missing Magical8BitPlug 2 plugin.")
def test_set_vst3_preset():
plugin_file = plugin_named("Magical8BitPlug")
assert (
plugin_file is not None and "vst3" in plugin_file
), f"Expected a vst plugin: {plugin_file}"
plugin = load_test_plugin(plugin_file)

# Pick a known valid value for one of the plugin parameters.
default_gain_value = plugin.gain
new_gain_value = 1.0
assert (
default_gain_value != new_gain_value
), f"Expected default gain to be different than {new_gain_value}"

# Update the parameter and get the resulting .vstpreset bytes.
plugin.gain = new_gain_value
preset_data = plugin.preset_data

# Change the parameter back to the default value.
plugin.gain = default_gain_value

# Sanity check that the parameter was successfully set.
assert (
plugin.gain == default_gain_value
), f"Expected gain to be reset to {default_gain_value}, but got {plugin.gain}"

# Load the .vstpreset bytes and make sure the parameter was
# updated.
plugin.preset_data = preset_data

assert (
plugin.gain == new_gain_value
), f"Expected gain to be {new_gain_value}, but got {plugin.gain}"


@pytest.mark.parametrize("plugin_filename", AVAILABLE_PLUGINS_IN_TEST_ENVIRONMENT)
def test_initial_parameters(plugin_filename: str):
parameters = {
Expand Down
Loading