Open adhooge opened 2 years ago
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:
py::pickle
definition to the Plugin
object in C++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)
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?
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
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
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 https://github.com/spotify/pedalboard/issues/187#issuecomment-1376205304
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 https://github.com/spotify/pedalboard/issues/187#issuecomment-1692655527
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")
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 usingpickle
(https://docs.python.org/3/library/pickle.html). This fails yielding aTypeError: cannot pickle 'Pedalboard' object
. After doing some research it might be due to the fact thatPedalboard.__dict__
is an empty dictionary and thus nothing can be pickled. Besides, the__reduce__
method called bypickle
for dumping yields the following error: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.