How to use CachedValue<>?


#1

here’s a quick snippet of my code for creating and adding parameters to AudioProcessorValueTreeState. I am then attempting to refer the CachedValues to the parameters, using the CachedValue<> referTo (…) method.

here’s what my struct of AudioProcessorParameters looks like (i have 6 channels of these)

struct MixerParams
{
    AudioProcessorParameter* volume;
    AudioProcessorParameter* pan;
    AudioProcessorParameter* fxA;
    AudioProcessorParameter* fxB;
    AudioProcessorParameter* timebase;
    AudioProcessorParameter* length;
    AudioProcessorParameter* drumSelector;
    
    CachedValue<float> volumeCache;
    CachedValue<float> panCache;
    CachedValue<float> fxACache;
    CachedValue<float> fxBCache;
    CachedValue<float> timebaseCache;
    CachedValue<float> lengthCache;
    CachedValue<float> drumSelectorCache;
};

and then here’s the code where i attempt (unsuccessfully, it seems) to refer the CachedValues to the AudioProcessorValueTreeState:

    String channelString = "channel" + std::to_string (channelNumber) + "_";
    String identifier;
    
    identifier = channelString + "pan";
    mixerParams[channelNumber].pan =
    mainValueTree.createAndAddParameter (identifier, identifier, identifier, knobRange, 64, nullptr, nullptr);
    mixerParams[channelNumber].panCache.referTo (mainValueTree.state, identifier, nullptr);

running this, the CachedValues are always staying at 0.0, so assume i’m doing something wrong with the connection. Any ideas?


#2

still stuck. please!


#3

You are assuming that the ValueTree layout of mainValueTree.state stores the parameter values as direct properties with their identifiers being the parameter ids. This is not the case - the layout of the tree is a bit more complicated. You could use something like the following in the constructor of your plugin:

static const Identifier gainIdentifier ("gain");
static const Identifier delayIdentifier ("delay");
static const Identifier idPropertyID ("id");
static const Identifier valuePropertyID ("value");


pluginState.createAndAddParameter ("gain", "Gain", "Gain", NormalisableRange<float> (0.0f, 1.0f),
                                   0.9f, nullptr, nullptr);
pluginState.createAndAddParameter ("delay", "Delay Feedback", "Delay Feedback",
                                   NormalisableRange<float> (0.0f, 1.0f),
                                   0.5f, nullptr, nullptr);

pluginState.state = ValueTree ("PARAMS");

ValueTree gainTree = pluginState.state.getChildWithProperty (idPropertyID, "gain");
ValueTree delayTree = pluginState.state.getChildWithProperty (idPropertyID, "delay");

gain. referTo (gainTree,  valuePropertyID, nullptr);
delay.referTo (delayTree, valuePropertyID, nullptr);

Where gain and delay are CachedValues.

I’m not sure if CachedValue is the right thing to use here. It does not give any benefits compared to mainValueTree.getRawParameterValue. Have you considered using this instead?


#4

I can’t get CachedValue to work. I create a CachedValue<float> cvtest in the header, then

MIDI_Int_AudioProcessor::MIDI_Int_AudioProcessor() : vtsParams(*this, nullptr)
{
	vtsParams.createAndAddParameter("pidMinPlayRange", "MinNotePlRange", String(),
	                                NormalisableRange<float>(1.0f, 128.0f, 1.0f), 1.0f,
	                                [] ( float value ) { return String(value); }, nullptr);
	
	vtsParams.addParameterListener("pidMinPlayRange", this);
	
	vtsParams.state = ValueTree(Identifier("VST_Parameters"));

	auto vt = vtsParams.state.getChildWithProperty( "id" , "pidMinPlayRange" );
	cvtest.referTo ( vt , "value" , nullptr );
}

But cvtest always returns 0. ValueTree vt is ok, it’s valid and has “id” and “value” properties. Have I done something wrong?


#5

CachedValue<>::referTo() takes a value tree by reference and internally adds a listener to it. In your case vt gets destroyed when vstsParams() goes out of scope. So no object to refer to.
You would need to save a reference to vt in your class to make this work.


#6

I don’t get it, Achder, vtsParams never gets out of scope inside the AudioProcessor. Besides, I log cvtest immediately after creation and the result is still 0.

So fabian’s example shouldn’t work as well

ValueTree gainTree = pluginState.state.getChildWithProperty (idPropertyID, "gain");
gain. referTo (gainTree,  valuePropertyID, nullptr);

ANyway, I tried this

cvtest.referTo(vtsParams.state.getChildWithProperty("id", "pidMinPlayRange"), "value", nullptr);

But still no go.


#7

I think that is wrong, a ValueTree is always only a wrapper around a SharedObject, so no problem when it goes out of scope, the shared object is still referenced in the tree.
Also the listeners are attached to the shared object rather than to the ValueTree leaf.

To debug I would check step by step, if the returned values you get are actually valid, like

jassert (vt.isValid());

and so on…


#8

Nope. In the documentation it says:
"[…] The listener is added to this specific ValueTree object, and not to the shared object that it refers to. When this object is deleted, all the listeners will be lost, even if other references to the same ValueTree still exist. […]"

@acabreira:
It’s not about vstParams, it’s about vt specifically. The above means, that the listener (that is internally set by cvtest) is set specifically to your instance called vt. And since getChildWithProperty() returns an object and not a reference all listeners will be lost when vt goes out of scope. Try saving vt as a member in your class.


#9

Sorry @Achder, you are right.

I got confused by the “details” section of ValueTree together with the implementation:

But it only saves a link of what to call, so it goes from the SharedObject via the local reference to the listener…


#10

CachedValue contains a ValueTree targetTree which is what the listener attaches itself to. Using a member ValueTree means you don’t have to remove yourself as a listener and it’s impossible for the source ValueTree (or the underlying shared object) to dangle.


#11

Ok, I’m officially lost. I don’t get why this doesn’t work:

//PluginProcessor.h
ValueTree vt1;
CachedValue<float> cv1;

// PluginProcessor.cpp
	vtsParams.state = ValueTree(Identifier("VST_Parameters"));
	vt1 = vtsParams.state.getChildWithProperty("id", "pidMinPlayRange");
	cv1.referTo(vt1, "value", nullptr);

Neither this:

// PluginProcessor.cpp
	vtsParams.state = ValueTree(Identifier("VST_Parameters"));
        cv1.referTo(vtsParams.state.getChildWithProperty("id", "pidMinPlayRange"), "value", nullptr);

Could you guys give me a working example?
Thanks for your patience.


#12

Whoops, of course. Makes sense.


#13

Fabian, could you please, take a look at my code above and see why it doesn’t work? It is based on your example here. Thanks.


#14

Hmmm not sure why it’s not working. I’m attaching a working version of the JuceDemoPlugin here:

PluginProcessor.cpp

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

    This file was auto-generated by the Jucer!

    It contains the basic startup code for a Juce application.

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

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

AudioProcessor* JUCE_CALLTYPE createPluginFilter();


//==============================================================================
/** A demo synth sound that's just a basic sine wave.. */
class SineWaveSound : public SynthesiserSound
{
public:
    SineWaveSound() {}

    bool appliesToNote (int /*midiNoteNumber*/) override  { return true; }
    bool appliesToChannel (int /*midiChannel*/) override  { return true; }
};

//==============================================================================
/** A simple demo synth voice that just plays a sine wave.. */
class SineWaveVoice  : public SynthesiserVoice
{
public:
    SineWaveVoice()
        : angleDelta (0.0),
          tailOff (0.0)
    {
    }

    bool canPlaySound (SynthesiserSound* sound) override
    {
        return dynamic_cast<SineWaveSound*> (sound) != nullptr;
    }

    void startNote (int midiNoteNumber, float velocity,
                    SynthesiserSound* /*sound*/,
                    int /*currentPitchWheelPosition*/) override
    {
        currentAngle = 0.0;
        level = velocity * 0.15;
        tailOff = 0.0;

        double cyclesPerSecond = MidiMessage::getMidiNoteInHertz (midiNoteNumber);
        double cyclesPerSample = cyclesPerSecond / getSampleRate();

        angleDelta = cyclesPerSample * 2.0 * double_Pi;
    }

    void stopNote (float /*velocity*/, bool allowTailOff) override
    {
        if (allowTailOff)
        {
            // start a tail-off by setting this flag. The render callback will pick up on
            // this and do a fade out, calling clearCurrentNote() when it's finished.

            if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
                                // stopNote method could be called more than once.
                tailOff = 1.0;
        }
        else
        {
            // we're being told to stop playing immediately, so reset everything..

            clearCurrentNote();
            angleDelta = 0.0;
        }
    }

    void pitchWheelMoved (int /*newValue*/) override
    {
        // can't be bothered implementing this for the demo!
    }

    void controllerMoved (int /*controllerNumber*/, int /*newValue*/) override
    {
        // not interested in controllers in this case.
    }

    void renderNextBlock (AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
    {
        processBlock (outputBuffer, startSample, numSamples);
    }

    void renderNextBlock (AudioBuffer<double>& outputBuffer, int startSample, int numSamples) override
    {
        processBlock (outputBuffer, startSample, numSamples);
    }

private:

    template <typename FloatType>
    void processBlock (AudioBuffer<FloatType>& outputBuffer, int startSample, int numSamples)
    {
        if (angleDelta != 0.0)
        {
            if (tailOff > 0)
            {
                while (--numSamples >= 0)
                {
                    const FloatType currentSample =
                        static_cast<FloatType> (std::sin (currentAngle) * level * tailOff);

                    for (int i = outputBuffer.getNumChannels(); --i >= 0;)
                        outputBuffer.addSample (i, startSample, currentSample);

                    currentAngle += angleDelta;
                    ++startSample;

                    tailOff *= 0.99;

                    if (tailOff <= 0.005)
                    {
                        clearCurrentNote();

                        angleDelta = 0.0;
                        break;
                    }
                }
            }
            else
            {
                while (--numSamples >= 0)
                {
                    const FloatType currentSample = static_cast<FloatType> (std::sin (currentAngle) * level);

                    for (int i = outputBuffer.getNumChannels(); --i >= 0;)
                        outputBuffer.addSample (i, startSample, currentSample);

                    currentAngle += angleDelta;
                    ++startSample;
                }
            }
        }
    }

    double currentAngle, angleDelta, level, tailOff;
};

//==============================================================================
JuceDemoPluginAudioProcessor::JuceDemoPluginAudioProcessor()
    : AudioProcessor (getBusesProperties()),
      lastUIWidth (400),
      lastUIHeight (200),
      pluginState (*this, nullptr),
      delayPosition (0)
{
    lastPosInfo.resetToDefault();

    // This creates our parameters. We'll keep some raw pointers to them in this class,
    // so that we can easily access them later, but the base class will take care of
    // deleting them for us.
   // addParameter (gainParam  = new AudioParameterFloat ("gain",  "Gain",           0.0f, 1.0f, 0.9f));
    //addParameter (delayParam = new AudioParameterFloat ("delay", "Delay Feedback", 0.0f, 1.0f, 0.5f));

    static const Identifier gainIdentifier ("gain");
    static const Identifier delayIdentifier ("delay");
    static const Identifier idPropertyID ("id");
    static const Identifier valuePropertyID ("value");

    pluginState.createAndAddParameter ("gain", "Gain", "Gain", NormalisableRange<float> (0.0f, 1.0f),
                                       0.9f, nullptr, nullptr);
    pluginState.createAndAddParameter ("delay", "Delay Feedback", "Delay Feedback",
                                       NormalisableRange<float> (0.0f, 1.0f),
                                       0.5f, nullptr, nullptr);

    pluginState.state = ValueTree ("PARAMS");

    ValueTree gainTree  = pluginState.state.getChildWithProperty (idPropertyID, "gain");
    ValueTree delayTree = pluginState.state.getChildWithProperty (idPropertyID, "delay");

    gain. referTo (gainTree,  valuePropertyID, nullptr);
    delay.referTo (delayTree, valuePropertyID, nullptr);

    initialiseSynth();
}

JuceDemoPluginAudioProcessor::~JuceDemoPluginAudioProcessor()
{
}

void JuceDemoPluginAudioProcessor::initialiseSynth()
{
    const int numVoices = 8;

    // Add some voices...
    for (int i = numVoices; --i >= 0;)
        synth.addVoice (new SineWaveVoice());

    // ..and give the synth a sound to play
    synth.addSound (new SineWaveSound());
}

//==============================================================================
bool JuceDemoPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    // Only mono/stereo and input/output must have same layout
    const AudioChannelSet& mainOutput = layouts.getMainOutputChannelSet();

    // input and output layout must be the same
    if (wrapperType != wrapperType_Standalone && layouts.getMainInputChannelSet() != mainOutput)
        return false;

    // do not allow disabling the main buses
    if (mainOutput.isDisabled()) return false;

    // only allow stereo and mono
    if (mainOutput.size() > 2) return false;

    return true;
}

AudioProcessor::BusesProperties JuceDemoPluginAudioProcessor::getBusesProperties()
{
    // This plug-in should not have any inputs when run as a standalone plug-in

    if (PluginHostType::getPluginLoadedAs() == wrapperType_Standalone)
        return BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true);
    else
        return BusesProperties().withInput  ("Input",  AudioChannelSet::stereo(), true)
                                .withOutput ("Output", AudioChannelSet::stereo(), true);
}

//==============================================================================
void JuceDemoPluginAudioProcessor::prepareToPlay (double newSampleRate, int /*samplesPerBlock*/)
{
    // Use this method as the place to do any pre-playback
    // initialisation that you need..
    synth.setCurrentPlaybackSampleRate (newSampleRate);
    keyboardState.reset();

    if (isUsingDoublePrecision())
    {
        delayBufferDouble.setSize (2, 12000);
        delayBufferFloat.setSize (1, 1);
    }
    else
    {
        delayBufferFloat.setSize (2, 12000);
        delayBufferDouble.setSize (1, 1);
    }

    reset();
}

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

void JuceDemoPluginAudioProcessor::reset()
{
    // Use this method as the place to clear any delay lines, buffers, etc, as it
    // means there's been a break in the audio's continuity.
    delayBufferFloat.clear();
    delayBufferDouble.clear();
}

template <typename FloatType>
void JuceDemoPluginAudioProcessor::process (AudioBuffer<FloatType>& buffer,
                                            MidiBuffer& midiMessages,
                                            AudioBuffer<FloatType>& delayBuffer)
{
    const int numSamples = buffer.getNumSamples();

    if (wrapperType == wrapperType_Standalone)
        buffer.clear();

    // Now pass any incoming midi messages to our keyboard state object, and let it
    // add messages to the buffer if the user is clicking on the on-screen keys
    keyboardState.processNextMidiBuffer (midiMessages, 0, numSamples, true);

    // and now get our synth to process these midi events and generate its output.
    synth.renderNextBlock (buffer, midiMessages, 0, numSamples);

    // Apply our delay effect to the new output..
    applyDelay (buffer, delayBuffer);

    if (wrapperType != wrapperType_Standalone)
    {
        // In case we have more outputs than inputs, we'll clear any output
        // channels that didn't contain input data, (because these aren't
        // guaranteed to be empty - they may contain garbage).
        for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
            buffer.clear (i, 0, numSamples);
    }

    applyGain (buffer, delayBuffer); // apply our gain-change to the outgoing data..

    // Now ask the host for the current time so we can store it to be displayed later...
    updateCurrentTimeInfoFromHost();
}

template <typename FloatType>
void JuceDemoPluginAudioProcessor::applyGain (AudioBuffer<FloatType>& buffer, AudioBuffer<FloatType>& delayBuffer)
{
    ignoreUnused (delayBuffer);
    const float gainLevel = gain.get();

    for (int channel = 0; channel < getTotalNumOutputChannels(); ++channel)
        buffer.applyGain (channel, 0, buffer.getNumSamples(), gainLevel);
}

template <typename FloatType>
void JuceDemoPluginAudioProcessor::applyDelay (AudioBuffer<FloatType>& buffer, AudioBuffer<FloatType>& delayBuffer)
{
    const int numSamples = buffer.getNumSamples();
    const float delayLevel = delay.get();

    int delayPos = 0;

    for (int channel = 0; channel < getTotalNumOutputChannels(); ++channel)
    {
        FloatType* const channelData = buffer.getWritePointer (channel);
        FloatType* const delayData = delayBuffer.getWritePointer (jmin (channel, delayBuffer.getNumChannels() - 1));
        delayPos = delayPosition;

        for (int i = 0; i < numSamples; ++i)
        {
            const FloatType in = channelData[i];
            channelData[i] += delayData[delayPos];
            delayData[delayPos] = (delayData[delayPos] + in) * delayLevel;

            if (++delayPos >= delayBuffer.getNumSamples())
                delayPos = 0;
        }
    }

    delayPosition = delayPos;
}

void JuceDemoPluginAudioProcessor::updateCurrentTimeInfoFromHost()
{
    if (AudioPlayHead* ph = getPlayHead())
    {
        AudioPlayHead::CurrentPositionInfo newTime;

        if (ph->getCurrentPosition (newTime))
        {
            lastPosInfo = newTime;  // Successfully got the current time from the host..
            return;
        }
    }

    // If the host fails to provide the current time, we'll just reset our copy to a default..
    lastPosInfo.resetToDefault();
}

//==============================================================================
AudioProcessorEditor* JuceDemoPluginAudioProcessor::createEditor()
{
    return new JuceDemoPluginAudioProcessorEditor (*this);
}

//==============================================================================
void JuceDemoPluginAudioProcessor::getStateInformation (MemoryBlock& destData)
{
    ScopedPointer<XmlElement> element (pluginState.state.createXml());

    if (element != nullptr)
        copyXmlToBinary (*element, destData);
}

void JuceDemoPluginAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    // You should use this method to restore your parameters from this memory block,
    // whose contents will have been created by the getStateInformation() call.

    // This getXmlFromBinary() helper function retrieves our XML from the binary blob..
    ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

    if (xmlState != nullptr)
        pluginState.state = ValueTree::fromXml (*xmlState);
}

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

PluginProcessor.h

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

    This file was auto-generated by the Jucer!

    It contains the basic startup code for a Juce application.

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

#pragma once

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


//==============================================================================
/**
    As the name suggest, this class does the actual audio processing.
*/
class JuceDemoPluginAudioProcessor  : public AudioProcessor
{
public:
    //==============================================================================
    JuceDemoPluginAudioProcessor();
    ~JuceDemoPluginAudioProcessor();

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void releaseResources() override;
    void reset() override;

    //==============================================================================
    void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (! isUsingDoublePrecision());
        process (buffer, midiMessages, delayBufferFloat);
    }

    void processBlock (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (isUsingDoublePrecision());
        process (buffer, midiMessages, delayBufferDouble);
    }

    //==============================================================================
    bool hasEditor() const override                                             { return true; }
    AudioProcessorEditor* createEditor() override;

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

    bool acceptsMidi() const override                                           { return true; }
    bool producesMidi() const override                                          { return true; }

    double getTailLengthSeconds() const override                                { return 0.0; }

    //==============================================================================
    int getNumPrograms() override                                               { return 0; }
    int getCurrentProgram() override                                            { return 0; }
    void setCurrentProgram (int /*index*/) override                             {}
    const String getProgramName (int /*index*/) override                        { return String(); }
    void changeProgramName (int /*index*/, const String& /*name*/) override     {}

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

    //==============================================================================
    // These properties are public so that our editor component can access them
    // A bit of a hacky way to do it, but it's only a demo! Obviously in your own
    // code you'll do this much more neatly..

    // this is kept up to date with the midi messages that arrive, and the UI component
    // registers with it so it can represent the incoming messages
    MidiKeyboardState keyboardState;

    // this keeps a copy of the last set of time info that was acquired during an audio
    // callback - the UI component will read this and display it.
    AudioPlayHead::CurrentPositionInfo lastPosInfo;

    // these are used to persist the UI's size - the values are stored along with the
    // filter's other parameters, and the UI component will update them when it gets
    // resized.
    int lastUIWidth, lastUIHeight;

    // Our parameters
    AudioProcessorValueTreeState pluginState;
    
    CachedValue<float> gain, delay;

private:
    //==============================================================================
    template <typename FloatType>
    void process (AudioBuffer<FloatType>& buffer, MidiBuffer& midiMessages, AudioBuffer<FloatType>& delayBuffer);
    template <typename FloatType>
    void applyGain (AudioBuffer<FloatType>&, AudioBuffer<FloatType>& delayBuffer);
    template <typename FloatType>
    void applyDelay (AudioBuffer<FloatType>&, AudioBuffer<FloatType>& delayBuffer);

    AudioBuffer<float> delayBufferFloat;
    AudioBuffer<double> delayBufferDouble;
    int delayPosition;

    Synthesiser synth;

    void initialiseSynth();
    void updateCurrentTimeInfoFromHost();
    static BusesProperties getBusesProperties();

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceDemoPluginAudioProcessor)
};

PluginEditor.h

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

    This file was auto-generated by the Jucer!

    It contains the basic startup code for a Juce application.

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

#pragma once

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


//==============================================================================
/** This is the editor component that our filter will display.
*/
class JuceDemoPluginAudioProcessorEditor  : public AudioProcessorEditor,
                                            private Timer
{
public:
    JuceDemoPluginAudioProcessorEditor (JuceDemoPluginAudioProcessor&);
    ~JuceDemoPluginAudioProcessorEditor();

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

private:
    class ParameterSlider;

    MidiKeyboardComponent midiKeyboard;
    Label timecodeDisplayLabel, gainLabel, delayLabel;
    Slider gainSlider, delaySlider;
    AudioProcessorValueTreeState::SliderAttachment gainSliderAttachment, delaySliderAttachment;

    //==============================================================================
    JuceDemoPluginAudioProcessor& getProcessor() const
    {
        return static_cast<JuceDemoPluginAudioProcessor&> (processor);
    }

    void updateTimecodeDisplay (AudioPlayHead::CurrentPositionInfo);
};

PluginEditor.cpp

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

    This file was auto-generated by the Jucer!

    It contains the basic startup code for a Juce application.

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

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

//==============================================================================
// This is a handy slider subclass that controls an AudioProcessorParameter
// (may move this class into the library itself at some point in the future..)
class JuceDemoPluginAudioProcessorEditor::ParameterSlider   : public Slider,
                                                              private Timer
{
public:
    ParameterSlider (AudioProcessorParameter& p)
        : Slider (p.getName (256)), param (p)
    {
        setRange (0.0, 1.0, 0.0);
        startTimerHz (30);
        updateSliderPos();
    }

    void valueChanged() override
    {
        if (isMouseButtonDown())
            param.setValueNotifyingHost ((float) Slider::getValue());
        else
            param.setValue ((float) Slider::getValue());
    }

    void timerCallback() override       { updateSliderPos(); }

    void startedDragging() override     { param.beginChangeGesture(); }
    void stoppedDragging() override     { param.endChangeGesture();   }

    double getValueFromText (const String& text) override   { return param.getValueForText (text); }
    String getTextFromValue (double value) override         { return param.getText ((float) value, 1024); }

    void updateSliderPos()
    {
        const float newValue = param.getValue();

        if (newValue != (float) Slider::getValue() && ! isMouseButtonDown())
            Slider::setValue (newValue);
    }

    AudioProcessorParameter& param;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ParameterSlider)
};

//==============================================================================
JuceDemoPluginAudioProcessorEditor::JuceDemoPluginAudioProcessorEditor (JuceDemoPluginAudioProcessor& owner)
    : AudioProcessorEditor (owner),
      midiKeyboard (owner.keyboardState, MidiKeyboardComponent::horizontalKeyboard),
      timecodeDisplayLabel (String()),
      gainLabel (String(), "Throughput level:"),
      delayLabel (String(), "Delay:"),
      gainSlider  (owner.pluginState.getParameter ("gain") ->name),
      delaySlider (owner.pluginState.getParameter ("delay")->name),
      gainSliderAttachment  (owner.pluginState, "gain",  gainSlider),
      delaySliderAttachment (owner.pluginState, "delay", delaySlider)
{
    // add some sliders..
    addAndMakeVisible (gainSlider);
    gainSlider.setSliderStyle (Slider::Rotary);

    addAndMakeVisible (delaySlider);
    delaySlider.setSliderStyle (Slider::Rotary);

    // add some labels for the sliders..
    gainLabel.attachToComponent (&gainSlider, false);
    gainLabel.setFont (Font (11.0f));

    delayLabel.attachToComponent (&delaySlider, false);
    delayLabel.setFont (Font (11.0f));

    // add the midi keyboard component..
    addAndMakeVisible (midiKeyboard);

    // add a label that will display the current timecode and status..
    addAndMakeVisible (timecodeDisplayLabel);
    timecodeDisplayLabel.setColour (Label::textColourId, Colours::blue);
    timecodeDisplayLabel.setFont (Font (Font::getDefaultMonospacedFontName(), 15.0f, Font::plain));

    // set resize limits for this plug-in
    setResizeLimits (400, 200, 800, 300);

    // set our component's initial size to be the last one that was stored in the filter's settings
    setSize (owner.lastUIWidth,
             owner.lastUIHeight);

    // start a timer which will keep our timecode display updated
    startTimerHz (30);
}

JuceDemoPluginAudioProcessorEditor::~JuceDemoPluginAudioProcessorEditor()
{
}

//==============================================================================
void JuceDemoPluginAudioProcessorEditor::paint (Graphics& g)
{
    g.setGradientFill (ColourGradient (Colours::white, 0, 0,
                                       Colours::lightgrey, 0, (float) getHeight(), false));
    g.fillAll();
}

void JuceDemoPluginAudioProcessorEditor::resized()
{
    // This lays out our child components...

    Rectangle<int> r (getLocalBounds().reduced (8));

    timecodeDisplayLabel.setBounds (r.removeFromTop (26));
    midiKeyboard.setBounds (r.removeFromBottom (70));

    r.removeFromTop (30);
    Rectangle<int> sliderArea (r.removeFromTop (50));
    gainSlider.setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth() / 2)));
    delaySlider.setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth())));

    getProcessor().lastUIWidth = getWidth();
    getProcessor().lastUIHeight = getHeight();
}

//==============================================================================
void JuceDemoPluginAudioProcessorEditor::timerCallback()
{
    updateTimecodeDisplay (getProcessor().lastPosInfo);
}

//==============================================================================
// quick-and-dirty function to format a timecode string
static String timeToTimecodeString (double seconds)
{
    const int millisecs = roundToInt (seconds * 1000.0);
    const int absMillisecs = std::abs (millisecs);

    return String::formatted ("%02d:%02d:%02d.%03d",
                              millisecs / 360000,
                              (absMillisecs / 60000) % 60,
                              (absMillisecs / 1000) % 60,
                              absMillisecs % 1000);
}

// quick-and-dirty function to format a bars/beats string
static String quarterNotePositionToBarsBeatsString (double quarterNotes, int numerator, int denominator)
{
    if (numerator == 0 || denominator == 0)
        return "1|1|000";

    const int quarterNotesPerBar = (numerator * 4 / denominator);
    const double beats  = (fmod (quarterNotes, quarterNotesPerBar) / quarterNotesPerBar) * numerator;

    const int bar    = ((int) quarterNotes) / quarterNotesPerBar + 1;
    const int beat   = ((int) beats) + 1;
    const int ticks  = ((int) (fmod (beats, 1.0) * 960.0 + 0.5));

    return String::formatted ("%d|%d|%03d", bar, beat, ticks);
}

// Updates the text in our position label.
void JuceDemoPluginAudioProcessorEditor::updateTimecodeDisplay (AudioPlayHead::CurrentPositionInfo pos)
{
    MemoryOutputStream displayText;

    displayText << "[" << SystemStats::getJUCEVersion() << "]   "
                << String (pos.bpm, 2) << " bpm, "
                << pos.timeSigNumerator << '/' << pos.timeSigDenominator
                << "  -  " << timeToTimecodeString (pos.timeInSeconds)
                << "  -  " << quarterNotePositionToBarsBeatsString (pos.ppqPosition,
                                                                    pos.timeSigNumerator,
                                                                    pos.timeSigDenominator);

    if (pos.isRecording)
        displayText << "  (recording)";
    else if (pos.isPlaying)
        displayText << "  (playing)";

    timecodeDisplayLabel.setText (displayText.toString(), dontSendNotification);
}