spotify / pedalboard

🎛 🔊 A Python library for audio.

Home Page:https://spotify.github.io/pedalboard

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

AudioProcessorParameter raw_value Setter Problem

kleblackford opened this issue · comments

Setting the raw_value attribute of an AudioProcessorParameter object does not work. I was attempting this (suggested here) as a workaround for the fact that resolution is usually lost when setting a parameter (should values really always be mapped between the minimum and maximum with a step count of 1000?)

Expected behaviour

Setting the raw_value attribute changes the plugin parameter value directly.

Actual behaviour

Nothing happens.

Steps to reproduce the behaviour

The following code with the attached VST plugin demonstrates this problem.

Amplifier.zip

import pedalboard

amplifier = pedalboard.load_plugin("Amplifier.vst3")

print("Parameters:", amplifier.parameters)

amplifier.amplifier_gain.raw_value = 0.5

print("Parameters:", amplifier.parameters)

Output:

Parameters: {'amplifier_gain': <pedalboard.AudioProcessorParameter name="Amplifier Gain" discrete raw_value=0.01 value=1.00 range=(0.0, 100.0, 0.1)>, 'bypass': <pedalboard.AudioProcessorParameter name="Bypass" discrete raw_value=0 value=Off boolean ("False" and "True")>}
Parameters: {'amplifier_gain': <pedalboard.AudioProcessorParameter name="Amplifier Gain" discrete raw_value=0.01 value=1.00 range=(0.0, 100.0, 0.1)>, 'bypass': <pedalboard.AudioProcessorParameter name="Bypass" discrete raw_value=0 value=Off boolean ("False" and "True")>}

I was able to reproduce your issue and I have also been attempting to use raw_value to set a parameter value, however when looking through the docs I found that it isn't exactly specified that raw_value could be used as a setter, it only makes mention of accessing that value and implies "Raw Control". I think this means you can use still use a value between [0, 1] but you have to calculate the actual value to set based on the given bounds. As a sort of workaround I did the following:

I used the range on effect.parameters[PARAM_NAME] to find the min-max values, they usually look something like:

(0.0, 100.0, 0.1)

Where the first two values are the min and max respectively. Then I added these two helpers:

def map_value(value, out_min, out_max):
  return out_min + value * (out_max - out_min)

def set_parameter_by_raw_value(plugin, parameter, new_raw_value):
  parameter_range = plugin.parameters[parameter].range
  min_value = parameter_range[0]
  max_value = parameter_range[1]
  updated_value = map_value(new_raw_value, min_value, max_value)
  setattr(plugin, parameter, updated_value)

Where map_value is a simple interpolation to map a value from [0, 1] to any given min/max range and set_parameter_by_raw_value uses that raw value number to set the correct unit value on the plugin parameter you want. Then I can use them like so:

effect = load_plugin("/Library/Audio/Plug-Ins/VST3/Raum.vst3")

print('--- RANGE ---')
print(effect.mix.range)

print('--- BEFORE ---')
print('VALUE:', effect.mix)
print('RAW_VALUE:', effect.mix.raw_value)

set_parameter_by_raw_value(effect, 'mix', 0.75)

print('--- AFTER ---')
print('VALUE:', effect.mix)
print('RAW_VALUE:', effect.mix.raw_value)

Output:

--- RANGE ---
(0.0, 100.0, 0.1)
--- BEFORE ---
VALUE: 50.0
RAW_VALUE: 0.5
--- AFTER ---
VALUE: 75.0
RAW_VALUE: 0.75

And to confirm, applying this effect over an input signal does work as expected with the correct updated mix value.

Thanks for the suggestion. Unfortunately I need to set the raw value to avoid the quantization. Any how, adding the following method to AudioProcessorParameter achieves what I wanted.

  def set_raw_value(self, raw_value: float):
      with self.__get_cpp_parameter() as parameter:
          parameter.raw_value = raw_value

It is probably still worth refactoring how the quantitation is achieved. It seems inefficient to populate a dictionary with probes of raw values at 1000 points and determining a minimum/maximum and step from this as shown here.

Thanks @kleblackford! It seems like there are two bugs here:

  1. The parameter mapping logic should detect the values for your plugin's parameter, but is not doing so. I haven't been able to test with your example plugin just yet, but thank you for providing that; Pedalboard should be able to auto-detect the type of parameter and its possible values.
  2. The raw_value attribute should be directly settable for cases like this; that's a bug. I'm not going to add a set_raw_value method, but adding a @property (or a special case in __setattr__) should do the trick to fix that.

It is probably still worth refactoring how the quantitation is achieved. It seems inefficient to populate a dictionary with probes of raw values at 1000 points and determining a minimum/maximum and step from this as shown here.

I agree, but given that Pedalboard only has access to the VST3 (and similar Audio Unit) API, I'm not sure of a better way to do this. VST3 only provides a floating-point parameter value (between 0 and 1) and an API to allow users to get the raw value for a provided text label. If there's an API I'm overlooking, I'd love to use it instead. Do you have any suggestions?

No worries. My use case is quite far from typical plugin applications so I have encountered a fair few problems (with JUCE also) so far :D

  1. It seems to me as the auto mapping works in most cases. But assuming a 1000 count step for continuous values is limiting. This caused problems for me as I need full precision when setting coefficients in my plugin. I think I have found a workaround (using OSC to send double precision floats/strings to my plugin).
  2. Of course, sounds reasonable. My workaround was just a quick lazy solution!

Yes, I think I have an idea of how this could be done:

The maximum, minimum and the type of a parameter can easily be obtained by asking the plugin for the string representations of the raw values 0 and 1 (we should probably add specific int support also).

Then we need to get directly the count to use in the quantisation. In the VST3 API there is stepCount which is exactly what is needed. We can use this to map values between the minimum and maximum. The JUCE equivalent of this is AudioProcessorParameter::getNumSteps (as used here). Unfortunately this does not seem to be working as intended with pedalboard. See the attached JUCE project test plugin with the following example:

parameterTestPlugin.zip

import pedalboard

plugin = pedalboard.load_plugin(r"parameterTestPlugin.vst3")

print(f"Parameters: \n {plugin.parameters} \n")

print("Ranged Int:")
print(f"\tRange:  {plugin.rangedint.range}")
print(f"\tSteps:  {plugin.rangedint.num_steps}")
print("Ranged Stepped Float:")
print(f"\tRange:  {plugin.rangedfloatwithstep.range}")
print(f"\tSteps:  {plugin.rangedfloatwithstep.num_steps}")
print("Continuous Ranged Float:")
print(f"\tRange:  {plugin.continousrangedfloat.range}")
print(f"\tSteps:  {plugin.continousrangedfloat.num_steps}")
print("Boolean:")
print(f"\tRange:  {plugin.boolean.range}")
print(f"\tSteps:  {plugin.boolean.num_steps}")
Parameters:
 {'rangedint': <pedalboard.AudioProcessorParameter name="rangedInt" discrete raw_value=0.375 value=50 range=(20.0, 100.0, 1.0)>, 'rangedfloatwithstep': <pedalboard.AudioProcessorParameter name="rangedfloatWithStep" discrete raw_value=0.1 value=1.0 range=(0.0, 10.0, 0.5)>, 'continousrangedfloat': <pedalboard.AudioProcessorParameter name="continousRangedFloat" discrete raw_value=0.1 value=1.0000000 range=(0.0, 10.0, ~0.010000125)>, 'boolean': <pedalboard.AudioProcessorParameter name="boolean" discrete raw_value=1 value=On boolean ("False" and "True")>, 'bypass': <pedalboard.AudioProcessorParameter name="Bypass" discrete raw_value=0 value=Off boolean ("False" and "True")>}

Ranged Int:
        Range:  (20.0, 100.0, 1.0)
        Steps:  2147483647      
Ranged Stepped Float:
        Range:  (0.0, 10.0, 0.5)
        Steps:  2147483647      
Continuous Ranged Float:        
        Range:  (0.0, 10.0, None)
        Steps:  2147483647
Boolean:
        Range:  (False, True, 1)
        Steps:  2

This could be a JUCE problem rather than a pedalboard problem. For example, with the AudioParameterInt, it seems that the getNumSteps method should return the correct number of steps (see here). If we can work out how to get the actual number of steps for the different parameter types then I would be happy to attempt a refactor of the current implementation.

One more question regarding:

I think I have found a workaround (using OSC to send double precision floats/strings to my plugin).

I was able to get some initial tests working, using OSC Receiver to send OSC messages with python-osc to my plugin. However when I load the plugin into pedalboard and try the same thing, the oscMessageReceived method only is called once I reach a debug point or the end of my code. Is there something inherent in the way pedalboard loads a plugin which causes the message manager not to process a message until a breakpoint (or end of the code). Perhaps it is something to do with using the plugin in a non-realtime capacity...

I figured it out. I fixed the problem by using OSCReceiver::RealtimeCallback instead of OSCReceiver::MessageLoopCallback . I suppose that loading the plugin in non-realtime deprioritizes the message thread, causing the message to be held. :)

Back to the original issue...

The source of the problem is in WeakTypeWrapper and I was able to solve it by defining setattr of WeakTypeWrapper after here as the following:

def __setattr__(self, name, value):
    if name == "_wrapped":
        return super().__setattr__(name, value)
    wrapped = self._wrapped()
    if hasattr(wrapped, name):
        return setattr(wrapped, name, value)
    if hasattr(super(), "__setattr__"):
        return super().__setattr__(name, value)
    raise AttributeError("'{}' has no attribute '{}'".format(base_type.__name__, name))

The tests seem to be passing with this change! :)

The source of the problem is in WeakTypeWrapper and I was able to solve it by defining setattr of WeakTypeWrapper after here as the following:

@kleblackford Curious, did you ever raise this fix as a PR? And if not, is that something you might be able to do? Would be awesome to see this land!

Back to the original issue...

The source of the problem is in WeakTypeWrapper and I was able to solve it by defining setattr of WeakTypeWrapper after here as the following:

def __setattr__(self, name, value):
    if name == "_wrapped":
        return super().__setattr__(name, value)
    wrapped = self._wrapped()
    if hasattr(wrapped, name):
        return setattr(wrapped, name, value)
    if hasattr(super(), "__setattr__"):
        return super().__setattr__(name, value)
    raise AttributeError("'{}' has no attribute '{}'".format(base_type.__name__, name))

The tests seem to be passing with this change! :)

Yeah, I fixed it with this. Never created a merge request with the changes though as I was working on a branch with some additional hacky fixes...

Yeah, I fixed it with this. Never created a merge request with the changes though

@kleblackford True true.. would you be open to separating them to a new branch and raising a merge request? Would be awesome to see the investigation/bugfix work you did get merged in for everyone's benefit!