Parameter automation, AudioProcessorValueTreeState, and Live


#1

This is probably something silly; most of my mistakes are.

I ran into some surprise difficulty with Live 10 and parameter automation.

I can find the parameters, draw in automation, but then once I press play, that nasty orange box with the arrow (the Re-Enable Automation Button) tells me that something has changed. It does not, of course, tell me what exactly.

The code below replicates this problem in Live, though it works just fine in Reaper and Logic. My guess is that it has something to do with a (mis)use of AudioProcessorValueTreeState?

(Updated: One line in the editor.cpp should not have been there. Should not make a difference to those testing with this code.)

/*============Test PluginProcessor.h============*/
#pragma once
#include "../JuceLibraryCode/JuceHeader.h"

class TestAudioProcessor  : public AudioProcessor
{
public:
    TestAudioProcessor();
    ~TestAudioProcessor();

    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;
    
    AudioProcessorValueTreeState* getParamValueTree() {return &tree;};
    
private:
    juce::dsp::Gain<float> gain;
    AudioProcessorValueTreeState tree;
          JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestAudioProcessor)
};

/*============Test PluginProcessor.cpp============*/

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

TestAudioProcessor::TestAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", AudioChannelSet::stereo(), true)
                     #endif
                       )
#endif
, tree(*this, nullptr)
{
    String pGain = "Gain";
    tree.createAndAddParameter (pGain, "GainName", TRANS ("TransGain"),
                                  NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5, false), 0.5f,
                                  [](float value) {return String(value * 100.f, 1);},
                                  [](const String& text) {return text.getFloatValue() / 100.0f;},
                                  false, true, false,
                                  AudioProcessorParameter::genericParameter);
    
    tree.state = ValueTree(String("ValueTreeState"));
}

TestAudioProcessor::~TestAudioProcessor(){}

const String TestAudioProcessor::getName() const
{
    return JucePlugin_Name;
}

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

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

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

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

int TestAudioProcessor::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 TestAudioProcessor::getCurrentProgram()
{
    return 0;
}

void TestAudioProcessor::setCurrentProgram (int index)
{
}

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

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

void TestAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    juce::dsp::ProcessSpec spec = { sampleRate,
                                    static_cast<uint32>(samplesPerBlock),
                                    static_cast<uint32>(this->getTotalNumOutputChannels()) };
    
    gain.prepare(spec);
}

void TestAudioProcessor::releaseResources()
{
    gain.reset();
}

#ifndef JucePlugin_PreferredChannelConfigurations
bool TestAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
  #if JucePlugin_IsMidiEffect
    ignoreUnused (layouts);
    return true;
  #else
    // This is the place where you check if the layout is supported.
    // In this template code we only support mono or stereo.
    if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
     && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
        return false;

    // This checks if the input layout matches the output layout
   #if ! JucePlugin_IsSynth
    if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
        return false;
   #endif

    return true;
  #endif
}
#endif

void TestAudioProcessor::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());
    
    gain.setGainLinear(*tree.getRawParameterValue("Gain"));
    
    dsp::AudioBlock<float> block (buffer);
    const juce::dsp::ProcessContextReplacing<float>& context = block;
    gain.process(context);
}

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

AudioProcessorEditor* TestAudioProcessor::createEditor()
{
    return new TestAudioProcessorEditor (*this);
}

void TestAudioProcessor::getStateInformation (MemoryBlock& destData)
{
    auto state = tree.copyState();
    ScopedPointer<XmlElement> xml (state.createXml());
    copyXmlToBinary (*xml, destData);
}

void TestAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
    
    if (xmlState != nullptr) {
        if (xmlState->hasTagName (tree.state.getType())) {
            tree.replaceState (ValueTree::fromXml (*xmlState));
        }
    }
}

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

/*============Test PluginEditor.h============*/

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

class TestAudioProcessorEditor  : public AudioProcessorEditor, public Slider::Listener
{
public:
    TestAudioProcessorEditor (TestAudioProcessor&);
    ~TestAudioProcessorEditor();

    void paint (Graphics&) override;
    void resized() override;
    void sliderValueChanged (Slider* slider) override;
    
private:
    TestAudioProcessor& processor;
    std::unique_ptr<juce::Label>  lblGain;
    std::unique_ptr<juce::Slider> sldGain;
    std::unique_ptr<AudioProcessorValueTreeState::SliderAttachment> sldAtt;
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestAudioProcessorEditor)
};

/*============Test PluginEditor.cpp============*/
#include "PluginProcessor.h"
#include "PluginEditor.h"

TestAudioProcessorEditor::TestAudioProcessorEditor (TestAudioProcessor& p)
    : AudioProcessorEditor (p), processor (p),
      lblGain (new juce::Label ("lblGain", TRANS("Gain"))),
      sldGain (new juce::Slider ("sldGain")),
      sldAtt(new AudioProcessorValueTreeState::SliderAttachment (*p.getParamValueTree(), "Gain", *sldGain))
{
    addAndMakeVisible (*lblGain);
    lblGain->setEditable (false, false, false);
    lblGain->setFont (Font (15.00f, Font::plain));
    lblGain->setJustificationType (Justification::centredLeft);
    lblGain->setColour (Label::textColourId, Colours::lightblue);
    
    addAndMakeVisible(*sldGain);
    sldGain->setRange (0., 1., 0.0001);
    sldGain->setSliderStyle (Slider::RotaryVerticalDrag);
    sldGain->setTextBoxStyle (Slider::NoTextBox, false, 80, 20);
    sldGain->addListener(this);
    
    setSize (400, 300);
}

TestAudioProcessorEditor::~TestAudioProcessorEditor (){}

void TestAudioProcessorEditor::paint (Graphics& g)
{
    g.fillAll (Colours::blue);
    g.setColour (Colours::red);
    g.setFont (15.0f);
    g.drawFittedText ("- - -", getLocalBounds(), Justification::centredBottom, 1);
}

void TestAudioProcessorEditor::resized()
{
    lblGain->setBounds (29, 0, 149, 124);
    sldGain->setBounds (22, 117, 149, 149);
}

void TestAudioProcessorEditor::sliderValueChanged (Slider* slider)
{
    if (slider == sldGain.get()) {
        // do something here, if you must
    }
}

MaxMsp/Gen Param -> Slider linking
#2

I am interested as well, since it seems to happen for my open source equalizer as well. Unfortunately I don’t have Live to test myself, just a user report…


#3

hmmm… interesting, I’ve just tested my code and this is happening also - didn’t use to be the case. Need to check whether I’ve done something to break this or not - will report back once I’ve investigated more.

Edit: I’m not using APT, just adding parameters manually.


#4

So I’ve just built the demo plugin and tried it on Live and it behaves the same - looks like automation is broken in Live. Tested on 9.7.5 and 10.0.2.


#5

Thanks everybody for digging in @AonesisAudio and @leehu

Are there plugins, that still work with automation in that live?
Is it the Live <-> Juce combination or Automation in general?


#6

Other plugins are fine in Live, so seems to be a JUCE/Live thing.

That button is lit when Live receives an update for a parameter that is automated (e.g. from the user moving a dial, or a control surface) so it looks like there’s potentially a feedback loop that when Live updates a parameter in JUCE it gets bounced back to Live causing it to light the button.

This is just a guess but I remember I had this feedback loop in the past in my code and it exhibited the same behaviour.


#7

That points down to the wrapper then :frowning:
Can it be isolated to AU or VST? I think Live can do both, right?

I am happy to look into the sources as well, just need to know which wrapper to look at…


#8

Having trouble building the AU using the new PIP for some reason but will update once I have


#9

Is this related?


#10

checked the AU - same issue


VST3 Wrapper ping-pong bug / Cubase processParameterChanges
#11

yes, i think it’s probably the same issue - @t0m says there should be a fix soon

EDIT: Not the same issue after all.


#12

Is the issue present in JUCE 5.2?


#13

I compiled both Daniel’s plug as well as what I posted a few days ago against Juce dev commit 88e76ff71.


#14

No, just pulled 5.2.0 and automation behaves as expected in Live.


#15

5.2.1 is fine too


#16

Are both AU and VST2 affected?


#17

yes.

I’ve just installed a plugin that I built yesterday before pulling the 5.3.0 commit on develop - probably a 5.2.1 develop from a week or so ago and the problem exists there too, so this problem is somewhere between them.

I’m wondering if this got broken a few weeks ago when all those parameter changes were made?


#18

broken by commit: https://github.com/WeAreROLI/JUCE/commit/107ba1fd69522346cac29866426c367b2ef45124


#20

Any updates on this?

Thank you


#21

Hi @t0m - any eta on this or are we better heading back to 5.2.1 for now? thx