Hi everyone!
I’m working on a plugin with separated L/R controls and a Link toggle. I researched how other products handle this and I ended up with the following Host-Client design. I wanted to share my implementation and see if I missed any hidden (or trivial) detail, especially regarding DAW automations, threading and JUCE’s attachment system.
Desired behaviour
- Unlinked
sliderLcontrolsparamLandsliderRcontrolsparamR.- Automations are independent.
- Linked
paramLis used by both channels in the processor/DSP.paramRis ignored.- Both sliders stay in sync. Moving either knobs updates the other and updates
paramL. - Automating
paramLmoves both knobs, automatingparamRhas no visual effect.
- State change
- Unlinked to linked →
sliderRsnaps to current value ofsliderL - Linked to unlinked →
sliderRsnaps toparamR’s value.
- Unlinked to linked →
Strategy
Manage the lifecycle of attachmentR (that links sliderR to paramR) according to the link toggle state. This to avoid slider-update feedback loops and conflicting automations.
Code
// Editor.h
juce::Slider m_sliderL, m_sliderR;
std::unique_ptr <juce::AudioProcessorValueTreeState::SliderAttachment> m_attachmentL, m_attachmentR;
juce::ToggleButton m_linkToggle;
void updateLinkedSliders(juce::Slider& host, juce::Slider& client, bool sendNotification)
{
if(m_linkToggle.getToggleState())
{
client.setValue(host.getValue(), sendNotification ? juce::sendNotification : juce::dontSendNotification);
}
}
// In Editor's constructor
m_attachmentL = std::make_unique <juce::AudioProcessorValueTreeState::SliderAttachment>(m_audioProcessor.m_apvts, "ParamL", m_sliderL);
m_attachmentR = std::make_unique <juce::AudioProcessorValueTreeState::SliderAttachment>(m_audioProcessor.m_apvts, "ParamR", m_sliderR);
m_sliderL.onValueChange = [this] { updateLinkedSliders(m_sliderL, m_sliderR, false); };
m_sliderR.onValueChange = [this] { updateLinkedSliders(m_sliderR, m_sliderL, true); };
m_linkToggle.onStateChange = [this]
{
if(m_linkToggle.getToggleState())
{
m_attachmentR.reset();
updateLinkedSliders(m_sliderL, m_sliderR, false);
}
else
{
m_attachmentR = std::make_unique <juce::AudioProcessorValueTreeState::SliderAttachment>(m_audioProcessor.m_apvts, "ParamR", m_sliderR);
}
};
Logic
- When link is active,
m_attachmentRis destroyed. This prevents the DAW from trying to movem_sliderRwhile it’s supposed to be followingm_sliderL. - Notification when linked:
- L → R: No notification is sent when updating
m_sliderR’s value.ParamRis ignored and them_attachmentRis destroyed anyway.- This allows to break the infinite
onValueChangeping-pong loop between the 2 sliders.
- R → L: Notification is sent when updating the
m_sliderL’s value.- This allows to update
ParamLregardless of which knob was dragged.
- This allows to update
- L → R: No notification is sent when updating
- Automation: Since
m_attachmentLstays active, DAW automation onParamLtriggersm_sliderL.onValueChange, which in turn updatesm_sliderRvisually.
Known side effects
The only downside I found is that m_sliderR is set twice (with the same value) when the user drags it (once by the mouse, once by the L→ R callback). However, while testing, this proved to be robust and I didn’t notice any lag/jitter or odd behaviour.
I’m not entirely sure about creating and destroying m_attachmentR each time the toggle changes state, but there seem to be no other way of dynamically linking/unlinking a slider from an attachment.
Is there a better approach for dealing with this? Am I missing some issue that this would cause?
Any other feedback?
Thanks!
