Using the slider attachment class with 2-value sliders


#1

Long shot i guess, but has anyone hacked the AudioProcessorValueTreeState::SliderAttachment class so that it will connect both the min and max values of a 2-value slider to the AudioProcessor?

if not, i’ll have to figure that out myself…so does anyone have any tips?


#2

I just had to resolve this. Here is my solution:

/*
  ==============================================================================

    TwoValueSliderAttachment.h
    Created: 11 Dec 2017 7:35:47pm
    Author:  Nikolay Tsenkov

  ==============================================================================
*/


#pragma once


#include "JuceHeader.h"


struct MultiValueAttachedControlBase :    public AudioProcessorValueTreeState::Listener,
                                          public AsyncUpdater
{
    MultiValueAttachedControlBase (AudioProcessorValueTreeState& s, Array<String> p)
        :
            state (s),
            paramIDs (p)
    {
        for (int i = 0; i < paramIDs.size (); ++i)
        {
            state.addParameterListener (paramIDs [i], this);
            lastValues.set (paramIDs [i], 0);
        }
    }

    void removeListener ()
    {
        for (int i = 0; i < paramIDs.size (); ++i)
        {
            state.removeParameterListener (paramIDs [i], this);
        }
    }

    void setNewUnnormalisedValue (const String& paramID, float newUnnormalisedValue)
    {
        if (AudioProcessorParameter* p = state.getParameter (paramID))
        {
            const float newValue = state
                .getParameterRange (paramID)
                .convertTo0to1 (newUnnormalisedValue);

            if (p->getValue() != newValue)
            {
                p->setValueNotifyingHost (newValue);
            }
        }
    }

    void sendInitialUpdate ()
    {
        for (int i = 0; i < paramIDs.size (); ++i)
        {
            if (float* v = state.getRawParameterValue (paramIDs [i]))
            {
                parameterChanged (paramIDs [i], *v);
            }
        }
    }

    void parameterChanged (const String& paramID, float newValue) override
    {
        lastValues.set (paramID, newValue);

        if (MessageManager::getInstance ()->isThisTheMessageThread())
        {
            cancelPendingUpdate ();
            setValue (paramID, newValue);
        }
        else
        {
            triggerAsyncUpdate ();
        }
    }

    void beginParameterChange (const String& paramID)
    {
        if (AudioProcessorParameter* p = state.getParameter (paramID))
        {
            p->beginChangeGesture();
        }
    }

    void endParameterChange (const String& paramID)
    {
        if (AudioProcessorParameter* p = state.getParameter (paramID))
        {
            p->endChangeGesture();
        }
    }

    void handleAsyncUpdate () override
    {
        for (int i = 0; i < paramIDs.size (); ++i)
        {
            const String paramID = paramIDs [i];
            setValue (paramID, lastValues [paramID]);
        }
    }

    virtual void setValue (const String&, float) = 0;

    AudioProcessorValueTreeState& state;
    Array<String> paramIDs;
    NamedValueSet lastValues;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiValueAttachedControlBase)
};


//==============================================================================


class TwoValueSliderAttachment
{
public:
    TwoValueSliderAttachment (
        AudioProcessorValueTreeState& stateToControl,
        const String& parameterID1,
        const String& parameterID2,
        Slider& sliderToControl
    )
        :
            pimpl (new Pimpl (
                stateToControl,
                parameterID1,
                parameterID2,
                sliderToControl
            ))
    {
    }

    ~TwoValueSliderAttachment ()
    {
    }

private:
    struct Pimpl :    private MultiValueAttachedControlBase,
                      private Slider::Listener
    {
        Pimpl (
            AudioProcessorValueTreeState& s,
            const String& p1,
            const String& p2,
            Slider& sl
        )
            :
                MultiValueAttachedControlBase (s, {p1, p2}),
                minParamID (p1),
                maxParamID (p2),
                slider (sl),
                ignoreCallbacks (false)
        {
            jassert (
                sl.getSliderStyle () == Slider::TwoValueHorizontal ||
                sl.getSliderStyle () == Slider::TwoValueVertical
            );

            NormalisableRange<float> range (
                s.getParameterRange (minParamID).start,

                s.getParameterRange (maxParamID).end,
                s.getParameterRange (maxParamID).interval,
                s.getParameterRange (maxParamID).skew
            );

            slider.setRange (range.start, range.end, range.interval);
            slider.setSkewFactor (range.skew, range.symmetricSkew);

            sendInitialUpdate();
            slider.addListener (this);
        }

        ~Pimpl()
        {
            slider.removeListener (this);
            removeListener();
        }

        void setValue (const String& paramID, float newValue) override
        {
            const ScopedLock selfCallbackLock (selfCallbackMutex);

            {
                ScopedValueSetter<bool> svs (ignoreCallbacks, true);

                bool allowNudgingOfOtherValues = true;
                if (paramID == minParamID)
                {
                    slider.setMinValue (
                        newValue,
                        sendNotificationSync,
                        allowNudgingOfOtherValues
                    );
                }
                else if (paramID == maxParamID)
                {
                    slider.setMaxValue (
                        newValue,
                        sendNotificationSync,
                        allowNudgingOfOtherValues
                    );
                }
                else
                {
                    jassertfalse;
                }
            }
        }

        void sliderValueChanged (Slider* s) override
        {
            const ScopedLock selfCallbackLock (selfCallbackMutex);

            if (
                (! ignoreCallbacks) &&
                (! ModifierKeys::getCurrentModifiers ().isRightButtonDown ())
            )
            {
                setNewUnnormalisedValue (
                    minParamID,
                    (float) s->getMinValue()
                );

                setNewUnnormalisedValue (
                    maxParamID,
                    (float) s->getMaxValue()
                );
            }
        }

        void sliderDragStarted (Slider*) override
        {
            beginParameterChange (minParamID);
            beginParameterChange (maxParamID);
        }

        void sliderDragEnded   (Slider*) override
        {
            endParameterChange (minParamID);
            endParameterChange (maxParamID);
        }


        String minParamID,
            maxParamID;
        Slider& slider;
        bool ignoreCallbacks;
        CriticalSection selfCallbackMutex;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
    };
    friend struct ContainerDeletePolicy<Pimpl>;
    ScopedPointer<Pimpl> pimpl;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoValueSliderAttachment)
};

I hope it will save some time for the next person to need this.


#3

cheers! my project got put on hold, but this will definitely come in handy! thanks