Dual-Mono EQ - Slider Linking

Hi everyone,

I have a dual-mono graphic EQ, and I want corresponding left and right sliders to move by the same value, at the same time, regardless of positive or negative direction, and regardless of which slider I engage. This is the code I have so far for the idea.

float linkBandGain (float currentLeftGain, float currentRightGain, float previousLeftGain, float previousRightGain, bool linkButtonON) {
float outputLeftGainOffset = 0.f;
float outputRightGainOffset = 0.f;
float finalOutput = 0.f;

if (linkButtonON == true && currentLeftGain != previousLeftGain)
{
    outputLeftGainOffset = previousRightGain - currentRightGain;
    finalOutput = outputLeftGainOffset;
}

if (linkButtonON == true && currentRightGain != previousRightGain)
{
    outputRightGainOffset = previousLeftGain - currentLeftGain;
    finalOutput = outputRightGainOffset;
}

return finalOutput; }

I am currently stuck as to how to acquire the “previous” gain, i.e. the gain at the start of the move, vs the gain at the end of the move. Waiting until the end of the move to change the opposite slider, however, will not create a smooth transition for the other, so I am stuck here in concept, too.

Thanks for your help!

Best,
Andy Bainton

If they control AudioProcessorParameters, and they’re linear (skew factor 1), and you don’t need to link automation, you could link the parameters with something like this:

class LinkedParameterPair : AudioProcessorParameter::Listener
{
public:
    void setParameters (AudioProcessorParameter* p0, AudioProcessorParameter* p1)
    {
        parameter[0] = p0;
        parameter[1] = p1;
        oldValue[0] = p0->getValue();
        oldValue[1] = p1->getValue();
        p0->addListener (this);
        p1->addListener (this);
    }

    void parameterValueChanged (int parameterIndex, float newSourceValue) override
    {
        auto target{ targetIndex (parameterIndex) }, source{ target ^ 1 };

        if (MessageManager::getInstance()->isThisTheMessageThread() && !linkingValue && linkIsOn())
        {
            auto newTargetValue{ parameter[target]->getValue() + newSourceValue - oldValue[source] };
            ScopedValueSetter svs{ linkingValue, true };
            parameter[target]->setValueNotifyingHost (newTargetValue);
        }

        oldValue[source] = newSourceValue;
    }

    void parameterGestureChanged (int parameterIndex, bool gestureIsStarting) override
    {
        if (linkingGesture) return;

        if (gestureIsStarting)
        {
            if (linkIsOn())
            {
                gestureIsActive = true;
                ScopedValueSetter svs{ linkingGesture, true };
                parameter[targetIndex (parameterIndex)]->beginChangeGesture();
            }
        }
        else if (gestureIsActive)
        {
            gestureIsActive = false;
            ScopedValueSetter svs{ linkingGesture, true };
            parameter[targetIndex (parameterIndex)]->endChangeGesture();
        }
    }

    std::function<bool()> linkIsOn;

private:
    size_t targetIndex (int parameterIndex) const
    {
        return parameterIndex == parameter[0]->getParameterIndex();
    }

    AudioProcessorParameter* parameter[2]{};
    std::atomic<float> oldValue[2]{};
    bool linkingValue{}, linkingGesture{}, gestureIsActive{};
};

If you need to link automation, it gets tricky. I ended up splitting the operation across two fifo queues (push the source change to an input fifo, link on the audio thread, push the update to an output fifo, update the target on the message thread).

1 Like

Hi, thank you for your help! Yes, they are AudioProcessorParamaters, and they do not need automation.

I really appreciate your code. I’m still working on implementing it. Here’s what my update[Band] paramaters look like.

    void MPEQAudioProcessor::updateLeftBell4k()
{
float leftBell4kGain = pow (10, *apvts.getRawParameterValue(LEFT_BELL_4K_GAIN_ID) / 20);

*leftBell4k.state = *dsp::IIR::Coefficients<float>::makePeakFilter(lastSampleRate, 4000.f, 0.707f, leftBell4kGain);
}

void MPEQAudioProcessor::updateRightBell4k()
{
float rightBell4kGain = pow (10, *apvts.getRawParameterValue(RIGHT_BELL_4K_GAIN_ID) / 20);

*rightBell4k.state = *dsp::IIR::Coefficients<float>::makePeakFilter(lastSampleRate, 4000.f, 0.707f, rightBell4kGain);
}

I would normally call a function from within these updaters, and I don’t know if this will work for what you wrote. How would I link my updater functions to what your code? What information should I send to it?

Best,
Andy Bainton

You wouldn’t call any of those functions directly. You create the LinkedParameterPair somewhere, initialised with a pair of parameters. When one parameter is updated from the UI, the other one is updated automatically. That is, some path goes from the UI event to beginChangeGesture / setValueNotifyingHost / endChangeGesture (say, through a SliderAttachment), and LinkedParameterPair catches those changes and sends them to the other parameter. It only works for linear skews because I’m taking the difference of normalised (0…1) values. linkIsOn should be assigned a lambda that returns the link status. When linking gestures, it asks if link is on at beginChange but not at endChange -there it asks if there’s an active gesture. This avoids leaving an orphan begin if link is deactivated before end (which may never happen in your case).

1 Like

Cool, thank you. In terms of instantiating the linked pair, does it matter where I do so? The UI seems like a good way to go. Would I place the code on the plugin editor .cpp? Also, given that I only want the pair to be linked when my “linked” button is on, do I have to modify what you wrote? Thanks for your patience, I am learning all this stuff.

I would place it in the processor. It belongs there logically, because it works on AudioProcessorParameters, but also there could be more than one editor instantiated, and there shouldn’t be more than one of these for each pair. Your link button should update something in the processor, either a proper parameter or just a bool somewhere. So in the AudioProcessor constructor you’d set a lambda to check that bool.

class MyAudioProcessor : AudioProcessor
{
    AudioProcessorValueTreeState state;
    LinkedParameterPair linkedPair;
    bool isLinkOn{};

    MyAudioProcessor()
    {
        linkedPair.setParameters (state.getParameter("first"), state.getParameter("second");
        linkedPair.isLinkOn = [&]() { return isLinkOn; };
    }
};

There are other ways of doing this without a lambda -this seems like the simpler most general way.

1 Like

Hey, I think I am starting to understand this. So if I set a linked parameter pair for say the slider attachments in my GUI, will that link the two sliders? I understand now that that the class you wrote does the work of linking and updating the sliders, I just need a bool to turn the link on and off, and a defined set of which couples of parameters are attached. I have a bool/button for each pair already, and I have the names of all of my slider attachments. I just need to know how to put them together. Is that what this code above is supposed to do?

Thank you for you help!
Andy Bainton

Basically. The class doesn’t link controls per se, it links AudioProcessorParameters for changes coming from the message thread. It doesn’t have a direct connection to controls or attachments, but of course anything that listens to the parameters will get updated.

1 Like

Hey again, I am implementing the second class, and I got this error, and I’m not sure why:

LinkedParameterPair shelf8kLinkedPair;
bool isLinkOn{};

shelf8kLinkedPair.setParameters(apvts.getParameter(LEFT_SHELF_8K_GAIN_ID), apvts.getParameter(RIGHT_SHELF_8K_GAIN_ID));
shelf8kLinkedPair.isLinkOn = [&]() { return isLinkOn; }; //Error: No member named 'isLinkOn' in 'LinkedParameterPair'

Do you have some ideas?

Thanks,
Andy Bainton

I had named it linkIsOn -isLinkOn sounds better though.

Thanks!