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

AudioProcessorParameter raw_value Setter Problem #243

Open
kleblackford opened this issue Aug 17, 2023 · 12 comments
Open

AudioProcessorParameter raw_value Setter Problem #243

kleblackford opened this issue Aug 17, 2023 · 12 comments

Comments

@kleblackford
Copy link

kleblackford commented Aug 17, 2023

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")>}
@kleblackford kleblackford changed the title Raw Value Setter Prbolem AudioProcessorParameter Raw Value Setter Problem Aug 17, 2023
@kleblackford kleblackford changed the title AudioProcessorParameter Raw Value Setter Problem AudioProcessorParameter raw_value Setter Problem Aug 17, 2023
@nafeu
Copy link

nafeu commented Aug 19, 2023

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.

@kleblackford
Copy link
Author

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.

@psobot
Copy link
Member

psobot commented Aug 22, 2023

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?

@kleblackford
Copy link
Author

kleblackford commented Aug 23, 2023

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.

@kleblackford
Copy link
Author

kleblackford commented Aug 23, 2023

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...

@kleblackford
Copy link
Author

kleblackford commented Aug 23, 2023

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. :)

@kleblackford
Copy link
Author

kleblackford commented Aug 24, 2023

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! :)

@0xdevalias
Copy link

0xdevalias commented May 16, 2024

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!

@kleblackford
Copy link
Author

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...

@0xdevalias
Copy link

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!

@fblang
Copy link

fblang commented Jun 20, 2024

@kleblackford If I have your permission I could take care of opening a pull request with your proposed changes. I don't want to take credit for it though, I would just be happy to have this in a release. Let me know what you think.

@kleblackford
Copy link
Author

Go ahead - sorry I was too lazy to get round to it! Thanks :)

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

5 participants