/* ============================================================================== 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 valueRange, float defaultValue, std::function valueToTextFunction, std::function textToValueFunction, bool isMetaParameter, bool isAutomatableParameter, bool isDiscrete, AudioProcessorParameter::Category category, bool isBoolean) : AudioParameterFloat (parameterID, parameterName, valueRange, defaultValue, labelText, category, valueToTextFunction == nullptr ? std::function() : [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& 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 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 listeners; float unnormalisedValue{}; std::atomic 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 param) const override { if (param == nullptr) { jassertfalse; return; } state->addParameterAdapter (*param); state->processor.addParameter (param.release()); } void visit (std::unique_ptr group) const override { if (group == nullptr) { jassertfalse; return; } for (const auto param : group->getParameters (true)) { if (const auto rangedParam = dynamic_cast (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 range, float defaultVal, std::function valueToTextFunction, std::function textToValueFunction, bool isMetaParameter, bool isAutomatableParameter, bool isDiscreteParameter, AudioProcessorParameter::Category category, bool isBooleanParameter) { return createAndAddParameter (std::make_unique (paramID, paramName, labelText, range, defaultVal, std::move (valueToTextFunction), std::move (textToValueFunction), isMetaParameter, isAutomatableParameter, isDiscreteParameter, category, isBooleanParameter)); } RangedAudioParameter* AudioProcessorValueTreeState::createAndAddParameter (std::unique_ptr 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 (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 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 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 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 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 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 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 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 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 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 inline bool operator== (const NormalisableRange& a, const NormalisableRange& 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 inline bool operator!= (const NormalisableRange& a, const NormalisableRange& 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&, 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 (String(), String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 ("", "", "", std::make_unique ("a", "", false), std::make_unique ("b", "", NormalisableRange{}, 0.0f)), std::make_unique ("", "", "", std::make_unique ("c", "", 0, 1, 0), std::make_unique ("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 (key, String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 (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 (key, String(), String(), NormalisableRange(), 0.0f, nullptr, nullptr)); proc.state.createAndAddParameter (std::make_unique (key, String(), String(), NormalisableRange(), 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 (key, String(), String(), NormalisableRange(), 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 ( key, String(), String(), NormalisableRange (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 (key, String(), String(), NormalisableRange(), 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 (key, "", false)); expect (proc.state.getParameterRange (key) == NormalisableRange (0.0f, 1.0f, 1.0f)); } beginTest ("Float parameters retain their specified range"); { const auto key = "id"; const auto range = NormalisableRange { -100, 100, 0.7f, 0.2f, true }; TestAudioProcessor proc (std::make_unique (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 (key, "", min, max, 0)); expect (proc.state.getParameterRange (key) == NormalisableRange (float (min), float (max))); } beginTest ("Choice parameters retain their specified range"); { const auto key = "id"; const auto choices = StringArray { "", "", "" }; TestAudioProcessor proc (std::make_unique (key, "", choices, 0)); expect (proc.state.getParameterRange (key) == NormalisableRange (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 (key, String(), String(), NormalisableRange(), 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 (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 (key, String(), String(), NormalisableRange(), 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 (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