Issue with DSP processors chain and GUI

Hi all,
I am using the juce_dsp module to create a simple plugin with a simple GUI, involving processor chains. Specifically, the plugin has 2 oscillators with controls for setting frequency and amplitude. The GUI has 5 sliders (oscillator1’s frequency, oscillator1’s amplitude, oscillator2’s frequency, oscillator2’s amplitude, and a master gain). Here the screenshot.

The issue I am having is that when I control the gain of oscillator2 also the gain of oscillator1 changes accordingly. I really can’t spot where the issue is (of course no errors in variable names, I double checked it). I suspect that I am missing something at conceptual level, e.g., about how the processors relate to each other in the chain or about how the GUI relates to the processors’ parameters (also involving the value tree).

I would be immensely grateful to anybody who could explain me what I am doing wrong. I copy/paste the code here and also attach the corresponding 5 files (see the zip).

File MyCustomDspOscillator.h

    #pragma once

//==============================================================================
template <typename Type>
class MyCustomDspOscillator
{
public:
    //==============================================================================
    MyCustomDspOscillator()
    {
        auto& osc = OscillatorProcessorChain.template get<oscIndex>();
        
        int waveform = 1;
        
        if (waveform == 0){
            osc.initialise ([] (Type x)
                            {
                                return jmap (x, Type (-MathConstants<double>::pi), Type (MathConstants<double>::pi), Type (-1), Type (1));
                            }, 2);
        }else
        {
            osc.initialise ([] (Type x) { return std::sin (x); }, 128);
        }

        
        osc.setFrequency(432.0f, false);
        
        auto& gain = OscillatorProcessorChain.template get<gainIndex>();
        gain.setGainLinear(0.015); 
    }

    //==============================================================================
    void setFrequency (Type newValue, bool force = false)
    {
        auto& osc = OscillatorProcessorChain.template get<oscIndex>();
        osc.setFrequency (newValue, force);
    }

    //==============================================================================
    void setLevel (Type newValue)
    {
        auto& gain = OscillatorProcessorChain.template get<gainIndex>();
        gain.setGainLinear (newValue); 
    }

    //==============================================================================
    void reset() noexcept
    {
         OscillatorProcessorChain.reset();
    }

    //==============================================================================
    template <typename ProcessContext>
    void process (const ProcessContext& context) noexcept
    {
        OscillatorProcessorChain.process (context);
    }

    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        OscillatorProcessorChain.prepare (spec);
    }

private:
    //==============================================================================
    enum
    {
        oscIndex,
        gainIndex
    };

    juce::dsp::ProcessorChain<juce::dsp::Oscillator<Type>, juce::dsp::Gain<Type>> OscillatorProcessorChain;
};

File PuginProcessor.h

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"
#include "MyCustomDspOscillator.h"

//==============================================================================
/**
*/
class MyDspOscillatorGeneratorAudioProcessor  : public AudioProcessor
{
public:
    //==============================================================================
    MyDspOscillatorGeneratorAudioProcessor();
    ~MyDspOscillatorGeneratorAudioProcessor();

    //==============================================================================
    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void releaseResources() override;

   #ifndef JucePlugin_PreferredChannelConfigurations
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
   #endif

    void processBlock (AudioBuffer<float>&, MidiBuffer&) override;

    //==============================================================================
    AudioProcessorEditor* createEditor() override;
    bool hasEditor() const override;

    //==============================================================================
    const String getName() const override;

    bool acceptsMidi() const override;
    bool producesMidi() const override;
    bool isMidiEffect() const override;
    double getTailLengthSeconds() const override;

    //==============================================================================
    int getNumPrograms() override;
    int getCurrentProgram() override;
    void setCurrentProgram (int index) override;
    const String getProgramName (int index) override;
    void changeProgramName (int index, const String& newName) override;

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override;
    void setStateInformation (const void* data, int sizeInBytes) override;

    //==============================================================================
    void updateParameters();
    void process(dsp::ProcessContextReplacing<float> context);
    void reset() override;
    
private:
    //==============================================================================
    
    AudioProcessorValueTreeState parameters;
    
    std::atomic<float>* osc1GainParameter  = nullptr;
    std::atomic<float>* osc1FreqParameter = nullptr;
    std::atomic<float>* osc2GainParameter  = nullptr;
    std::atomic<float>* osc2FreqParameter = nullptr;
    std::atomic<float>* globalGainParameter  = nullptr;
    
    enum
    {
        osc1Index,
        osc2Index,
        masterGainIndex
    };

    juce::dsp::ProcessorChain<MyCustomDspOscillator<float>, MyCustomDspOscillator<float>, juce::dsp::Gain<float>> processorChain;
    
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyDspOscillatorGeneratorAudioProcessor)
};

File PluginProcessor.cpp

    #include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
MyDspOscillatorGeneratorAudioProcessor::MyDspOscillatorGeneratorAudioProcessor()
     : AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true)),
       parameters (*this, nullptr, Identifier ("MyDspOscillatorGenerator"),
                        {
                            std::make_unique<AudioParameterFloat> ("osc1_gain",            // parameterID
                                                                   "Osc1 Gain",            // parameter name
                                                                   0.0f,              // minimum value
                                                                   1.0f,              // maximum value
                                                                   0.5f),             // default value
                            std::make_unique<AudioParameterFloat> ("osc1_freq",            // parameterID
                                                                   "Osc1 Freq",            // parameter name
                                                                   100.0f,              // minimum value
                                                                   5000.0f,              // maximum value
                                                                   432.0f),             // default value
                            std::make_unique<AudioParameterFloat> ("osc2_gain",            // parameterID
                                                                   "Osc2 Gain",            // parameter name
                                                                   0.0f,              // minimum value
                                                                   1.0f,              // maximum value
                                                                   0.5f),             // default value
                            std::make_unique<AudioParameterFloat> ("osc2_freq",            // parameterID
                                                                   "Osc2 Freq",            // parameter name
                                                                   100.0f,              // minimum value
                                                                   5000.0f,              // maximum value
                                                                   432.0f),             // default value
                            std::make_unique<AudioParameterFloat> ("global_gain",            // parameterID
                                                                   "Global Gain",            // parameter name
                                                                   0.0f,              // minimum value
                                                                   1.0f,              // maximum value
                                                                   0.5f)             // default value
           
                        })
{
    osc1GainParameter   = parameters.getRawParameterValue ("osc1_gain");
    osc1FreqParameter   = parameters.getRawParameterValue ("osc1_freq");
    osc2GainParameter   = parameters.getRawParameterValue ("osc2_gain");
    osc2FreqParameter   = parameters.getRawParameterValue ("osc2_freq");
    globalGainParameter = parameters.getRawParameterValue ("global_gain");
}

MyDspOscillatorGeneratorAudioProcessor::~MyDspOscillatorGeneratorAudioProcessor()
{
}

//==============================================================================
const String MyDspOscillatorGeneratorAudioProcessor::getName() const
{
    return JucePlugin_Name;
}

bool MyDspOscillatorGeneratorAudioProcessor::acceptsMidi() const
{
   #if JucePlugin_WantsMidiInput
    return true;
   #else
    return false;
   #endif
}

bool MyDspOscillatorGeneratorAudioProcessor::producesMidi() const
{
   #if JucePlugin_ProducesMidiOutput
    return true;
   #else
    return false;
   #endif
}

bool MyDspOscillatorGeneratorAudioProcessor::isMidiEffect() const
{
   #if JucePlugin_IsMidiEffect
    return true;
   #else
    return false;
   #endif
}

double MyDspOscillatorGeneratorAudioProcessor::getTailLengthSeconds() const
{
    return 0.0;
}

int MyDspOscillatorGeneratorAudioProcessor::getNumPrograms()
{
    return 1;   // NB: some hosts don't cope very well if you tell them there are 0 programs,
                // so this should be at least 1, even if you're not really implementing programs.
}

int MyDspOscillatorGeneratorAudioProcessor::getCurrentProgram()
{
    return 0;
}

void MyDspOscillatorGeneratorAudioProcessor::setCurrentProgram (int index)
{
}

const String MyDspOscillatorGeneratorAudioProcessor::getProgramName (int index)
{
    return {};
}

void MyDspOscillatorGeneratorAudioProcessor::changeProgramName (int index, const String& newName)
{
}

//==============================================================================
void MyDspOscillatorGeneratorAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{

    auto channels = static_cast<uint32> (jmin (getMainBusNumInputChannels(), getMainBusNumOutputChannels()));
    dsp::ProcessSpec spec { sampleRate, static_cast<uint32> (samplesPerBlock), channels };
    processorChain.prepare (spec);
    
    updateParameters();
    reset();
}

void MyDspOscillatorGeneratorAudioProcessor::releaseResources()
{
    // When playback stops, you can use this as an opportunity to free up any
    // spare memory, etc.
}


bool MyDspOscillatorGeneratorAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    if (layouts.getMainOutputChannelSet() == AudioChannelSet::disabled())
        return false;
    
    if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
     && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
        return false;
 
    return true;
}


void MyDspOscillatorGeneratorAudioProcessor::updateParameters()
{
    //Update your parameters for your processes
    
    
    
    float osc1_freqHz = *osc1FreqParameter;
    float osc2_freqHz = *osc2FreqParameter;
    float osc1_amplitiude = *osc1GainParameter;
    float osc2_amplitiude = *osc2GainParameter;
    
    float global_amplitude = *globalGainParameter;;
    
    processorChain.get<osc1Index>().setFrequency (osc1_freqHz, true);
    processorChain.get<osc1Index>().setLevel (osc1_amplitiude);
    
    processorChain.get<osc2Index>().setFrequency (osc2_freqHz, true);
    processorChain.get<osc2Index>().setLevel (osc2_amplitiude);
    
    
    processorChain.get<masterGainIndex>().setGainLinear (global_amplitude);
    
}


void MyDspOscillatorGeneratorAudioProcessor::process(dsp::ProcessContextReplacing<float> context)
{

    processorChain.process (context);
}

void MyDspOscillatorGeneratorAudioProcessor::reset()
{
    processorChain.reset();
}





void MyDspOscillatorGeneratorAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());
    
    updateParameters();
    
    dsp::AudioBlock<float> block (buffer);
    process(dsp::ProcessContextReplacing<float> (block));
    
}

//==============================================================================
bool MyDspOscillatorGeneratorAudioProcessor::hasEditor() const
{
    return true; // (change this to false if you choose to not supply an editor)
}

AudioProcessorEditor* MyDspOscillatorGeneratorAudioProcessor::createEditor()
{
    return new MyDspOscillatorGeneratorAudioProcessorEditor (*this, parameters);
}

//==============================================================================
void MyDspOscillatorGeneratorAudioProcessor::getStateInformation (MemoryBlock& destData)
{
    auto state = parameters.copyState();
    std::unique_ptr<XmlElement> xml (state.createXml());
    copyXmlToBinary (*xml, destData);
}

void MyDspOscillatorGeneratorAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    std::unique_ptr<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

    if (xmlState.get() != nullptr)
        if (xmlState->hasTagName (parameters.state.getType()))
            parameters.replaceState (ValueTree::fromXml (*xmlState));
}

//==============================================================================
// This creates new instances of the plugin..
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new MyDspOscillatorGeneratorAudioProcessor();
}

File PluginEditor.h

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"

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

class MyDspOscillatorGeneratorAudioProcessorEditor  : public AudioProcessorEditor
{
public:
    
    enum
    {
        paramControlHeight = 40,
        paramLabelWidth    = 80,
        paramSliderWidth   = 350
    };

    typedef AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
    
    
    MyDspOscillatorGeneratorAudioProcessorEditor (MyDspOscillatorGeneratorAudioProcessor& parent, AudioProcessorValueTreeState& vts);
    ~MyDspOscillatorGeneratorAudioProcessorEditor();

    //==============================================================================
    void paint (Graphics&) override;
    void resized() override;

private:
    AudioProcessorValueTreeState& valueTreeState;

    Label osc1GainLabel;
    Label osc1FreqLabel;
    Slider osc1GainSlider;
    Slider osc1FreqSlider;

    Label osc2GainLabel;
    Label osc2FreqLabel;
    Slider osc2GainSlider;
    Slider osc2FreqSlider;
    
    Label globalGainLabel;
    Slider globalGainSlider;

    std::unique_ptr<SliderAttachment> osc1GainAttachment;
    std::unique_ptr<SliderAttachment> osc1FreqAttachment;
    
    std::unique_ptr<SliderAttachment> osc2GainAttachment;
    std::unique_ptr<SliderAttachment> osc2FreqAttachment;
    
    std::unique_ptr<SliderAttachment> globalGainAttachment;
    

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyDspOscillatorGeneratorAudioProcessorEditor)
};

File PluginEditor.cpp

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
MyDspOscillatorGeneratorAudioProcessorEditor::MyDspOscillatorGeneratorAudioProcessorEditor (MyDspOscillatorGeneratorAudioProcessor& p, AudioProcessorValueTreeState& vts)
    : AudioProcessorEditor (&p), valueTreeState (vts)
{
    osc1GainLabel.setText ("Gain osc1", dontSendNotification);
    osc1GainLabel.attachToComponent (&osc1GainSlider, true);
    addAndMakeVisible (osc1GainLabel);
    
    osc1FreqLabel.setText ("Freq osc1", dontSendNotification);
    osc1FreqLabel.attachToComponent (&osc1FreqSlider, true);
    addAndMakeVisible (osc1FreqLabel);
    
    getLookAndFeel().setColour (Slider::thumbColourId, Colours::red);
    addAndMakeVisible (osc1GainSlider);
    
    osc1FreqSlider.setTextValueSuffix (" Hz");
    addAndMakeVisible (osc1FreqSlider);
    
    osc1GainAttachment.reset (new SliderAttachment (valueTreeState, "osc1_gain", osc1GainSlider));
    osc1FreqAttachment.reset (new SliderAttachment (valueTreeState, "osc1_freq", osc1FreqSlider));

    
    
    osc2GainLabel.setText ("Gain osc2", dontSendNotification);
    osc2GainLabel.attachToComponent (&osc2GainSlider, true);
    addAndMakeVisible (osc2GainLabel);
    
    osc2FreqLabel.setText ("Freq osc2", dontSendNotification);
    osc2FreqLabel.attachToComponent (&osc2FreqSlider, true);
    addAndMakeVisible (osc2FreqLabel);
    
    getLookAndFeel().setColour (Slider::thumbColourId, Colours::red);
    addAndMakeVisible (osc2GainSlider);
    
    osc2FreqSlider.setTextValueSuffix (" Hz");
    addAndMakeVisible (osc2FreqSlider);
    
    osc2GainAttachment.reset (new SliderAttachment (valueTreeState, "osc2_gain", osc2GainSlider));
    osc2FreqAttachment.reset (new SliderAttachment (valueTreeState, "osc2_freq", osc2FreqSlider));
    
    
    
    globalGainLabel.setText ("Global Gain", dontSendNotification);
    globalGainLabel.attachToComponent (&globalGainSlider, true);
    addAndMakeVisible (globalGainLabel);
    
    getLookAndFeel().setColour (Slider::thumbColourId, Colours::red);
    addAndMakeVisible (globalGainSlider);
    
    globalGainAttachment.reset (new SliderAttachment (valueTreeState, "global_gain", globalGainSlider));
    
    
    
    setSize (paramSliderWidth + paramLabelWidth + 50, jmax (500, paramControlHeight * 2 + 20));

}

MyDspOscillatorGeneratorAudioProcessorEditor::~MyDspOscillatorGeneratorAudioProcessorEditor()
{
}

//==============================================================================
void MyDspOscillatorGeneratorAudioProcessorEditor::paint (Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
}

void MyDspOscillatorGeneratorAudioProcessorEditor::resized()
{
    auto sliderLeft = paramLabelWidth + 10;
    osc1GainSlider.setBounds (sliderLeft, 20, getWidth() - sliderLeft - 10, 20);
    osc1FreqSlider.setBounds (sliderLeft, 50, getWidth() - sliderLeft - 10, 20);
    
    osc2GainSlider.setBounds (sliderLeft, 80, getWidth() - sliderLeft - 10, 20);
    osc2FreqSlider.setBounds (sliderLeft, 110, getWidth() - sliderLeft - 10, 20);
    
    globalGainSlider.setBounds (sliderLeft, 140, getWidth() - sliderLeft - 10, 20);
}

Source.zip (7.7 KB)

Works fine for me here, although I had to change

std::atomic<float>* osc1GainParameter  = nullptr;

to float* as I was getting a compiler error. Not sure why you think you need to use std::atomic here, but I’m no expert.

The API in develop has changed to return a std::atomic<float>* instead of the previously used float*. Because it is unknown, which thread accesses the parameter, it needs to be wrapped in atomic.
You will have to change it back, once you update JUCE.

Hi all,
thanks for your replies. Rory, I have doubts in believing that it works for you. Indeed, my understanding is that the processor chain ultimately implements something like this (OSC1 * gain1 + OSC2) * gain2 * mastergain.

Therefore, to make it work as I want I created 2 buffers, one for each oscillator-gain pair. I attach the code for those interested. Here the relevant part from the file MyDSPOscillatorGenerator.h

    //==============================================================================
    template <typename ProcessContext>
    void process (const ProcessContext& context) noexcept
    {
        
        auto&& outBlock = context.getOutputBlock();
        auto blockToUse = tempBlock.getSubBlock (0, outBlock.getNumSamples());
        juce::dsp::ProcessContextReplacing<float> tempContext (blockToUse);
        OscillatorProcessorChain.process (tempContext);
        outBlock.copyFrom (context.getInputBlock()).add (blockToUse);
    }
    
    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        tempBlock = juce::dsp::AudioBlock<float> (heapBlock, spec.numChannels, spec.maximumBlockSize);
        OscillatorProcessorChain.prepare (spec);
    }
    
    
private:
    //==============================================================================
    juce::HeapBlock<char> heapBlock;
    juce::dsp::AudioBlock<float> tempBlock;
    
    enum
    {
        oscIndex,
        gainIndex
    };

    juce::dsp::ProcessorChain<juce::dsp::Oscillator<Type>, juce::dsp::Gain<Type>> OscillatorProcessorChain;
};

The question now is whether this approach is the most efficient one.

Since these are my first steps in the dsp module, any suggestion for improvement would be much appreciated (even stylistic suggestions).
Source.zip (8.3 KB)

To clarify, I only checked that the sliders can move independently, and as Daniel pointed out, I’m on the master branch so I didn’t be to use std::atomic. I can try with the dev branch later but it seems you already got it working now?

P.s. thanks Daniel for pointing out why atomics are needed here :+1:

Did you ever notice this little typo? (extra “i” in amplitude) might go unnoticed as this is just a declaration. Hope you are getting it all worked out