1181 lines
44 KiB
C++
1181 lines
44 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2017 - ROLI Ltd.
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
|
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
|
27th April 2017).
|
|
|
|
End User License Agreement: www.juce.com/juce-5-licence
|
|
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
//==============================================================================
|
|
AudioProcessorValueTreeState::Parameter::Parameter (const String& parameterID,
|
|
const String& parameterName,
|
|
const String& labelText,
|
|
NormalisableRange<float> valueRange,
|
|
float defaultValue,
|
|
std::function<String (float)> valueToTextFunction,
|
|
std::function<float (const String&)> textToValueFunction,
|
|
bool isMetaParameter,
|
|
bool isAutomatableParameter,
|
|
bool isDiscrete,
|
|
AudioProcessorParameter::Category category,
|
|
bool isBoolean)
|
|
: AudioParameterFloat (parameterID,
|
|
parameterName,
|
|
valueRange,
|
|
defaultValue,
|
|
labelText,
|
|
category,
|
|
valueToTextFunction == nullptr ? std::function<String (float v, int)>()
|
|
: [valueToTextFunction](float v, int) { return valueToTextFunction (v); },
|
|
std::move (textToValueFunction)),
|
|
unsnappedDefault (valueRange.convertTo0to1 (defaultValue)),
|
|
metaParameter (isMetaParameter),
|
|
automatable (isAutomatableParameter),
|
|
discrete (isDiscrete),
|
|
boolean (isBoolean)
|
|
{
|
|
}
|
|
|
|
float AudioProcessorValueTreeState::Parameter::getDefaultValue() const { return unsnappedDefault; }
|
|
int AudioProcessorValueTreeState::Parameter::getNumSteps() const { return RangedAudioParameter::getNumSteps(); }
|
|
|
|
bool AudioProcessorValueTreeState::Parameter::isMetaParameter() const { return metaParameter; }
|
|
bool AudioProcessorValueTreeState::Parameter::isAutomatable() const { return automatable; }
|
|
bool AudioProcessorValueTreeState::Parameter::isDiscrete() const { return discrete; }
|
|
bool AudioProcessorValueTreeState::Parameter::isBoolean() const { return boolean; }
|
|
|
|
//==============================================================================
|
|
class AudioProcessorValueTreeState::ParameterAdapter : private AudioProcessorParameter::Listener
|
|
{
|
|
private:
|
|
using Listener = AudioProcessorValueTreeState::Listener;
|
|
|
|
public:
|
|
explicit ParameterAdapter (RangedAudioParameter& parameterIn)
|
|
: parameter (parameterIn),
|
|
// For legacy reasons, the unnormalised value should *not* be snapped on construction
|
|
unnormalisedValue (getRange().convertFrom0to1 (parameter.getDefaultValue()))
|
|
{
|
|
parameter.addListener (this);
|
|
}
|
|
|
|
~ParameterAdapter() override { parameter.removeListener (this); }
|
|
|
|
void addListener (Listener* l) { listeners.add (l); }
|
|
void removeListener (Listener* l) { listeners.remove (l); }
|
|
|
|
RangedAudioParameter& getParameter() { return parameter; }
|
|
const RangedAudioParameter& getParameter() const { return parameter; }
|
|
|
|
const NormalisableRange<float>& getRange() const { return parameter.getNormalisableRange(); }
|
|
|
|
float getDenormalisedDefaultValue() const { return denormalise (parameter.getDefaultValue()); }
|
|
|
|
void setDenormalisedValue (float value)
|
|
{
|
|
if (value == unnormalisedValue)
|
|
return;
|
|
|
|
setNormalisedValue (normalise (value));
|
|
}
|
|
|
|
float getDenormalisedValueForText (const String& text) const
|
|
{
|
|
return denormalise (parameter.getValueForText (text));
|
|
}
|
|
|
|
String getTextForDenormalisedValue (float value) const
|
|
{
|
|
return parameter.getText (normalise (value), 0);
|
|
}
|
|
|
|
float getDenormalisedValue() const { return unnormalisedValue; }
|
|
float& getRawDenormalisedValue() { return unnormalisedValue; }
|
|
|
|
bool flushToTree (const Identifier& key, UndoManager* um)
|
|
{
|
|
auto needsUpdateTestValue = true;
|
|
|
|
if (! needsUpdate.compare_exchange_strong (needsUpdateTestValue, false))
|
|
return false;
|
|
|
|
if (auto valueProperty = tree.getPropertyPointer (key))
|
|
{
|
|
if ((float) *valueProperty != unnormalisedValue)
|
|
{
|
|
ScopedValueSetter<bool> svs (ignoreParameterChangedCallbacks, true);
|
|
tree.setProperty (key, unnormalisedValue, um);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tree.setProperty (key, unnormalisedValue, nullptr);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ValueTree tree;
|
|
|
|
private:
|
|
void parameterGestureChanged (int, bool) override {}
|
|
|
|
void parameterValueChanged (int, float) override
|
|
{
|
|
const auto newValue = denormalise (parameter.getValue());
|
|
|
|
if (unnormalisedValue == newValue && ! listenersNeedCalling)
|
|
return;
|
|
|
|
unnormalisedValue = newValue;
|
|
listeners.call ([=](Listener& l) { l.parameterChanged (parameter.paramID, unnormalisedValue); });
|
|
listenersNeedCalling = false;
|
|
needsUpdate = true;
|
|
}
|
|
|
|
float denormalise (float normalised) const
|
|
{
|
|
return getParameter().convertFrom0to1 (normalised);
|
|
}
|
|
|
|
float normalise (float denormalised) const
|
|
{
|
|
return getParameter().convertTo0to1 (denormalised);
|
|
}
|
|
|
|
void setNormalisedValue (float value)
|
|
{
|
|
if (ignoreParameterChangedCallbacks)
|
|
return;
|
|
|
|
parameter.setValueNotifyingHost (value);
|
|
}
|
|
|
|
RangedAudioParameter& parameter;
|
|
ListenerList<Listener> listeners;
|
|
float unnormalisedValue{};
|
|
std::atomic<bool> needsUpdate { true };
|
|
bool listenersNeedCalling { true }, ignoreParameterChangedCallbacks { false };
|
|
};
|
|
|
|
//==============================================================================
|
|
AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& processorToConnectTo,
|
|
UndoManager* undoManagerToUse,
|
|
const Identifier& valueTreeType,
|
|
ParameterLayout parameterLayout)
|
|
: AudioProcessorValueTreeState (processorToConnectTo, undoManagerToUse)
|
|
{
|
|
struct PushBackVisitor : ParameterLayout::Visitor
|
|
{
|
|
explicit PushBackVisitor (AudioProcessorValueTreeState& stateIn)
|
|
: state (&stateIn) {}
|
|
|
|
void visit (std::unique_ptr<RangedAudioParameter> param) const override
|
|
{
|
|
if (param == nullptr)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
state->addParameterAdapter (*param);
|
|
state->processor.addParameter (param.release());
|
|
}
|
|
|
|
void visit (std::unique_ptr<AudioProcessorParameterGroup> group) const override
|
|
{
|
|
if (group == nullptr)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
for (const auto param : group->getParameters (true))
|
|
{
|
|
if (const auto rangedParam = dynamic_cast<RangedAudioParameter*> (param))
|
|
{
|
|
state->addParameterAdapter (*rangedParam);
|
|
}
|
|
else
|
|
{
|
|
// If you hit this assertion then you are attempting to add a parameter that is
|
|
// not derived from RangedAudioParameter to the AudioProcessorValueTreeState.
|
|
jassertfalse;
|
|
}
|
|
}
|
|
|
|
state->processor.addParameterGroup (move (group));
|
|
}
|
|
|
|
AudioProcessorValueTreeState* state;
|
|
};
|
|
|
|
for (auto& item : parameterLayout.parameters)
|
|
item->accept (PushBackVisitor (*this));
|
|
|
|
state = ValueTree (valueTreeType);
|
|
}
|
|
|
|
AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& p, UndoManager* um)
|
|
: processor (p), undoManager (um)
|
|
{
|
|
startTimerHz (10);
|
|
state.addListener (this);
|
|
}
|
|
|
|
AudioProcessorValueTreeState::~AudioProcessorValueTreeState() {}
|
|
|
|
//==============================================================================
|
|
RangedAudioParameter* AudioProcessorValueTreeState::createAndAddParameter (const String& paramID,
|
|
const String& paramName,
|
|
const String& labelText,
|
|
NormalisableRange<float> range,
|
|
float defaultVal,
|
|
std::function<String (float)> valueToTextFunction,
|
|
std::function<float (const String&)> textToValueFunction,
|
|
bool isMetaParameter,
|
|
bool isAutomatableParameter,
|
|
bool isDiscreteParameter,
|
|
AudioProcessorParameter::Category category,
|
|
bool isBooleanParameter)
|
|
{
|
|
return createAndAddParameter (std::make_unique<Parameter> (paramID,
|
|
paramName,
|
|
labelText,
|
|
range,
|
|
defaultVal,
|
|
std::move (valueToTextFunction),
|
|
std::move (textToValueFunction),
|
|
isMetaParameter,
|
|
isAutomatableParameter,
|
|
isDiscreteParameter,
|
|
category,
|
|
isBooleanParameter));
|
|
}
|
|
|
|
RangedAudioParameter* AudioProcessorValueTreeState::createAndAddParameter (std::unique_ptr<RangedAudioParameter> param)
|
|
{
|
|
// All parameters must be created before giving this manager a ValueTree state!
|
|
jassert (! state.isValid());
|
|
|
|
if (getParameter (param->paramID) != nullptr)
|
|
return nullptr;
|
|
|
|
addParameterAdapter (*param);
|
|
|
|
processor.addParameter (param.get());
|
|
|
|
return param.release();
|
|
}
|
|
|
|
//==============================================================================
|
|
void AudioProcessorValueTreeState::addParameterAdapter (RangedAudioParameter& param)
|
|
{
|
|
adapterTable.emplace (param.paramID, std::make_unique<ParameterAdapter> (param));
|
|
}
|
|
|
|
AudioProcessorValueTreeState::ParameterAdapter* AudioProcessorValueTreeState::getParameterAdapter (StringRef paramID) const
|
|
{
|
|
auto it = adapterTable.find (paramID);
|
|
return it == adapterTable.end() ? nullptr : it->second.get();
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::addParameterListener (StringRef paramID, Listener* listener)
|
|
{
|
|
if (auto* p = getParameterAdapter (paramID))
|
|
p->addListener (listener);
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::removeParameterListener (StringRef paramID, Listener* listener)
|
|
{
|
|
if (auto* p = getParameterAdapter (paramID))
|
|
p->removeListener (listener);
|
|
}
|
|
|
|
Value AudioProcessorValueTreeState::getParameterAsValue (StringRef paramID) const
|
|
{
|
|
if (auto* adapter = getParameterAdapter (paramID))
|
|
if (adapter->tree.isValid())
|
|
return adapter->tree.getPropertyAsValue (valuePropertyID, undoManager);
|
|
|
|
return {};
|
|
}
|
|
|
|
NormalisableRange<float> AudioProcessorValueTreeState::getParameterRange (StringRef paramID) const noexcept
|
|
{
|
|
if (auto* p = getParameterAdapter (paramID))
|
|
return p->getRange();
|
|
|
|
return {};
|
|
}
|
|
|
|
RangedAudioParameter* AudioProcessorValueTreeState::getParameter (StringRef paramID) const noexcept
|
|
{
|
|
if (auto adapter = getParameterAdapter (paramID))
|
|
return &adapter->getParameter();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
float* AudioProcessorValueTreeState::getRawParameterValue (StringRef paramID) const noexcept
|
|
{
|
|
if (auto* p = getParameterAdapter (paramID))
|
|
return &p->getRawDenormalisedValue();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
ValueTree AudioProcessorValueTreeState::copyState()
|
|
{
|
|
ScopedLock lock (valueTreeChanging);
|
|
flushParameterValuesToValueTree();
|
|
return state.createCopy();
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::replaceState (const ValueTree& newState)
|
|
{
|
|
ScopedLock lock (valueTreeChanging);
|
|
|
|
state = newState;
|
|
|
|
if (undoManager != nullptr)
|
|
undoManager->clearUndoHistory();
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::setNewState (ValueTree vt)
|
|
{
|
|
jassert (vt.getParent() == state);
|
|
|
|
if (auto* p = getParameterAdapter (vt.getProperty (idPropertyID).toString()))
|
|
{
|
|
p->tree = vt;
|
|
p->setDenormalisedValue (p->tree.getProperty (valuePropertyID, p->getDenormalisedDefaultValue()));
|
|
}
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::updateParameterConnectionsToChildTrees()
|
|
{
|
|
ScopedLock lock (valueTreeChanging);
|
|
|
|
for (auto& p : adapterTable)
|
|
p.second->tree = ValueTree();
|
|
|
|
for (const auto& child : state)
|
|
setNewState (child);
|
|
|
|
for (auto& p : adapterTable)
|
|
{
|
|
auto& adapter = *p.second;
|
|
|
|
if (! adapter.tree.isValid())
|
|
{
|
|
adapter.tree = ValueTree (valueType);
|
|
adapter.tree.setProperty (idPropertyID, adapter.getParameter().paramID, nullptr);
|
|
state.appendChild (adapter.tree, nullptr);
|
|
}
|
|
}
|
|
|
|
flushParameterValuesToValueTree();
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::valueTreePropertyChanged (ValueTree& tree, const Identifier&)
|
|
{
|
|
if (tree.hasType (valueType) && tree.getParent() == state)
|
|
setNewState (tree);
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::valueTreeChildAdded (ValueTree& parent, ValueTree& tree)
|
|
{
|
|
if (parent == state && tree.hasType (valueType))
|
|
setNewState (tree);
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::valueTreeChildRemoved (ValueTree&, ValueTree&, int)
|
|
{
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::valueTreeRedirected (ValueTree& v)
|
|
{
|
|
if (v == state)
|
|
updateParameterConnectionsToChildTrees();
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::valueTreeChildOrderChanged (ValueTree&, int, int) {}
|
|
void AudioProcessorValueTreeState::valueTreeParentChanged (ValueTree&) {}
|
|
|
|
bool AudioProcessorValueTreeState::flushParameterValuesToValueTree()
|
|
{
|
|
ScopedLock lock (valueTreeChanging);
|
|
|
|
bool anyUpdated = false;
|
|
|
|
for (auto& p : adapterTable)
|
|
anyUpdated |= p.second->flushToTree (valuePropertyID, undoManager);
|
|
|
|
return anyUpdated;
|
|
}
|
|
|
|
void AudioProcessorValueTreeState::timerCallback()
|
|
{
|
|
auto anythingUpdated = flushParameterValuesToValueTree();
|
|
|
|
startTimer (anythingUpdated ? 1000 / 50
|
|
: jlimit (50, 500, getTimerInterval() + 20));
|
|
}
|
|
|
|
//==============================================================================
|
|
struct AttachedControlBase : public AudioProcessorValueTreeState::Listener,
|
|
public AsyncUpdater
|
|
{
|
|
AttachedControlBase (AudioProcessorValueTreeState& s, const String& p)
|
|
: state (s), paramID (p), lastValue (0)
|
|
{
|
|
state.addParameterListener (paramID, this);
|
|
}
|
|
|
|
void removeListener()
|
|
{
|
|
state.removeParameterListener (paramID, this);
|
|
}
|
|
|
|
void setNewDenormalisedValue (float newDenormalisedValue)
|
|
{
|
|
if (auto* p = state.getParameter (paramID))
|
|
{
|
|
const float newValue = state.getParameterRange (paramID)
|
|
.convertTo0to1 (newDenormalisedValue);
|
|
|
|
if (p->getValue() != newValue)
|
|
p->setValueNotifyingHost (newValue);
|
|
}
|
|
}
|
|
|
|
void sendInitialUpdate()
|
|
{
|
|
if (auto* v = state.getRawParameterValue (paramID))
|
|
parameterChanged (paramID, *v);
|
|
}
|
|
|
|
void parameterChanged (const String&, float newValue) override
|
|
{
|
|
lastValue = newValue;
|
|
|
|
if (MessageManager::getInstance()->isThisTheMessageThread())
|
|
{
|
|
cancelPendingUpdate();
|
|
setValue (newValue);
|
|
}
|
|
else
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
}
|
|
|
|
void beginParameterChange()
|
|
{
|
|
if (auto* p = state.getParameter (paramID))
|
|
{
|
|
if (state.undoManager != nullptr)
|
|
state.undoManager->beginNewTransaction();
|
|
|
|
p->beginChangeGesture();
|
|
}
|
|
}
|
|
|
|
void endParameterChange()
|
|
{
|
|
if (AudioProcessorParameter* p = state.getParameter (paramID))
|
|
p->endChangeGesture();
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
setValue (lastValue);
|
|
}
|
|
|
|
virtual void setValue (float) = 0;
|
|
|
|
AudioProcessorValueTreeState& state;
|
|
String paramID;
|
|
float lastValue;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AttachedControlBase)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct AudioProcessorValueTreeState::SliderAttachment::Pimpl : private AttachedControlBase,
|
|
private Slider::Listener
|
|
{
|
|
Pimpl (AudioProcessorValueTreeState& s, const String& p, Slider& sl)
|
|
: AttachedControlBase (s, p), slider (sl), ignoreCallbacks (false)
|
|
{
|
|
NormalisableRange<float> range (state.getParameterRange (paramID));
|
|
|
|
if (auto* param = state.getParameterAdapter (paramID))
|
|
{
|
|
slider.valueFromTextFunction = [param](const String& text) { return (double) param->getDenormalisedValueForText (text); };
|
|
slider.textFromValueFunction = [param](double value) { return param->getTextForDenormalisedValue ((float) value); };
|
|
slider.setDoubleClickReturnValue (true, range.convertFrom0to1 (param->getParameter().getDefaultValue()));
|
|
}
|
|
|
|
if (range.interval != 0.0f || range.skew != 1.0f)
|
|
{
|
|
slider.setRange (range.start, range.end, range.interval);
|
|
slider.setSkewFactor (range.skew, range.symmetricSkew);
|
|
}
|
|
else
|
|
{
|
|
auto convertFrom0To1Function = [range](double currentRangeStart,
|
|
double currentRangeEnd,
|
|
double normalisedValue) mutable
|
|
{
|
|
range.start = (float) currentRangeStart;
|
|
range.end = (float) currentRangeEnd;
|
|
return (double) range.convertFrom0to1 ((float) normalisedValue);
|
|
};
|
|
|
|
auto convertTo0To1Function = [range](double currentRangeStart,
|
|
double currentRangeEnd,
|
|
double mappedValue) mutable
|
|
{
|
|
range.start = (float) currentRangeStart;
|
|
range.end = (float) currentRangeEnd;
|
|
return (double) range.convertTo0to1 ((float) mappedValue);
|
|
};
|
|
|
|
auto snapToLegalValueFunction = [range](double currentRangeStart,
|
|
double currentRangeEnd,
|
|
double valueToSnap) mutable
|
|
{
|
|
range.start = (float) currentRangeStart;
|
|
range.end = (float) currentRangeEnd;
|
|
return (double) range.snapToLegalValue ((float) valueToSnap);
|
|
};
|
|
|
|
slider.setNormalisableRange ({ (double) range.start,
|
|
(double) range.end,
|
|
convertFrom0To1Function,
|
|
convertTo0To1Function,
|
|
snapToLegalValueFunction });
|
|
}
|
|
|
|
sendInitialUpdate();
|
|
slider.addListener (this);
|
|
}
|
|
|
|
~Pimpl() override
|
|
{
|
|
slider.removeListener (this);
|
|
removeListener();
|
|
}
|
|
|
|
void setValue (float newValue) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
{
|
|
ScopedValueSetter<bool> svs (ignoreCallbacks, true);
|
|
slider.setValue (newValue, sendNotificationSync);
|
|
}
|
|
}
|
|
|
|
void sliderValueChanged (Slider* s) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
if ((! ignoreCallbacks) && (! ModifierKeys::currentModifiers.isRightButtonDown()))
|
|
setNewDenormalisedValue ((float) s->getValue());
|
|
}
|
|
|
|
void sliderDragStarted (Slider*) override { beginParameterChange(); }
|
|
void sliderDragEnded (Slider*) override { endParameterChange(); }
|
|
|
|
Slider& slider;
|
|
bool ignoreCallbacks;
|
|
CriticalSection selfCallbackMutex;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
|
};
|
|
|
|
AudioProcessorValueTreeState::SliderAttachment::SliderAttachment (AudioProcessorValueTreeState& s, const String& p, Slider& sl)
|
|
: pimpl (new Pimpl (s, p, sl))
|
|
{
|
|
}
|
|
|
|
AudioProcessorValueTreeState::SliderAttachment::~SliderAttachment() {}
|
|
|
|
//==============================================================================
|
|
struct AudioProcessorValueTreeState::ComboBoxAttachment::Pimpl : private AttachedControlBase,
|
|
private ComboBox::Listener
|
|
{
|
|
Pimpl (AudioProcessorValueTreeState& s, const String& p, ComboBox& c)
|
|
: AttachedControlBase (s, p), combo (c), ignoreCallbacks (false)
|
|
{
|
|
sendInitialUpdate();
|
|
combo.addListener (this);
|
|
}
|
|
|
|
~Pimpl() override
|
|
{
|
|
combo.removeListener (this);
|
|
removeListener();
|
|
}
|
|
|
|
void setValue (float newValue) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
if (state.getParameter (paramID) != nullptr)
|
|
{
|
|
auto normValue = state.getParameterRange (paramID)
|
|
.convertTo0to1 (newValue);
|
|
auto index = roundToInt (normValue * (combo.getNumItems() - 1));
|
|
|
|
if (index != combo.getSelectedItemIndex())
|
|
{
|
|
ScopedValueSetter<bool> svs (ignoreCallbacks, true);
|
|
combo.setSelectedItemIndex (index, sendNotificationSync);
|
|
}
|
|
}
|
|
}
|
|
|
|
void comboBoxChanged (ComboBox*) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
if (! ignoreCallbacks)
|
|
{
|
|
if (auto* p = state.getParameter (paramID))
|
|
{
|
|
auto newValue = (float) combo.getSelectedItemIndex() / (combo.getNumItems() - 1);
|
|
|
|
if (p->getValue() != newValue)
|
|
{
|
|
beginParameterChange();
|
|
p->setValueNotifyingHost (newValue);
|
|
endParameterChange();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ComboBox& combo;
|
|
bool ignoreCallbacks;
|
|
CriticalSection selfCallbackMutex;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
|
};
|
|
|
|
AudioProcessorValueTreeState::ComboBoxAttachment::ComboBoxAttachment (AudioProcessorValueTreeState& s, const String& p, ComboBox& c)
|
|
: pimpl (new Pimpl (s, p, c))
|
|
{
|
|
}
|
|
|
|
AudioProcessorValueTreeState::ComboBoxAttachment::~ComboBoxAttachment() {}
|
|
|
|
//==============================================================================
|
|
struct AudioProcessorValueTreeState::ButtonAttachment::Pimpl : private AttachedControlBase,
|
|
private Button::Listener
|
|
{
|
|
Pimpl (AudioProcessorValueTreeState& s, const String& p, Button& b)
|
|
: AttachedControlBase (s, p), button (b), ignoreCallbacks (false)
|
|
{
|
|
sendInitialUpdate();
|
|
button.addListener (this);
|
|
}
|
|
|
|
~Pimpl() override
|
|
{
|
|
button.removeListener (this);
|
|
removeListener();
|
|
}
|
|
|
|
void setValue (float newValue) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
{
|
|
ScopedValueSetter<bool> svs (ignoreCallbacks, true);
|
|
button.setToggleState (newValue >= 0.5f, sendNotificationSync);
|
|
}
|
|
}
|
|
|
|
void buttonClicked (Button* b) override
|
|
{
|
|
const ScopedLock selfCallbackLock (selfCallbackMutex);
|
|
|
|
if (! ignoreCallbacks)
|
|
{
|
|
beginParameterChange();
|
|
setNewDenormalisedValue (b->getToggleState() ? 1.0f : 0.0f);
|
|
endParameterChange();
|
|
}
|
|
}
|
|
|
|
Button& button;
|
|
bool ignoreCallbacks;
|
|
CriticalSection selfCallbackMutex;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
|
};
|
|
|
|
AudioProcessorValueTreeState::ButtonAttachment::ButtonAttachment (AudioProcessorValueTreeState& s, const String& p, Button& b)
|
|
: pimpl (new Pimpl (s, p, b))
|
|
{
|
|
}
|
|
|
|
AudioProcessorValueTreeState::ButtonAttachment::~ButtonAttachment() {}
|
|
|
|
#if JUCE_UNIT_TESTS
|
|
|
|
static struct ParameterAdapterTests final : public UnitTest
|
|
{
|
|
ParameterAdapterTests() : UnitTest ("Parameter Adapter") {}
|
|
|
|
void runTest() override
|
|
{
|
|
beginTest ("The default value is returned correctly");
|
|
{
|
|
const auto test = [&] (NormalisableRange<float> range, float value)
|
|
{
|
|
AudioParameterFloat param ({}, {}, range, value, {});
|
|
|
|
AudioProcessorValueTreeState::ParameterAdapter adapter (param);
|
|
|
|
expectEquals (adapter.getDenormalisedDefaultValue(), value);
|
|
};
|
|
|
|
test ({ -100, 100 }, 0);
|
|
test ({ -2.5, 12.5 }, 10);
|
|
}
|
|
|
|
beginTest ("Denormalised parameter values can be retrieved");
|
|
{
|
|
const auto test = [&](NormalisableRange<float> range, float value)
|
|
{
|
|
AudioParameterFloat param ({}, {}, range, {}, {});
|
|
AudioProcessorValueTreeState::ParameterAdapter adapter (param);
|
|
|
|
adapter.setDenormalisedValue (value);
|
|
|
|
expectEquals (adapter.getDenormalisedValue(), value);
|
|
expectEquals (adapter.getRawDenormalisedValue(), value);
|
|
};
|
|
|
|
test ({ -20, -10 }, -15);
|
|
test ({ 0, 7.5 }, 2.5);
|
|
}
|
|
|
|
beginTest ("Floats can be converted to text");
|
|
{
|
|
const auto test = [&](NormalisableRange<float> range, float value, String expected)
|
|
{
|
|
AudioParameterFloat param ({}, {}, range, {}, {});
|
|
AudioProcessorValueTreeState::ParameterAdapter adapter (param);
|
|
|
|
expectEquals (adapter.getTextForDenormalisedValue (value), expected);
|
|
};
|
|
|
|
test ({ -100, 100 }, 0, "0.0000000");
|
|
test ({ -2.5, 12.5 }, 10, "10.0000000");
|
|
test ({ -20, -10 }, -15, "-15.0000000");
|
|
test ({ 0, 7.5 }, 2.5, "2.5000000");
|
|
}
|
|
|
|
beginTest ("Text can be converted to floats");
|
|
{
|
|
const auto test = [&](NormalisableRange<float> range, String text, float expected)
|
|
{
|
|
AudioParameterFloat param ({}, {}, range, {}, {});
|
|
AudioProcessorValueTreeState::ParameterAdapter adapter (param);
|
|
|
|
expectEquals (adapter.getDenormalisedValueForText (text), expected);
|
|
};
|
|
|
|
test ({ -100, 100 }, "0.0", 0);
|
|
test ({ -2.5, 12.5 }, "10.0", 10);
|
|
test ({ -20, -10 }, "-15.0", -15);
|
|
test ({ 0, 7.5 }, "2.5", 2.5);
|
|
}
|
|
}
|
|
} parameterAdapterTests;
|
|
|
|
namespace
|
|
{
|
|
template <typename ValueType>
|
|
inline bool operator== (const NormalisableRange<ValueType>& a,
|
|
const NormalisableRange<ValueType>& b)
|
|
{
|
|
return std::tie (a.start, a.end, a.interval, a.skew, a.symmetricSkew)
|
|
== std::tie (b.start, b.end, b.interval, b.skew, b.symmetricSkew);
|
|
}
|
|
|
|
template <typename ValueType>
|
|
inline bool operator!= (const NormalisableRange<ValueType>& a,
|
|
const NormalisableRange<ValueType>& b)
|
|
{
|
|
return ! (a == b);
|
|
}
|
|
} // namespace
|
|
|
|
static class AudioProcessorValueTreeStateTests final : public UnitTest
|
|
{
|
|
private:
|
|
using Parameter = AudioProcessorValueTreeState::Parameter;
|
|
using ParameterGroup = AudioProcessorParameterGroup;
|
|
using ParameterLayout = AudioProcessorValueTreeState::ParameterLayout;
|
|
|
|
class TestAudioProcessor : public AudioProcessor
|
|
{
|
|
public:
|
|
TestAudioProcessor() = default;
|
|
|
|
explicit TestAudioProcessor (ParameterLayout layout)
|
|
: state (*this, nullptr, "state", std::move (layout)) {}
|
|
|
|
const String getName() const override { return {}; }
|
|
void prepareToPlay (double, int) override {}
|
|
void releaseResources() override {}
|
|
void processBlock (AudioBuffer<float>&, MidiBuffer&) override {}
|
|
double getTailLengthSeconds() const override { return {}; }
|
|
bool acceptsMidi() const override { return {}; }
|
|
bool producesMidi() const override { return {}; }
|
|
AudioProcessorEditor* createEditor() override { return {}; }
|
|
bool hasEditor() const override { return {}; }
|
|
int getNumPrograms() override { return 1; }
|
|
int getCurrentProgram() override { return {}; }
|
|
void setCurrentProgram (int) override {}
|
|
const String getProgramName (int) override { return {}; }
|
|
void changeProgramName (int, const String&) override {}
|
|
void getStateInformation (MemoryBlock&) override {}
|
|
void setStateInformation (const void*, int) override {}
|
|
|
|
AudioProcessorValueTreeState state { *this, nullptr };
|
|
};
|
|
|
|
struct Listener final : public AudioProcessorValueTreeState::Listener
|
|
{
|
|
void parameterChanged (const String& idIn, float valueIn) override
|
|
{
|
|
id = idIn;
|
|
value = valueIn;
|
|
}
|
|
|
|
String id;
|
|
float value{};
|
|
};
|
|
|
|
public:
|
|
AudioProcessorValueTreeStateTests() : UnitTest ("Audio Processor Value Tree State", "AudioProcessor parameters") {}
|
|
|
|
void runTest() override
|
|
{
|
|
ScopedJuceInitialiser_GUI scopedJuceInitialiser_gui;
|
|
|
|
beginTest ("After calling createAndAddParameter, the number of parameters increases by one");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
proc.state.createAndAddParameter (std::make_unique<Parameter> (String(), String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
|
|
expectEquals (proc.getParameters().size(), 1);
|
|
}
|
|
|
|
beginTest ("After creating a normal named parameter, we can later retrieve that parameter");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
|
|
expect (proc.state.getParameter (key) == param);
|
|
}
|
|
|
|
beginTest ("After construction, the value tree has the expected format");
|
|
{
|
|
TestAudioProcessor proc ({
|
|
std::make_unique<AudioProcessorParameterGroup> ("", "", "",
|
|
std::make_unique<AudioParameterBool> ("a", "", false),
|
|
std::make_unique<AudioParameterFloat> ("b", "", NormalisableRange<float>{}, 0.0f)),
|
|
std::make_unique<AudioProcessorParameterGroup> ("", "", "",
|
|
std::make_unique<AudioParameterInt> ("c", "", 0, 1, 0),
|
|
std::make_unique<AudioParameterChoice> ("d", "", StringArray { "foo", "bar" }, 0)) });
|
|
|
|
const auto valueTree = proc.state.copyState();
|
|
|
|
expectEquals (valueTree.getNumChildren(), 4);
|
|
|
|
for (auto child : valueTree)
|
|
{
|
|
expect (child.hasType ("PARAM"));
|
|
expect (child.hasProperty ("id"));
|
|
expect (child.hasProperty ("value"));
|
|
}
|
|
}
|
|
|
|
beginTest ("Meta parameters can be created");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr, true));
|
|
|
|
expect (param->isMetaParameter());
|
|
}
|
|
|
|
beginTest ("Automatable parameters can be created");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr, false, true));
|
|
|
|
expect (param->isAutomatable());
|
|
}
|
|
|
|
beginTest ("Discrete parameters can be created");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr, false, false, true));
|
|
|
|
expect (param->isDiscrete());
|
|
}
|
|
|
|
beginTest ("Custom category parameters can be created");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr, false, false, false,
|
|
AudioProcessorParameter::Category::inputMeter));
|
|
|
|
expect (param->category == AudioProcessorParameter::Category::inputMeter);
|
|
}
|
|
|
|
beginTest ("Boolean parameters can be created");
|
|
{
|
|
TestAudioProcessor proc;
|
|
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr, false, false, false,
|
|
AudioProcessorParameter::Category::genericParameter, true));
|
|
|
|
expect (param->isBoolean());
|
|
}
|
|
|
|
beginTest ("After creating a custom named parameter, we can later retrieve that parameter");
|
|
{
|
|
const auto key = "id";
|
|
auto param = std::make_unique<AudioParameterBool> (key, "", false);
|
|
const auto paramPtr = param.get();
|
|
|
|
TestAudioProcessor proc (std::move (param));
|
|
|
|
expect (proc.state.getParameter (key) == paramPtr);
|
|
}
|
|
|
|
beginTest ("After adding a normal parameter that already exists, the AudioProcessor parameters are unchanged");
|
|
{
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
|
|
proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
|
|
expectEquals (proc.getParameters().size(), 1);
|
|
expect (proc.getParameters().getFirst() == param);
|
|
}
|
|
|
|
beginTest ("After setting a parameter value, that value is reflected in the state");
|
|
{
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
|
|
const auto value = 0.5f;
|
|
param->setValueNotifyingHost (value);
|
|
|
|
expectEquals (*proc.state.getRawParameterValue (key), value);
|
|
}
|
|
|
|
beginTest ("After adding an APVTS::Parameter, its value is the default value");
|
|
{
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
const auto value = 5.0f;
|
|
|
|
proc.state.createAndAddParameter (std::make_unique<Parameter> (
|
|
key,
|
|
String(),
|
|
String(),
|
|
NormalisableRange<float> (0.0f, 100.0f, 10.0f),
|
|
value,
|
|
nullptr,
|
|
nullptr));
|
|
|
|
expectEquals (*proc.state.getRawParameterValue (key), value);
|
|
}
|
|
|
|
beginTest ("Listeners receive notifications when parameters change");
|
|
{
|
|
Listener listener;
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
const auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
proc.state.addParameterListener (key, &listener);
|
|
|
|
const auto value = 0.5f;
|
|
param->setValueNotifyingHost (value);
|
|
|
|
expectEquals (listener.id, String { key });
|
|
expectEquals (listener.value, value);
|
|
}
|
|
|
|
beginTest ("Bool parameters have a range of 0-1");
|
|
{
|
|
const auto key = "id";
|
|
|
|
TestAudioProcessor proc (std::make_unique<AudioParameterBool> (key, "", false));
|
|
|
|
expect (proc.state.getParameterRange (key) == NormalisableRange<float> (0.0f, 1.0f, 1.0f));
|
|
}
|
|
|
|
beginTest ("Float parameters retain their specified range");
|
|
{
|
|
const auto key = "id";
|
|
const auto range = NormalisableRange<float> { -100, 100, 0.7f, 0.2f, true };
|
|
|
|
TestAudioProcessor proc (std::make_unique<AudioParameterFloat> (key, "", range, 0.0f));
|
|
|
|
expect (proc.state.getParameterRange (key) == range);
|
|
}
|
|
|
|
beginTest ("Int parameters retain their specified range");
|
|
{
|
|
const auto key = "id";
|
|
const auto min = -27;
|
|
const auto max = 53;
|
|
|
|
TestAudioProcessor proc (std::make_unique<AudioParameterInt> (key, "", min, max, 0));
|
|
|
|
expect (proc.state.getParameterRange (key) == NormalisableRange<float> (float (min), float (max)));
|
|
}
|
|
|
|
beginTest ("Choice parameters retain their specified range");
|
|
{
|
|
const auto key = "id";
|
|
const auto choices = StringArray { "", "", "" };
|
|
|
|
TestAudioProcessor proc (std::make_unique<AudioParameterChoice> (key, "", choices, 0));
|
|
|
|
expect (proc.state.getParameterRange (key) == NormalisableRange<float> (0.0f, (float) (choices.size() - 1)));
|
|
expect (proc.state.getParameter (key)->getNumSteps() == choices.size());
|
|
}
|
|
|
|
beginTest ("When the parameter value is changed, normal parameter values are updated");
|
|
{
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
const auto initialValue = 0.2f;
|
|
auto param = proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
initialValue, nullptr, nullptr));
|
|
proc.state.state = ValueTree { "state" };
|
|
|
|
auto value = proc.state.getParameterAsValue (key);
|
|
expectEquals (float (value.getValue()), initialValue);
|
|
|
|
const auto newValue = 0.75f;
|
|
value = newValue;
|
|
|
|
expectEquals (param->getValue(), newValue);
|
|
expectEquals (*proc.state.getRawParameterValue (key), newValue);
|
|
}
|
|
|
|
beginTest ("When the parameter value is changed, custom parameter values are updated");
|
|
{
|
|
const auto key = "id";
|
|
const auto choices = StringArray ("foo", "bar", "baz");
|
|
auto param = std::make_unique<AudioParameterChoice> (key, "", choices, 0);
|
|
const auto paramPtr = param.get();
|
|
TestAudioProcessor proc (std::move (param));
|
|
|
|
const auto newValue = 2.0f;
|
|
auto value = proc.state.getParameterAsValue (key);
|
|
value = newValue;
|
|
|
|
expectEquals (paramPtr->getCurrentChoiceName(), choices[int (newValue)]);
|
|
expectEquals (*proc.state.getRawParameterValue (key), newValue);
|
|
}
|
|
|
|
beginTest ("When the parameter value is changed, listeners are notified");
|
|
{
|
|
Listener listener;
|
|
TestAudioProcessor proc;
|
|
const auto key = "id";
|
|
proc.state.createAndAddParameter (std::make_unique<Parameter> (key, String(), String(), NormalisableRange<float>(),
|
|
0.0f, nullptr, nullptr));
|
|
proc.state.addParameterListener (key, &listener);
|
|
proc.state.state = ValueTree { "state" };
|
|
|
|
const auto newValue = 0.75f;
|
|
proc.state.getParameterAsValue (key) = newValue;
|
|
|
|
expectEquals (listener.value, newValue);
|
|
expectEquals (listener.id, String { key });
|
|
}
|
|
|
|
beginTest ("When the parameter value is changed, listeners are notified");
|
|
{
|
|
const auto key = "id";
|
|
const auto choices = StringArray { "foo", "bar", "baz" };
|
|
Listener listener;
|
|
TestAudioProcessor proc (std::make_unique<AudioParameterChoice> (key, "", choices, 0));
|
|
proc.state.addParameterListener (key, &listener);
|
|
|
|
const auto newValue = 2.0f;
|
|
proc.state.getParameterAsValue (key) = newValue;
|
|
|
|
expectEquals (listener.value, newValue);
|
|
expectEquals (listener.id, String (key));
|
|
}
|
|
}
|
|
} audioProcessorValueTreeStateTests;
|
|
|
|
#endif
|
|
|
|
} // namespace juce
|