Slider - change attached parameter dynamically

Hello forum,

I’m working on a MIDI plugin / standalone app to control an orchestra of 7 hardware devices, each one having its own dedicated MIDI input channel and the same set of CC parameters.
For now I have a “MIDI channel” drop-down menu, and a set of knobs that allow to control one device at a time. It’s working well enough as long as I instantiate one plugin per device and set the right channel on each instance.

I’m currently trying to update the plugin to control all the devices from the same instance.
I duplicated the set of device-specific parameters and put them in parameter groups, and I’d like the knobs to reflect and control what’s happening on the currently selected channel / parameter group instead of bloating the UI with a much larger number of knobs.

I’m looking for advice from more experienced JUCE users before diving into various experiments.
So far I can think of three possible strategies :

  • create all the sliders 7 times and only paint and listen to the active ones (doesn’t feel very elegant but maybe the best and most straightforward solution)
  • modify / update slider attachments, but the SliderAttachment API (constructor only) doesn’t suggest this is a good idea
  • use some generic logic based on calls like slider.getValueObject().referTo(/* ... */) and sliderRange.convertTo0to1(slider.getValueObject().getValue()) which I already used successfully to allow linking / unlinking two knobs in the plugin

I would probably try solution 1 first, but any advice is welcome.
Thanks in advance !

I guess it’s that way in order to keep it simple. But I don’t think there’s anything against re-doing attachments on the fly and never got any problem doing so.

1 Like

That’s great to hear.
I was afraid I would potentially run into problems with automations, or encounter unexpected issues with the parameters in the hosts.
From what you say it seems that it’s only AudioParameter’s job to deal with the connection with the host, which definitely makes sense (and sounds like what I expected).
Thanks a lot for the tip, it helps a lot.
I’ll post back here when I’m done updating my code.

1 Like

I think using a std::unique_ptr is the way to go here.
If you change the attachment to a new attachment, make sure to set the uniqueptr first to null before creating the new attachment. Otherwise you will run into a callback „loop“

2 Likes

Thanks @Rincewind for the additional advice.

In my existing code I’m declaring my attachments in a header file like std::unique_ptr<SliderAttachment> attachment; and instantiating them in the constructor implementation using attachment.reset(new SliderAttachment(/* ... */));
I was hoping that resetting the unique_ptr’s to new attachments whenever needed would just work, but I’ll remember to try and reset to nullptr first if it is not enough.
I need to keep reading JUCE’s codebase, I didn’t go through AudioProcessorValueTreeState yet and can’t figure out how this callback loop could occur.

For now I’m still in the process of refactoring …

Cheers !

It won’t be enough, you are creating the new attachment while the old attachment is still active. First thing a new attachment does, is flush the current parameter value to the component, which then gets picked up by the soon to be deleted attachment and by that flushed into the old parameter.

You are (by accident) overwriting the old attached parameter value with the new attached parameter. Possibly a very difficult bug to spot even for the end user, who doesn’t realise, that his configuration goes to hell.

1 Like

I see, thanks for saving me from future headaches :slight_smile:
I’ll dig this and keep you posted here.

1 Like

Ok, so after spending some time figuring out how to fix a segfault due to my MidiChannel parameter trying to modify some not-existing-yet attachments, I can at last confirm that the technique is working fine.

Thanks @mathieudemange for pointing me in the right direction, and thanks @Rincewind for the extra hint, setting the std::unique_ptr<SomeGuiAttachment> to nullptr before creating the new attachment is indeed required for the technique to work.

For the record, I ended up using something like this (inspired by this post from @daniel) :

// abstract base attachment wrapper class
class DynamicGuiAttachment {
protected:
    juce::AudioProcessorValueTreeState& apvts;
    juce::String parameterId;

    juce::String getFullParameterId(juce::String groupId) {
        return groupId + parameterGroupSeparator + parameterId;
    }

public:
    DynamicGuiAttachment(
        juce::AudioProcessorValueTreeState& vts,
        juce::String paramId
    ) : apvts(vts), parameterId(paramId) {}

    virtual void attachToGroup(juce::String groupId) = 0;
};

// implementation for slider (one must exist for each type of GUI control)
class DynamicSliderAttachment : public DynamicGuiAttachment {
    std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
    juce::Slider& slider;

public:
    DynamicSliderAttachment(
        juce::AudioProcessorValueTreeState& vts,
        juce::String paramId,
        juce::Slider& sldr
    ) : DynamicGuiAttachment(vts, paramId), slider(sldr) {}

    void attachToGroup(juce::String groupId) override {
        attachment = nullptr; // MANDATORY !!!
        attachment.reset(new SliderAttachment(
            apvts, getFullParameterId(groupId), slider
        ));
    }
};

class MyWrapperComponent {
    std::map<std::string, std::unique_ptr<DynamicGuiAttachment>> attachments;
    juce::Slider mySlider;

public:
    MyWrapperComponent(PluginProcessor& processor) {
        addAndMakeVisible(mySlider);
        attachments["myParameter"] = std::make_unique<DynamicSliderAttachment>(
            processor.getValueTreeState(), "myParameter", mySlider
        );
        // etc ...
    }

    // groupId is inferred from the currently selected channel
    void setActiveParameterGroup(juce::String groupId) {
        for (const auto& [key, attachment] : attachments) {
            attachment->attachToGroup(groupId);
        }
    }
};

I should precise that I’m using an AudioProcessorParameterGroup containing the same set of AudioParameterFloat’s for each channel in the AudioProcessorValueTreeState, and a "global" group with a "MidiChannel" parameter to control which channel group the GUI controls are attached to.

Hope this might help someone …
Cheers !

1 Like

That looks very good to me! But if I may :slight_smile: - I wouldn’t put the MIDI Channel (/group of parameters) that the UI is currently controlling as an AudioParameter into the tree. If I understand correctly your plugin is behaving differently deepening on the MIDI channel but the audio rendering doesn’t care what set of parameters the UI is currently editing. I’d rather use a ValueTree (future proof for more only frontend related values) or just a juce::Value that you attach listeners to. You can add these states as xml to your set and getStateInformation methods. Just make sure to lock the MessageThread before creating the XML/access the Value object while in the getStateInformation.

Yes you’re right, it doesn’t make much sense to automate only the displaying of channel specific messages passing through the plugin. Actually the plugin is MIDI only, no audio is involved at all, and it’s behaving exactly the same whatever channel is selected, the channel drop down menu is only here for display and manual control purpose.

I already have an "Expanded" non-audio toggle parameter that allows me to expand or collapse the plugin’s GUI, which I create and access like this :

auto e = apvts.state.getOrCreateChildWithName("UIParameters", nullptr);
e.getProperty("Expanded", false); // get parameter value
e.setProperty("Expanded", true, nullptr); // set parameter value

I guess I could add the "MidiChannel" parameter to the "UIParameters" subtree.

Concerning the locking, I’m already using atomic structures to pass values around between processBlock and the MessageThread, so I think it should be OK to do the same for MidiChannel.
I thought getStateInformation and setStateInformation were called on the MessageThread, but maybe I don’t really get your point here ?