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

Save and Load Presets Automatically for VST3 Plugins? #187

Open
Isak-Andersson opened this issue Jan 8, 2023 · 10 comments
Open

Save and Load Presets Automatically for VST3 Plugins? #187

Isak-Andersson opened this issue Jan 8, 2023 · 10 comments

Comments

@Isak-Andersson
Copy link

Isak-Andersson commented Jan 8, 2023

Hello. I'm trying to save the changes I make after I use the show_editor function. What's the best way of saving presets for plugins?

I tried dumping the whole session with dill.dump_session but this gives me this warning and error:
PicklingWarning: Pickling a PyCapsule (None) does not pickle any C data structures and could cause segmentation faults or other memory errors when unpickling. TypeError: cannot pickle 'pedalboard_native.io.WriteableAudioFile' object

Using load_preset with .fxp files isn't much better. It doesn't work with certain plugins and I also don't see a way of saving it. Maybe a save_preset function could be a feature in future versions?

Trying to load a preset for ChorusGAS.vst3 gives me this error:
RuntimeError: Plugin returned an error when loading data from preset file

Any ideas on how to approach this issue?

@psobot
Copy link
Member

psobot commented Jan 9, 2023

Hi @Isak-Andersson!

There are a couple ways forward here, in order from easiest to hardest:

  • Any changes you make with show_editor should be exposed to Pedalboard via the parameters of the plugin itself. (i.e.: if your plugin has a frequency_hz parameter, changing that value in the plugin's UI should result in the corresponding value changing when accessing plugin.frequency_hz).

    A simple way to serialize this data would be something like:

    my_plugin = load_plugin(...)
    my_plugin.show_editor() # make some edits
    
    # Read out the value of every parameter that this plugin exposes:
    param_value_dict = {parameter_name: getattr(my_plugin, parameter_name) for parameter_name in my_plugin.parameters.keys()}
    
    # Do something with param_value_dict, like serializing it to JSON:
    with open("params.json", "w") as f:
        json.dump(param_value_dict, f)
    
    # To reload, just iterate over this dictionary and use `setattr` instead:
    for parameter_name, serialized_value in param_value_dict.items():
        setattr(my_plugin, parameter_name, serialized_value)
  • Alternatively, if your plugin doesn't expose all of its UI parameters to its host, this would be a good reason for us to add setters and getters for all plugin state; i.e.: my_plugin.state could return a bytes object containing the entire plugin's serialized state, ready to be reloaded later.

Let me know if that first option works for you at all, and if not, I'll see what I can do about adding a .state property.

@Isak-Andersson
Copy link
Author

Thanks for the answer. I'm not very familiar with JSON files. Is there a good reason to use them instead of just pure txt files?

json.dump(param_value_dict, f) gives me this error:
TypeError: Object of type WeakTypeWrapper is not JSON serializable

Printing the dict it looks like this:
{'bypass': False, 'ch1_enabled': True, 'ch1_level_db': 0.0, 'ch2_level_db': 0.0, 'ch3_level_db': 0.0, 'ch4_level_db': 0.0, 'ch1_note': 'D#5', 'ch1_detune_cnt': 0.0, 'ch1_depth': 0.0, 'ch1_rate_hz': 0.5, 'ch1_lfo_wave': 'Sinus', 'ch1_feedback_x': 0.0, 'ch1_feed_low_cut_hz': 20.0, 'ch2_enabled': True, 'ch2_note': 'D#5', 'ch2_detune_cnt': 0.0, 'ch2_depth': 0.0, 'ch2_rate_hz': 0.5, 'ch2_lfo_wave': 'Sinus', 'ch2_feedback_x': 0.0, 'ch2_feed_low_cut_hz': 20.0, 'ch3_enabled': True, 'ch3_note': 'D#5', 'ch3_detune_cnt': 0.0, 'ch3_depth': 0.0, 'ch3_rate_hz': 0.5, 'ch3_lfo_wave': 'Sinus', 'ch3_feedback_x': 0.0, 'ch3_feed_low_cut_hz': 20.0, 'ch4_enabled': True, 'ch4_note': 'D#5', 'ch4_detune_cnt': 0.0, 'ch4_depth': 0.0, 'ch4_rate_hz': 0.5, 'ch4_lfo_wave': 'Sinus', 'ch4_feedback_x': 0.0, 'ch4_feed_low_cut_hz': 20.0, 'dry_wet_ratio': 50.0}

Looks reasonable to me. Any idea what the problem could be?

@psobot
Copy link
Member

psobot commented Jan 9, 2023

Is there a good reason to use them instead of just pure txt files?

Yes; JSON handles serialization for you, so you don't need to define your own file format, or write your own serialization and deserialization code. JSON is widely compatible with various programming languages, has types (i.e.: string, float, boolean, etc), is human readable, and can be nicely formatted automatically.

json.dump(param_value_dict, f) gives me this error:

Aha, great find - I haven't tested this code snippet with many plugins, and this falls over due to a problem in Pedalboard with boolean parameters. This is a bit longer but should work instead:

my_plugin = load_plugin(...)
my_plugin.show_editor() # make some edits

# Read out the value of every parameter that this plugin exposes:
param_value_dict = {parameter_name: getattr(my_plugin, parameter_name) for parameter_name in 
my_plugin.parameters.keys()}

# Unwrap boolean values (which Pedalboard tries to transparently wrap
# for developer convenience, but the JSON library is unfamiliar with):
from pedalboard.pedalboard import WrappedBool
param_value_dict = {k: (bool(v) if isinstance(v, WrappedBool) else v) for k, v in param_value_dict.items()}

# Do something with param_value_dict, like serializing it to JSON:
with open("params.json", "w") as f:
    json.dump(param_value_dict, f)

# To reload, just iterate over this dictionary and use `setattr` instead:
for parameter_name, serialized_value in param_value_dict.items():
    setattr(my_plugin, parameter_name, serialized_value)

@Isak-Andersson
Copy link
Author

Thank you. This works nicely for my purpose.

@Hikari-Tsai
Copy link

Just for the record, the path of class WrappedBool had beed modified. The import path is
from pedalboard._pedalboard import WrappedBool

@garraww
Copy link

garraww commented Aug 25, 2023

If you already have a JSON file configured with the parameter values ​​and you want to use that JSON to assign the values ​​to the object parameters :

#  Load parameter values ​​from JSON file
with open("params.json", "r") as f:
    param_value_dict = json.load(f)

#  For each parameter and value in the dictionary, assign the value to the effect object
for parameter_name, serialized_value in param_value_dict.items():
    setattr(effect, parameter_name, serialized_value)

@famzah
Copy link

famzah commented Oct 22, 2023

Hi @psobot,

First of all, congrats for the great work!

Alternatively, if your plugin doesn't expose all of its UI parameters to its host, this would be a good reason for us to add setters and getters for all plugin state; i.e.: my_plugin.state could return a bytes object containing the entire plugin's serialized state, ready to be reloaded later.

I hit this corner case with the VST plugins of MeldaProduction. Tested with the MSpectralPan plugin on Windows. The plugin functions properly once I configure it using show_editor() but I cannot get (nor save) its state.

If you have a quick hack or suggestion for the my_plugin.state bytes object, I can try it and report back.

@famzah
Copy link

famzah commented Oct 22, 2023

I noticed one more thing. Some of the plugin setting values do work and are loaded from the JSON. Those saved values are applied to the VST plugin in terms of its effect on the audio. But the plugin interface doesn't show the custom values. Therefore, the updates by setattr are not reflected in the UI. Is there a more early phase to init a VST plugin? Or the my_plugin.state bytes object will cure this all?

Edit> I also tried to provide the custom values as "parameter_values" when calling load_plugin().

@0xdevalias
Copy link

0xdevalias commented Nov 8, 2023

Alternatively, if your plugin doesn't expose all of its UI parameters to its host, this would be a good reason for us to add setters and getters for all plugin state; i.e.: my_plugin.state could return a bytes object containing the entire plugin's serialized state, ready to be reloaded later.

@psobot I would love this functionality as a bit of a 'just in case' fallback, as it's hard to know ahead of time what plugins are going to 'behave' in exposing all their features, and which aren't.


Is there a more early phase to init a VST plugin?

@famzah Looking at the pedalboard docs for load_plugin, it can take additional parameters, including the parameter_values:

Does that work for you?

Edit: My bad.. only just saw your edit that said you tried that already.

Edit 2: I also wonder if this issue is potentially related?

@0xdevalias
Copy link

This other issue may also be be relevant here, as it talks about another way of controlling preset loading/changing within VST plugins:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants