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

Pedalboard objects are not pickleable or serializable #96

Open
adhooge opened this issue Mar 28, 2022 · 6 comments
Open

Pedalboard objects are not pickleable or serializable #96

adhooge opened this issue Mar 28, 2022 · 6 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@adhooge
Copy link

adhooge commented Mar 28, 2022

First of all, thanks a lot for the amazing library, it's a great help!

While toying around, I wanted to store Pedalboard instances by directly dumping them using pickle (https://docs.python.org/3/library/pickle.html).
This fails yielding a TypeError: cannot pickle 'Pedalboard' object.
After doing some research it might be due to the fact that Pedalboard.__dict__ is an empty dictionary and thus nothing can be pickled.
Besides, the __reduce__ method called by pickle for dumping yields the following error:

terminate called after throwing an instance of 'std::runtime_error'
what():  instance allocation failed: new instance has no pybind11-registered base types

I don't know if that is a relevant issue, I think it would be great to be able to save Pedalboard instances in some way. Maybe pickling it is not the correct way to do it and there exists another technique?

I'd love to help fixing that issue if it is considered relevant.

@psobot
Copy link
Member

psobot commented Mar 28, 2022

Thanks @adhooge!

Pedalboard objects are definitely not (yet) pickleable; I'm usually a bit wary of pickling (due to the nasty edge-cases that can arise) but I think it'd be reasonable in this case. It'd be possible to add this functionality by:

  • choosing which state to serialize (probably all plugin parameters, plus internal plugin state for VSTs/Audio Units)
  • adding a default py::pickle definition to the Plugin object in C++
  • adding comprehensive tests for pickling different types of plugins

There's also a couple of complications to consider, like what happens when pickling/unpickling a plugin that references an external file? (i.e.: Convolution or some VSTs) Should we add dict-based serialization as well, so that people can serialize Pedalboard plugins to their own format of choice? (JSON, YAML, etc)

@psobot psobot changed the title TypeError: cannot pickle 'Pedalboard' object Pedalboard objects are not pickleable or serializable Mar 28, 2022
@psobot psobot added enhancement New feature or request help wanted Extra attention is needed labels Mar 28, 2022
@adhooge
Copy link
Author

adhooge commented Mar 28, 2022

Thanks for your answer!

I did not think about the possible issue of Plugins with external files, it would indeed require further consideration.
Saving the plugins in JSON or other similar format might be useful too.
Essentially, I believe it might be an interesting feature to be able to save a Pedalboard in some way when processing data for future use. It could be done manually for each project through some kind of configuration file but maybe a standardized way of doing it through Pedalboard might help?

@adhooge
Copy link
Author

adhooge commented Apr 14, 2022

Hi!

I did not try changing the entire module since I'm not familiar with C/C++ bindings in Python but I've written simple helper functions for accessing the parameters of plugins. It's kinda ugly and I don't think it works on complex plugins but it is working fine with the default plugins (Distortion, Chorus, Compressor ...) which are the ones I am using right now. I share my code here, hoping it will help a few other people.

def get_fx_settings(fx: pdb.Plugin):
    fx_settings = {}
    items = list(fx.__class__.__dict__.items())
    for item in items:
        if isinstance(item[1], property):
            fx_settings[item[0]] = item[1].__get__(fx, fx.__class__)
    return fx_settings


def set_fx_settings(fx: pdb.Plugin, settings: dict):
    items = list(fx.__class__.__dict__.items())
    for item in items:
        if isinstance(item[1], property):
            if item[0] not in settings.keys():
                warnings.warn(f'{item[0]} not found in settings. Keeping previous value.', UserWarning)
            else:
                item[1].__set__(fx, settings[item[0]])
    return fx

The output of get_fx_settings is a dictionary so it can easily be saved in whichever way you find convenient.

Minimal working example:

import pedalboard as pdb

disto = pdb.Distortion(40)
settings = get_fx_settings(disto)
print(settings)                     # {'drive_db': 40.0}
disto2 = pdb.Distortion()   
print(disto2.drive_db)         # 25.0
disto2 = set_fx_settings(disto2, settings)
print(disto2.drive_db)         # 40.0

@hagenw
Copy link

hagenw commented Oct 17, 2022

I guess the main goal of serialization is to re-load a given set of transformations, e.g. when stored in a cache together with the audio files. One way is to serialize to YAML, which has the advantage that it is also human readable. You can achieve this by deriving your classes from audobject.Object. If you want to handle random arguments, you also need to have another object handling those.

Let's imagine we have a transform pedalboard.PinkNoise that has a snr_db argument, inherits from audobject.Object and is serializable as well as pedalboard.observe.List which also dervies from audobject.Object and handles drawing numbers randomly from a list:

transform = pedalboard.PinkNoise(snr_db=pedalboard.observe.List([-5, 0, 10, 20]))

You can then store it to a YAML file:

transform.to_yaml('transform.yaml')

and load it from the YAML file:

import audobject

transform = audobject.from_yaml('transform.yaml')

The corresponding YAML file looks like this:

$pedalboard.PinkNoise==0.7.0:
  snr_db:
    $pedalboard.observe.List==0.7.0:
      elements:
      - -5
      - 0
      - 10
      - 20

@0xdevalias
Copy link

Here are some other tangentially related notes from another issue, about serialising plugin parameters to json, and loading them back again:


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)

Originally posted by @psobot in #187 (comment)


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)

Originally posted by @garraww in #187 (comment)

@hagenw
Copy link

hagenw commented Mar 22, 2024

We have recently released https://github.com/audeering/auglib for audio augmentations. You can use auglib.transform.Function as a wrapper for pedalboard transforms and then serialize them.
For an example see https://audeering.github.io/auglib/external.html#pedalboard, which automatically serializes the transform and caches the results.

You can also include random parameters in a transform:

import auglib
import pedalboard


def pedalboard_transform(signal, sampling_rate, threshold_db, ratio, room_size):
    r"""Custom augmentation using pedalboard."""
    import pedalboard
    board = pedalboard.Pedalboard(
        [   
            pedalboard.Compressor(threshold_db=threshold_db, ratio=ratio),
            pedalboard.Chorus(),
            pedalboard.Phaser(),
            pedalboard.Reverb(room_size=room_size),
        ],  
    )   
    return board(signal, sampling_rate)


transform = auglib.transform.Compose(
    [   
        auglib.transform.Function(
            pedalboard_transform,
            function_args={
                "threshold_db": auglib.observe.IntUni(-55, -45),
                "ratio": auglib.observe.IntUni(20, 30),
                "room_size": auglib.observe.FloatNorm(0.25, 0.02),
            },
        ),  
        auglib.transform.NormalizeByPeak(),
    ]   
)

Serialize it to a YAML file.

transform.to_yaml("transform.yaml")

Inspect the YAML file

with open("transform.yaml", "r") as f:
    print(f.read())

which returns

$auglib.core.transform.Compose==1.0.2:
  transforms:
  - $auglib.core.transform.Function==1.0.2:
      function: "def pedalboard_transform(signal, sampling_rate, threshold_db, ratio,\
        \ room_size):\n    r\"\"\"Custom augmentation using pedalboard.\"\"\"\n  \
        \  import pedalboard\n    board = pedalboard.Pedalboard(\n        [   \n \
        \           pedalboard.Compressor(threshold_db=threshold_db, ratio=ratio),\n\
        \            pedalboard.Chorus(),\n            pedalboard.Phaser(),\n    \
        \        pedalboard.Reverb(room_size=room_size),\n        ],  \n    )   \n\
        \    return board(signal, sampling_rate)\n"
      function_args:
        threshold_db:
          $auglib.core.observe.IntUni==1.0.2:
            low: -55
            high: -45
        ratio:
          $auglib.core.observe.IntUni==1.0.2:
            low: 20
            high: 30
        room_size:
          $auglib.core.observe.FloatNorm==1.0.2:
            mean: 0.25
            std: 0.02
            minimum: -.inf
            maximum: .inf
      preserve_level: false
      bypass_prob: null
  - $auglib.core.transform.NormalizeByPeak==1.0.2:
      peak_db: 0.0
      clip: false
      preserve_level: false
      bypass_prob: null
  preserve_level: false
  bypass_prob: null

Load transform from YAML file

import audobject

transform = audobject.from_yaml("transform.yaml")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants