Linking multiple GUI components to APVTS Parameter

I am creating a Viewport that allows its contained component to be zoomed. Eventually I will probably control the zoom with gestures, but for now I am using a Slider since JUCE does not natively support pinch gestures on iOS (please let me know if I’m wrong about this!). Currently, I have a Parameter in my APVTS called “viewportZoomScaleFactor”; I have a struct AttachedSlider that connects a Slider to that Parameter with a juce::AudioProcessorValueTreeState::SliderAttachment and also creates a ParameterAttachment, with the idea that the ParameterAttachment callback would update the zoom of the Viewport.

struct AttachedSlider
    {
        AttachedSlider(PitchPlanePluginAudioProcessor& p, juce::String paramID, std::function<void(float)> paramChangedCallback, juce::UndoManager* undoManager) :
            sliderAttachment(p.getApvts(), paramID, slider),
            parameterAttachment(*p.getApvts().getParameter(paramID), paramChangedCallback, undoManager)
        {
            slider.setSliderStyle(juce::Slider::RotaryVerticalDrag);
            slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, slider.getTextBoxWidth(), slider.getTextBoxHeight());
        }
        
        juce::Slider slider;
        juce::AudioProcessorValueTreeState::SliderAttachment sliderAttachment;
        juce::ParameterAttachment parameterAttachment;
    };

}

I define the ParameterAttachment callback and then instantiate an AttachedSlider:

std::function<void(float)> zoomCallback = [this] (float newValue)
    {
        float value = audioProcessor.getApvts().getRawParameterValue("viewportZoomScaleFactor")->load();
        viewedComponent.setTransform(juce::AffineTransform::scale(value));
        viewedComponentContainer.setBounds(playSurface.getBoundsInParent());
        DBG("ZOOM: " << value);
    };
    AttachedSlider zoomSlider { audioProcessor, "viewportZoomScaleFactor", zoomCallback, nullptr };

I did it this way because I thought it made more sense for the Viewport to be attached to the actual Parameter, rather than to respond to onSliderValueChanged, but I’m wondering if I’ve overcomplicated everything. Also I am aware that I could have the ParameterAttachment be a member of the Viewport rather than part of the AttachedSlider struct, this was just a quicker way for me to test this for now. Anyway, the Viewport zooms almost as expected. On to the problems:

  1. It seems that the ParameterAttachment callback happens before the most recent Parameter value is updated. So my Viewport is always zoomed according to the penultimate Parameter value, rather than the current value. I tried looking at the ParameterAttachment code but I haven’t figured out exactly how the paramChangedCallback is triggered there.
  2. Would it be simpler to have SliderValueChanged() trigger the Viewport zoom functions? Since the Slider and Parameter are attached, a Parameter change would update the Slider, which would then update the Viewport. The issue I’m considering is if I have a view in which the Slider isn’t present, but I still want the Viewport to zoom in response to DAW automation, are there potential problems?
  3. Maybe part of the problem is the ParameterAttachment::setValueAsCompleteGesture versus continuous gestures? Do I need to incorporate startGesture() and endGesture() in some way?
  4. Within the ParameterAttachment callback std::function, I’m not sure what the input float argument is. Does the ParameterAttachment automatically call the callback with the updated Parameter value somehow? Again, I did look at the juce code but I’m struggling to understand how the callback is instantiated/implemented.
    Thanks for any help!

Just put three back ticks before and after the code on a single line :slight_smile:

1 Like

Thanks @Daniel really appreciate you, I’ve learned so much from your posts (including the idea of having an AttachedSlider struct, which has saved me a lot of time!)

1 Like

I only skimmed it, but in general I would avoid putting the zoom factor as AudioProcessorParameter.
Those parameters are to communicate DSP related values to the host. It is easy to see, if you consider, if a user would want to automate the zoom from their DAW.

You don’t need an AudioProcessorParameter to store the zoom setting. You are free to place whichever information you want to store in getStateInformation() and setStateInformation()

I hear you as a general rule, but in this case I do actually want the zoom factor to be automatable, and I’m also just wondering about general good practices for syncing multiple GUI objects and this issue with the ParameterAttachment callback timing. I will definitely look into get- and setStateInformation() for other variables that I don’t want to be automated though!

For timing and for listeners it is all the same in juce: you cannot assume any order.

Attaching two GUI controls to the same parameter should be no problem, I did that several times. What could be a problem, if one control has a different number of steps, e.g. combining a button and a slider would (potentially, I haven’t checked) render the slider effectively two state as well. But that is kinda obvious.

Also note the comment in APVTS parameterChanged(): at the time firing it is possible, that not all parameters are updated for that processBlock call yet, because this callback is sent synchronously, but not necessarily from the audio thread. It can come from any thread, which might be a problem.

That’s a helpful piece of advice regarding the order of events. Perhaps I’ll try some other listener patterns and see what I can come up with. I need to learn more about threading but my main concern is just that I want the GUI zoom function to respond to the current (most-up-to-date) Parameter value rather than the second-to-last Parameter value.