diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index ade8751a39..0c596691d0 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,39 @@ JUCE breaking changes Develop ======= +Change +------ +The AudioProcessorValueTreeState::createAndAddParameter function has been +deprecated. + +Possible Issues +--------------- +Deprecation warnings will be seen when compiling code which uses this function +and eventually builds will fail when it is later removed from the API. + +Workaround +---------- +Previous calls to + +createAndAddParameter (paramID, paramName, ...); + +can be directly replaced with + +using Parameter = AudioProcessorValueTreeState::Parameter; +createAndAddParameter (std::make_unique (paramID, paramName, ...)); + +but an even better approach is to use the new AudioProcessorValueTreeState +constructor where you can pass both RangedAudioParameters and +AudioProcessorParameterGroups of RangedAudioParameters to the +AudioProcessorValueTreeState and initialise the ValueTree simultaneously. + +Rationale +--------- +The new createAndAddParameter method is much more flexible and enables any +parameter types derived from RangedAudioParameter to be managed by the +AudioProcessorValueTreeState. + + Change ------ The Projucer's per-exporter Android SDK/NDK path options have been removed. @@ -15,7 +48,7 @@ Projects that previously used these fields may no longer build. Workaround ---------- Use the Projucer's global paths settings to point to these directories, either -by opening the "Projucer/File->Global Paths..." menu item or using the +by opening the "Projucer/File->Global Paths..." menu item or using the "--set-global-search-path" command-line option. Rationale diff --git a/examples/Plugins/AudioPluginDemo.h b/examples/Plugins/AudioPluginDemo.h index f7b428bda0..5d78c58740 100644 --- a/examples/Plugins/AudioPluginDemo.h +++ b/examples/Plugins/AudioPluginDemo.h @@ -175,19 +175,14 @@ public: //============================================================================== JuceDemoPluginAudioProcessor() : AudioProcessor (getBusesProperties()), - state (*this, nullptr) + state (*this, nullptr, "state", + { std::make_unique ("gain", "Gain", NormalisableRange (0.0f, 1.0f), 0.9f), + std::make_unique ("delay", "Delay Feedback", NormalisableRange (0.0f, 1.0f), 0.5f) }) { lastPosInfo.resetToDefault(); - // This creates our parameters - state.createAndAddParameter ("gain", "Gain", {}, {}, 0.9f, {}, {}); - state.createAndAddParameter ("delay", "Delay Feedback", {}, {}, 0.5f, {}, {}); - - state.state = ValueTree ("state", {}, - { - // add a sub-tree to store the state of our UI - {"uiState", {{"width", 400}, {"height", 200}}, {}} - }); + // Add a sub-tree to store the state of our UI + state.state.addChild ({ "uiState", { { "width", 400 }, { "height", 200 } }, {} }, -1, nullptr); initialiseSynth(); } diff --git a/examples/Plugins/InterAppAudioEffectPluginDemo.h b/examples/Plugins/InterAppAudioEffectPluginDemo.h index c05606f95d..46e8af1244 100644 --- a/examples/Plugins/InterAppAudioEffectPluginDemo.h +++ b/examples/Plugins/InterAppAudioEffectPluginDemo.h @@ -152,17 +152,9 @@ public: : AudioProcessor (BusesProperties() .withInput ("Input", AudioChannelSet::stereo(), true) .withOutput ("Output", AudioChannelSet::stereo(), true)), - parameters (*this, nullptr) + parameters (*this, nullptr, "InterAppAudioEffect", + { std::make_unique ("gain", "Gain", NormalisableRange (0.0f, 1.0f), 1.0f / 3.14f) }) { - parameters.createAndAddParameter ("gain", - "Gain", - {}, - NormalisableRange (0.0f, 1.0f), - (float) (1.0 / 3.14), - nullptr, - nullptr); - - parameters.state = ValueTree (Identifier ("InterAppAudioEffect")); } ~IAAEffectProcessor() {} diff --git a/modules/juce_audio_processors/juce_audio_processors.cpp b/modules/juce_audio_processors/juce_audio_processors.cpp index 06e56ee0f2..fcd6b8f503 100644 --- a/modules/juce_audio_processors/juce_audio_processors.cpp +++ b/modules/juce_audio_processors/juce_audio_processors.cpp @@ -39,6 +39,8 @@ #include "juce_audio_processors.h" #include +#include + //============================================================================== #if JUCE_MAC #if JUCE_SUPPORT_CARBON \ diff --git a/modules/juce_audio_processors/juce_audio_processors.h b/modules/juce_audio_processors/juce_audio_processors.h index fe6c1dc16e..6e9bd06589 100644 --- a/modules/juce_audio_processors/juce_audio_processors.h +++ b/modules/juce_audio_processors/juce_audio_processors.h @@ -127,6 +127,7 @@ #include "scanning/juce_PluginDirectoryScanner.h" #include "scanning/juce_PluginListComponent.h" #include "utilities/juce_AudioProcessorParameterWithID.h" +#include "utilities/juce_RangedAudioParameter.h" #include "utilities/juce_AudioParameterFloat.h" #include "utilities/juce_AudioParameterInt.h" #include "utilities/juce_AudioParameterBool.h" diff --git a/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h b/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h index fd010ed528..24f7176ed3 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h +++ b/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h @@ -34,7 +34,7 @@ namespace juce @tags{Audio} */ -class JUCE_API AudioParameterBool : public AudioProcessorParameterWithID +class JUCE_API AudioParameterBool : public RangedAudioParameter { public: /** Creates a AudioParameterBool with the specified parameters. @@ -60,12 +60,16 @@ public: /** Returns the parameter's current boolean value. */ bool get() const noexcept { return value >= 0.5f; } + /** Returns the parameter's current boolean value. */ operator bool() const noexcept { return get(); } /** Changes the parameter's current value to a new boolean. */ AudioParameterBool& operator= (bool newValue); + /** Returns the range of values that the parameter can take. */ + const NormalisableRange& getNormalisableRange() const override { return range; } + protected: /** Override this method if you are interested in receiving callbacks when the parameter value changes. @@ -83,6 +87,7 @@ private: String getText (float, int) const override; float getValueForText (const String&) const override; + const NormalisableRange range { 0.0f, 1.0f, 1.0f }; float value; const float defaultValue; std::function stringFromBoolFunction; diff --git a/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h b/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h index 7abd04ecad..d584bb91fe 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h +++ b/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h @@ -35,7 +35,7 @@ namespace juce @tags{Audio} */ -class JUCE_API AudioParameterChoice : public AudioProcessorParameterWithID +class JUCE_API AudioParameterChoice : public RangedAudioParameter { public: /** Creates a AudioParameterChoice with the specified parameters. @@ -64,17 +64,22 @@ public: /** Returns the current index of the selected item. */ int getIndex() const noexcept { return roundToInt (value); } + /** Returns the current index of the selected item. */ operator int() const noexcept { return getIndex(); } /** Returns the name of the currently selected item. */ String getCurrentChoiceName() const noexcept { return choices[getIndex()]; } + /** Returns the name of the currently selected item. */ operator String() const noexcept { return getCurrentChoiceName(); } /** Changes the selected item to a new index. */ AudioParameterChoice& operator= (int newValue); + /** Returns the range of values that the parameter can take. */ + const NormalisableRange& getNormalisableRange() const override { return range; } + /** Provides access to the list of choices that this parameter is working with. */ const StringArray choices; @@ -94,12 +99,8 @@ private: String getText (float, int) const override; float getValueForText (const String&) const override; - int limitRange (int) const noexcept; - float convertTo0to1 (int) const noexcept; - int convertFrom0to1 (float) const noexcept; - + const NormalisableRange range; float value; - const int maxIndex; const float defaultValue; std::function stringFromIndexFunction; std::function indexFromStringFunction; diff --git a/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h b/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h index 6193d7f9ab..28410c5d86 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h +++ b/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h @@ -35,7 +35,7 @@ namespace juce @tags{Audio} */ -class JUCE_API AudioParameterFloat : public AudioProcessorParameterWithID +class JUCE_API AudioParameterFloat : public RangedAudioParameter { public: /** Creates a AudioParameterFloat with the specified parameters. @@ -53,7 +53,8 @@ public: converts it into a non-normalised value. Some hosts use this to allow users to type in parameter values. */ - AudioParameterFloat (const String& parameterID, const String& name, + AudioParameterFloat (const String& parameterID, + const String& name, NormalisableRange normalisableRange, float defaultValue, const String& label = String(), @@ -66,7 +67,8 @@ public: For control over skew factors, you can use the other constructor and provide a NormalisableRange. */ - AudioParameterFloat (String parameterID, String name, + AudioParameterFloat (String parameterID, + String name, float minValue, float maxValue, float defaultValue); @@ -76,12 +78,16 @@ public: /** Returns the parameter's current value. */ float get() const noexcept { return value; } + /** Returns the parameter's current value. */ operator float() const noexcept { return value; } /** Changes the parameter's current value. */ AudioParameterFloat& operator= (float newValue); + /** Returns the range of values that the parameter can take. */ + const NormalisableRange& getNormalisableRange() const override { return range; } + /** Provides access to the parameter's range. */ NormalisableRange range; diff --git a/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h b/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h index 17feac122a..76d4dfa42d 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h +++ b/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h @@ -35,7 +35,7 @@ namespace juce @tags{Audio} */ -class JUCE_API AudioParameterInt : public AudioProcessorParameterWithID +class JUCE_API AudioParameterInt : public RangedAudioParameter { public: /** Creates a AudioParameterInt with the specified parameters. @@ -65,6 +65,7 @@ public: /** Returns the parameter's current value as an integer. */ int get() const noexcept { return roundToInt (value); } + /** Returns the parameter's current value as an integer. */ operator int() const noexcept { return get(); } @@ -74,7 +75,10 @@ public: AudioParameterInt& operator= (int newValue); /** Returns the parameter's range. */ - Range getRange() const noexcept { return Range (minValue, maxValue); } + Range getRange() const noexcept { return { (int) getNormalisableRange().start, (int) getNormalisableRange().end }; } + + /** Returns the range of values that the parameter can take. */ + const NormalisableRange& getNormalisableRange() const override { return range; } protected: /** Override this method if you are interested in receiving callbacks @@ -91,11 +95,7 @@ private: String getText (float, int) const override; float getValueForText (const String&) const override; - int limitRange (int) const noexcept; - float convertTo0to1 (int) const noexcept; - int convertFrom0to1 (float) const noexcept; - - const int minValue, maxValue, rangeOfValues; + const NormalisableRange range; float value; const float defaultValue; std::function stringFromIntFunction; diff --git a/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h b/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h index 607958e59b..2c3bd4456b 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h +++ b/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h @@ -37,7 +37,7 @@ namespace juce class JUCE_API AudioProcessorParameterWithID : public AudioProcessorParameter { public: - /** Creation of this object requires providing a name and ID which will be + /** The creation of this object requires providing a name and ID which will be constant for its lifetime. */ AudioProcessorParameterWithID (const String& parameterID, diff --git a/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp b/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp index f162a7d8a3..026f110462 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp +++ b/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp @@ -48,7 +48,7 @@ AudioParameterFloat::AudioParameterFloat (const String& idToUse, const String& n const String& labelToUse, Category categoryToUse, std::function stringFromValue, std::function valueFromString) - : AudioProcessorParameterWithID (idToUse, nameToUse, labelToUse, categoryToUse), + : RangedAudioParameter (idToUse, nameToUse, labelToUse, categoryToUse), range (r), value (def), defaultValue (def), stringFromValueFunction (stringFromValue), valueFromStringFunction (valueFromString) @@ -71,32 +71,36 @@ AudioParameterFloat::AudioParameterFloat (String pid, String nm, float minValue, AudioParameterFloat::~AudioParameterFloat() {} -float AudioParameterFloat::getValue() const { return range.convertTo0to1 (value); } -void AudioParameterFloat::setValue (float newValue) { value = range.convertFrom0to1 (newValue); valueChanged (get()); } -float AudioParameterFloat::getDefaultValue() const { return range.convertTo0to1 (defaultValue); } +float AudioParameterFloat::getValue() const { return convertTo0to1 (value); } +void AudioParameterFloat::setValue (float newValue) { value = convertFrom0to1 (newValue); valueChanged (get()); } +float AudioParameterFloat::getDefaultValue() const { return convertTo0to1 (defaultValue); } int AudioParameterFloat::getNumSteps() const { return AudioProcessorParameterWithID::getNumSteps(); } -String AudioParameterFloat::getText (float v, int length) const { return stringFromValueFunction (range.convertFrom0to1 (v), length); } -float AudioParameterFloat::getValueForText (const String& text) const { return range.convertTo0to1 (valueFromStringFunction (text)); } +String AudioParameterFloat::getText (float v, int length) const { return stringFromValueFunction (convertFrom0to1 (v), length); } +float AudioParameterFloat::getValueForText (const String& text) const { return convertTo0to1 (valueFromStringFunction (text)); } void AudioParameterFloat::valueChanged (float) {} AudioParameterFloat& AudioParameterFloat::operator= (float newValue) { if (value != newValue) - setValueNotifyingHost (range.convertTo0to1 (newValue)); + setValueNotifyingHost (convertTo0to1 (newValue)); return *this; } + //============================================================================== AudioParameterInt::AudioParameterInt (const String& idToUse, const String& nameToUse, - int mn, int mx, int def, + int minValue, int maxValue, int def, const String& labelToUse, std::function stringFromInt, std::function intFromString) - : AudioProcessorParameterWithID (idToUse, nameToUse, labelToUse), - minValue (mn), maxValue (mx), rangeOfValues (maxValue - minValue), + : RangedAudioParameter (idToUse, nameToUse, labelToUse), + range ((float) minValue, (float) maxValue, + [](float start, float end, float v) { return jlimit (start, end, v * (end - start) + start); }, + [](float start, float end, float v) { return jlimit (0.0f, 1.0f, (v - start) / (end - start)); }, + [](float start, float end, float v) { return (float) roundToInt (juce::jlimit (start, end, v)); }), value ((float) def), - defaultValue (convertTo0to1 (def)), + defaultValue (convertTo0to1 ((float) def)), stringFromIntFunction (stringFromInt), intFromStringFunction (intFromString) { @@ -111,22 +115,18 @@ AudioParameterInt::AudioParameterInt (const String& idToUse, const String& nameT AudioParameterInt::~AudioParameterInt() {} -int AudioParameterInt::limitRange (int v) const noexcept { return jlimit (minValue, maxValue, v); } -float AudioParameterInt::convertTo0to1 (int v) const noexcept { return (limitRange (v) - minValue) / (float) rangeOfValues; } -int AudioParameterInt::convertFrom0to1 (float v) const noexcept { return limitRange (roundToInt ((v * (float) rangeOfValues) + minValue)); } - -float AudioParameterInt::getValue() const { return convertTo0to1 (roundToInt (value)); } -void AudioParameterInt::setValue (float newValue) { value = (float) convertFrom0to1 (newValue); valueChanged (get()); } +float AudioParameterInt::getValue() const { return convertTo0to1 (value); } +void AudioParameterInt::setValue (float newValue) { value = convertFrom0to1 (newValue); valueChanged (get()); } float AudioParameterInt::getDefaultValue() const { return defaultValue; } -int AudioParameterInt::getNumSteps() const { return rangeOfValues + 1; } -float AudioParameterInt::getValueForText (const String& text) const { return convertTo0to1 (intFromStringFunction (text)); } -String AudioParameterInt::getText (float v, int length) const { return stringFromIntFunction (convertFrom0to1 (v), length); } +int AudioParameterInt::getNumSteps() const { return ((int) getNormalisableRange().getRange().getLength()) + 1; } +float AudioParameterInt::getValueForText (const String& text) const { return convertTo0to1 ((float) intFromStringFunction (text)); } +String AudioParameterInt::getText (float v, int length) const { return stringFromIntFunction ((int) convertFrom0to1 (v), length); } void AudioParameterInt::valueChanged (int) {} AudioParameterInt& AudioParameterInt::operator= (int newValue) { if (get() != newValue) - setValueNotifyingHost (convertTo0to1 (newValue)); + setValueNotifyingHost (convertTo0to1 ((float) newValue)); return *this; } @@ -137,7 +137,7 @@ AudioParameterBool::AudioParameterBool (const String& idToUse, const String& nam bool def, const String& labelToUse, std::function stringFromBool, std::function boolFromString) - : AudioProcessorParameterWithID (idToUse, nameToUse, labelToUse), + : RangedAudioParameter (idToUse, nameToUse, labelToUse), value (def ? 1.0f : 0.0f), defaultValue (value), stringFromBoolFunction (stringFromBool), @@ -209,10 +209,13 @@ AudioParameterChoice::AudioParameterChoice (const String& idToUse, const String& const StringArray& c, int def, const String& labelToUse, std::function stringFromIndex, std::function indexFromString) - : AudioProcessorParameterWithID (idToUse, nameToUse, labelToUse), choices (c), + : RangedAudioParameter (idToUse, nameToUse, labelToUse), choices (c), + range (0.0f, choices.size() - 1.0f, + [](float, float end, float v) { return jlimit (0.0f, end, v * end); }, + [](float, float end, float v) { return jlimit (0.0f, 1.0f, v / end); }, + [](float start, float end, float v) { return (float) roundToInt (juce::jlimit (start, end, v)); }), value ((float) def), - maxIndex (choices.size() - 1), - defaultValue (convertTo0to1 (def)), + defaultValue (convertTo0to1 ((float) def)), stringFromIndexFunction (stringFromIndex), indexFromStringFunction (indexFromString) { @@ -227,25 +230,108 @@ AudioParameterChoice::AudioParameterChoice (const String& idToUse, const String& AudioParameterChoice::~AudioParameterChoice() {} -int AudioParameterChoice::limitRange (int v) const noexcept { return jlimit (0, maxIndex, v); } -float AudioParameterChoice::convertTo0to1 (int v) const noexcept { return jlimit (0.0f, 1.0f, v / (float) maxIndex); } -int AudioParameterChoice::convertFrom0to1 (float v) const noexcept { return limitRange (roundToInt (v * (float) maxIndex)); } - -float AudioParameterChoice::getValue() const { return convertTo0to1 (roundToInt (value)); } -void AudioParameterChoice::setValue (float newValue) { value = (float) convertFrom0to1 (newValue); valueChanged (getIndex()); } +float AudioParameterChoice::getValue() const { return convertTo0to1 (value); } +void AudioParameterChoice::setValue (float newValue) { value = convertFrom0to1 (newValue); valueChanged (getIndex()); } float AudioParameterChoice::getDefaultValue() const { return defaultValue; } int AudioParameterChoice::getNumSteps() const { return choices.size(); } bool AudioParameterChoice::isDiscrete() const { return true; } -float AudioParameterChoice::getValueForText (const String& text) const { return convertTo0to1 (indexFromStringFunction (text)); } -String AudioParameterChoice::getText (float v, int length) const { return stringFromIndexFunction (convertFrom0to1 (v), length); } +float AudioParameterChoice::getValueForText (const String& text) const { return convertTo0to1 ((float) indexFromStringFunction (text)); } +String AudioParameterChoice::getText (float v, int length) const { return stringFromIndexFunction ((int) convertFrom0to1 (v), length); } void AudioParameterChoice::valueChanged (int) {} AudioParameterChoice& AudioParameterChoice::operator= (int newValue) { if (getIndex() != newValue) - setValueNotifyingHost (convertTo0to1 (newValue)); + setValueNotifyingHost (convertTo0to1 ((float) newValue)); return *this; } +#if JUCE_UNIT_TESTS + +static struct SpecialisedAudioParameterTests final : public UnitTest +{ + SpecialisedAudioParameterTests() : UnitTest ("Specialised Audio Parameters", "AudioProcessor parameters") {} + + void runTest() override + { + beginTest ("A choice parameter with three options switches at the correct points"); + { + AudioParameterChoice choice ({}, {}, { "a", "b", "c" }, {}); + + choice.setValueNotifyingHost (0.0f); + expectEquals (choice.getIndex(), 0); + + choice.setValueNotifyingHost (0.2f); + expectEquals (choice.getIndex(), 0); + + choice.setValueNotifyingHost (0.3f); + expectEquals (choice.getIndex(), 1); + + choice.setValueNotifyingHost (0.7f); + expectEquals (choice.getIndex(), 1); + + choice.setValueNotifyingHost (0.8f); + expectEquals (choice.getIndex(), 2); + + choice.setValueNotifyingHost (1.0f); + expectEquals (choice.getIndex(), 2); + } + + beginTest ("An int parameter with three options switches at the correct points"); + { + AudioParameterInt intParam ({}, {}, 1, 3, 1); + + intParam.setValueNotifyingHost (0.0f); + expectEquals (intParam.get(), 1); + + intParam.setValueNotifyingHost (0.2f); + expectEquals (intParam.get(), 1); + + intParam.setValueNotifyingHost (0.3f); + expectEquals (intParam.get(), 2); + + intParam.setValueNotifyingHost (0.7f); + expectEquals (intParam.get(), 2); + + intParam.setValueNotifyingHost (0.8f); + expectEquals (intParam.get(), 3); + + intParam.setValueNotifyingHost (1.0f); + expectEquals (intParam.get(), 3); + } + + + beginTest ("Choice parameters handle out-of-bounds input"); + { + AudioParameterChoice choiceParam ({}, {}, { "a", "b", "c" }, {}); + + choiceParam.setValueNotifyingHost (-0.5f); + expectEquals (choiceParam.getIndex(), 0); + + choiceParam.setValueNotifyingHost (1.5f); + expectEquals (choiceParam.getIndex(), 2); + } + + beginTest ("Int parameters handle out-of-bounds input"); + { + AudioParameterInt intParam ({}, {}, -1, 2, 0); + + intParam.setValueNotifyingHost (-0.5f); + expectEquals (intParam.get(), -1); + + intParam.setValueNotifyingHost (1.5f); + expectEquals (intParam.get(), 2); + + intParam = -5; + expectEquals (intParam.get(), -1); + + intParam = 5; + expectEquals (intParam.get(), 2); + } + } +} specialisedAudioParameterTests; + +#endif + } // namespace juce diff --git a/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp b/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp index e177eb0f39..48686bb218 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp +++ b/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp @@ -27,165 +27,210 @@ namespace juce { -struct AudioProcessorValueTreeState::Parameter : public AudioProcessorParameterWithID, - private ValueTree::Listener +//============================================================================== +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)), + metaParameter (isMetaParameter), + automatable (isAutomatableParameter), + discrete (isDiscrete), + boolean (isBoolean) { - Parameter (AudioProcessorValueTreeState& s, - const String& parameterID, const String& paramName, const String& labelText, - NormalisableRange r, float defaultVal, - std::function valueToText, - std::function textToValue, - bool meta, - bool automatable, - bool discrete, - AudioProcessorParameter::Category paramCategory, - bool boolean) - : AudioProcessorParameterWithID (parameterID, paramName, labelText, paramCategory), - owner (s), valueToTextFunction (valueToText), textToValueFunction (textToValue), - range (r), value (defaultVal), defaultValue (defaultVal), - listenersNeedCalling (true), - isMetaParam (meta), - isAutomatableParam (automatable), - isDiscreteParam (discrete), - isBooleanParam (boolean) +} + + 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) { - value = defaultValue; - state.addListener (this); + parameter.addListener (this); } - ~Parameter() + ~ParameterAdapter() noexcept { 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) { - // should have detached all callbacks before destroying the parameters! - jassert (listeners.size() <= 1); + if (value == unnormalisedValue) + return; + + setNormalisedValue (normalise (value)); } - float getValue() const override { return range.convertTo0to1 (value); } - float getDefaultValue() const override { return range.convertTo0to1 (defaultValue); } - - float getValueForText (const String& text) const override + float getDenormalisedValueForText (const String& text) const { - return range.convertTo0to1 (textToValueFunction != nullptr ? textToValueFunction (text) - : text.getFloatValue()); + return denormalise (parameter.getValueForText (text)); } - String getText (float normalisedValue, int /*length*/) const override + String getTextForDenormalisedValue (float value) const { - auto v = range.convertFrom0to1 (normalisedValue); - return valueToTextFunction != nullptr ? valueToTextFunction (v) - : String (v, 2); + return parameter.getText (normalise (value), 0); } - int getNumSteps() const override + float getDenormalisedValue() const { return unnormalisedValue; } + float& getRawDenormalisedValue() { return unnormalisedValue; } + + bool flushToTree (ValueTree tree, const Identifier& key, UndoManager* um) { - if (range.interval > 0) - return (static_cast ((range.end - range.start) / range.interval) + 1); + auto needsUpdateTestValue = true; - return AudioProcessor::getDefaultNumParameterSteps(); - } + if (! needsUpdate.compare_exchange_strong (needsUpdateTestValue, false)) + return false; - void setValue (float newValue) override - { - newValue = range.snapToLegalValue (range.convertFrom0to1 (newValue)); - - if (value != newValue || listenersNeedCalling) + if (auto valueProperty = tree.getPropertyPointer (key)) { - value = newValue; - - listeners.call ([=] (AudioProcessorValueTreeState::Listener& l) { l.parameterChanged (paramID, value); }); - listenersNeedCalling = false; - - needsUpdate = true; - } - } - - void setNewState (const ValueTree& v) - { - state = v; - updateFromValueTree(); - } - - void setUnnormalisedValue (float newUnnormalisedValue) - { - if (value != newUnnormalisedValue) - { - const float newValue = range.convertTo0to1 (newUnnormalisedValue); - setValueNotifyingHost (newValue); - } - } - - void updateFromValueTree() - { - setUnnormalisedValue (state.getProperty (owner.valuePropertyID, defaultValue)); - } - - void copyValueToValueTree() - { - if (auto* valueProperty = state.getPropertyPointer (owner.valuePropertyID)) - { - if ((float) *valueProperty != value) + if ((float) *valueProperty != unnormalisedValue) { ScopedValueSetter svs (ignoreParameterChangedCallbacks, true); - state.setProperty (owner.valuePropertyID, value, owner.undoManager); + tree.setProperty (key, unnormalisedValue, um); } } else { - state.setProperty (owner.valuePropertyID, value, nullptr); + tree.setProperty (key, unnormalisedValue, nullptr); } + + return true; } - void valueTreePropertyChanged (ValueTree&, const Identifier& property) override +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; - if (property == owner.valuePropertyID) - updateFromValueTree(); + parameter.setValueNotifyingHost (value); } - void valueTreeChildAdded (ValueTree&, ValueTree&) override {} - void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override {} - void valueTreeChildOrderChanged (ValueTree&, int, int) override {} - void valueTreeParentChanged (ValueTree&) override {} - - static Parameter* getParameterForID (AudioProcessor& processor, StringRef paramID) noexcept - { - for (auto* ap : processor.getParameters()) - { - // When using this class, you must allow it to manage all the parameters in your AudioProcessor, and - // not add any parameter objects of other types! - jassert (dynamic_cast (ap) != nullptr); - - auto* p = static_cast (ap); - - if (paramID == p->paramID) - return p; - } - - return nullptr; - } - - bool isMetaParameter() const override { return isMetaParam; } - bool isAutomatable() const override { return isAutomatableParam; } - bool isDiscrete() const override { return isDiscreteParam; } - bool isBoolean() const override { return isBooleanParam; } - - AudioProcessorValueTreeState& owner; - ValueTree state; - ListenerList listeners; - std::function valueToTextFunction; - std::function textToValueFunction; - NormalisableRange range; - float value, defaultValue; + RangedAudioParameter& parameter; + ListenerList listeners; + float unnormalisedValue{}; std::atomic needsUpdate { true }; - bool listenersNeedCalling; - const bool isMetaParam, isAutomatableParam, isDiscreteParam, isBooleanParam; - bool ignoreParameterChangedCallbacks = false; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Parameter) + bool listenersNeedCalling { true }, ignoreParameterChangedCallbacks { false }; }; //============================================================================== +AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& processorToConnectTo, + UndoManager* undoManagerToUse, + const juce::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->parameters.emplace_back (std::make_unique (*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->parameters.emplace_back (std::make_unique (*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) { @@ -195,80 +240,98 @@ AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& p, U AudioProcessorValueTreeState::~AudioProcessorValueTreeState() {} -std::unique_ptr AudioProcessorValueTreeState::createParameter (const String& paramID, const String& paramName, - const String& labelText, NormalisableRange r, - float defaultVal, std::function valueToTextFunction, - std::function textToValueFunction, - bool isMetaParameter, - bool isAutomatableParameter, - bool isDiscreteParameter, - AudioProcessorParameter::Category category, - bool isBooleanParameter) +//============================================================================== +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()); - return std::unique_ptr (new Parameter (*this, paramID, paramName, labelText, r, - defaultVal, valueToTextFunction, textToValueFunction, - isMetaParameter, isAutomatableParameter, - isDiscreteParameter, category, isBooleanParameter)); + if (getParameter (param->paramID) != nullptr) + return nullptr; + + parameters.emplace_back (std::make_unique (*param)); + processor.addParameter (param.get()); + + return param.release(); } -AudioProcessorParameterWithID* AudioProcessorValueTreeState::createAndAddParameter (const String& paramID, const String& paramName, - const String& labelText, NormalisableRange r, - float defaultVal, std::function valueToTextFunction, - std::function textToValueFunction, - bool isMetaParameter, - bool isAutomatableParameter, - bool isDiscreteParameter, - AudioProcessorParameter::Category category, - bool isBooleanParameter) +//============================================================================== +AudioProcessorValueTreeState::ParameterAdapter* AudioProcessorValueTreeState::getParameterAdapter (StringRef paramID) const { - auto* p = createParameter (paramID, paramName, labelText, r, defaultVal, - valueToTextFunction, textToValueFunction, - isMetaParameter, isAutomatableParameter, isDiscreteParameter, - category, isBooleanParameter).release(); - processor.addParameter (p); - return p; + auto it = find_if (std::begin (parameters), std::end (parameters), + [&](const std::unique_ptr& p) { return p->getParameter().paramID == paramID; }); + return it == std::end (parameters) ? nullptr : it->get(); } void AudioProcessorValueTreeState::addParameterListener (StringRef paramID, Listener* listener) { - if (auto* p = Parameter::getParameterForID (processor, paramID)) - p->listeners.add (listener); + if (auto* p = getParameterAdapter (paramID)) + p->addListener (listener); } void AudioProcessorValueTreeState::removeParameterListener (StringRef paramID, Listener* listener) { - if (auto* p = Parameter::getParameterForID (processor, paramID)) - p->listeners.remove (listener); + if (auto* p = getParameterAdapter (paramID)) + p->removeListener (listener); } Value AudioProcessorValueTreeState::getParameterAsValue (StringRef paramID) const { - if (auto* p = Parameter::getParameterForID (processor, paramID)) - return p->state.getPropertyAsValue (valuePropertyID, undoManager); + auto v = getChildValueTree (paramID); + + if (v.isValid()) + return v.getPropertyAsValue (valuePropertyID, undoManager); return {}; } NormalisableRange AudioProcessorValueTreeState::getParameterRange (StringRef paramID) const noexcept { - if (auto* p = Parameter::getParameterForID (processor, paramID)) - return p->range; + if (auto* p = getParameterAdapter (paramID)) + return p->getRange(); - return NormalisableRange(); + return {}; } -AudioProcessorParameterWithID* AudioProcessorValueTreeState::getParameter (StringRef paramID) const noexcept +RangedAudioParameter* AudioProcessorValueTreeState::getParameter (StringRef paramID) const noexcept { - return Parameter::getParameterForID (processor, paramID); + if (auto adapter = getParameterAdapter (paramID)) + return &adapter->getParameter(); + + return nullptr; } float* AudioProcessorValueTreeState::getRawParameterValue (StringRef paramID) const noexcept { - if (auto* p = Parameter::getParameterForID (processor, paramID)) - return &(p->value); + if (auto* p = getParameterAdapter (paramID)) + return &p->getRawDenormalisedValue(); return nullptr; } @@ -276,9 +339,7 @@ float* AudioProcessorValueTreeState::getRawParameterValue (StringRef paramID) co ValueTree AudioProcessorValueTreeState::copyState() { ScopedLock lock (valueTreeChanging); - flushParameterValuesToValueTree(); - return state.createCopy(); } @@ -292,9 +353,14 @@ void AudioProcessorValueTreeState::replaceState (const ValueTree& newState) undoManager->clearUndoHistory(); } +ValueTree AudioProcessorValueTreeState::getChildValueTree (const String& paramID) const +{ + return { state.getChildWithProperty (idPropertyID, paramID) }; +} + ValueTree AudioProcessorValueTreeState::getOrCreateChildValueTree (const String& paramID) { - ValueTree v (state.getChildWithProperty (idPropertyID, paramID)); + auto v = getChildValueTree (paramID); if (! v.isValid()) { @@ -306,37 +372,44 @@ ValueTree AudioProcessorValueTreeState::getOrCreateChildValueTree (const String& return v; } +void AudioProcessorValueTreeState::setNewState (ParameterAdapter& p) +{ + const auto tree = getOrCreateChildValueTree (p.getParameter().paramID); + p.setDenormalisedValue (tree.getProperty (valuePropertyID, p.getDenormalisedDefaultValue())); +} + void AudioProcessorValueTreeState::updateParameterConnectionsToChildTrees() { ScopedLock lock (valueTreeChanging); - for (auto* param : processor.getParameters()) - { - jassert (dynamic_cast (param) != nullptr); - auto* p = static_cast (param); - - p->setNewState (getOrCreateChildValueTree (p->paramID)); - } + for (const auto& p : parameters) + setNewState (*p); } void AudioProcessorValueTreeState::valueTreePropertyChanged (ValueTree& tree, const Identifier& property) { - if (property == idPropertyID && tree.hasType (valueType) && tree.getParent() == state) + if (! (tree.hasType (valueType) && tree.getParent() == state)) + return; + + if (property == idPropertyID) updateParameterConnectionsToChildTrees(); + else if (property == valuePropertyID) + if (auto adapter = getParameterAdapter (tree.getProperty (idPropertyID).toString())) + adapter->setDenormalisedValue (tree.getProperty (valuePropertyID)); } void AudioProcessorValueTreeState::valueTreeChildAdded (ValueTree& parent, ValueTree& tree) { if (parent == state && tree.hasType (valueType)) - if (auto* param = Parameter::getParameterForID (processor, tree.getProperty (idPropertyID).toString())) - param->setNewState (getOrCreateChildValueTree (param->paramID)); + if (auto* param = getParameterAdapter (tree.getProperty (idPropertyID).toString())) + setNewState (*param); } void AudioProcessorValueTreeState::valueTreeChildRemoved (ValueTree& parent, ValueTree& tree, int) { if (parent == state && tree.hasType (valueType)) - if (auto* param = Parameter::getParameterForID (processor, tree.getProperty (idPropertyID).toString())) - param->setNewState (getOrCreateChildValueTree (param->paramID)); + if (auto* param = getParameterAdapter (tree.getProperty (idPropertyID).toString())) + setNewState (*param); } void AudioProcessorValueTreeState::valueTreeRedirected (ValueTree& v) @@ -352,23 +425,14 @@ bool AudioProcessorValueTreeState::flushParameterValuesToValueTree() { ScopedLock lock (valueTreeChanging); - bool anythingUpdated = false; - - for (auto* ap : processor.getParameters()) - { - jassert (dynamic_cast (ap) != nullptr); - auto* p = static_cast (ap); - - bool needsUpdateTestValue = true; - - if (p->needsUpdate.compare_exchange_strong (needsUpdateTestValue, false)) - { - p->copyValueToValueTree(); - anythingUpdated = true; - } - } - - return anythingUpdated; + return std::accumulate (std::begin (parameters), std::end (parameters), + false, + [this](bool anyUpdated, std::unique_ptr& ap) { + return ap->flushToTree (getChildValueTree (ap->getParameter().paramID), + valuePropertyID, + undoManager) + || anyUpdated; + }); } void AudioProcessorValueTreeState::timerCallback() @@ -379,9 +443,6 @@ void AudioProcessorValueTreeState::timerCallback() : jlimit (50, 500, getTimerInterval() + 20)); } -AudioProcessorValueTreeState::Listener::Listener() {} -AudioProcessorValueTreeState::Listener::~Listener() {} - //============================================================================== struct AttachedControlBase : public AudioProcessorValueTreeState::Listener, public AsyncUpdater @@ -397,12 +458,12 @@ struct AttachedControlBase : public AudioProcessorValueTreeState::Listener, state.removeParameterListener (paramID, this); } - void setNewUnnormalisedValue (float newUnnormalisedValue) + void setNewDenormalisedValue (float newDenormalisedValue) { if (auto* p = state.getParameter (paramID)) { const float newValue = state.getParameterRange (paramID) - .convertTo0to1 (newUnnormalisedValue); + .convertTo0to1 (newDenormalisedValue); if (p->getValue() != newValue) p->setValueNotifyingHost (newValue); @@ -477,48 +538,45 @@ struct AudioProcessorValueTreeState::SliderAttachment::Pimpl : private Attached } else { - auto convertFrom0To1Function = [range] (double currentRangeStart, - double currentRangeEnd, - double normalisedValue) mutable + 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 + 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 + 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, + slider.setNormalisableRange ({ (double) range.start, + (double) range.end, convertFrom0To1Function, convertTo0To1Function, snapToLegalValueFunction }); } - if (auto* param = dynamic_cast (state.getParameter (paramID))) + if (auto* param = state.getParameterAdapter (paramID)) { - if (param->textToValueFunction != nullptr) - slider.valueFromTextFunction = [param] (const String& text) { return (double) param->textToValueFunction (text); }; - - if (param->valueToTextFunction != nullptr) - slider.textFromValueFunction = [param] (double value) { return param->valueToTextFunction ((float) value); }; - - slider.setDoubleClickReturnValue (true, range.convertFrom0to1 (param->getDefaultValue())); + 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())); } sendInitialUpdate(); @@ -546,11 +604,11 @@ struct AudioProcessorValueTreeState::SliderAttachment::Pimpl : private Attached const ScopedLock selfCallbackLock (selfCallbackMutex); if ((! ignoreCallbacks) && (! ModifierKeys::currentModifiers.isRightButtonDown())) - setNewUnnormalisedValue ((float) s->getValue()); + setNewDenormalisedValue ((float) s->getValue()); } - void sliderDragStarted (Slider*) override { beginParameterChange(); } - void sliderDragEnded (Slider*) override { endParameterChange(); } + void sliderDragStarted (Slider*) override { beginParameterChange(); } + void sliderDragEnded (Slider*) override { endParameterChange(); } Slider& slider; bool ignoreCallbacks; @@ -669,7 +727,7 @@ struct AudioProcessorValueTreeState::ButtonAttachment::Pimpl : private Attached if (! ignoreCallbacks) { beginParameterChange(); - setNewUnnormalisedValue (b->getToggleState() ? 1.0f : 0.0f); + setNewDenormalisedValue (b->getToggleState() ? 1.0f : 0.0f); endParameterChange(); } } @@ -688,4 +746,413 @@ AudioProcessorValueTreeState::ButtonAttachment::ButtonAttachment (AudioProcessor 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, juce::String expected) + { + AudioParameterFloat param ({}, {}, range, {}, {}); + AudioProcessorValueTreeState::ParameterAdapter adapter (param); + + expectEquals (adapter.getTextForDenormalisedValue (value), expected); + }; + + test ({ -100, 100 }, 0, "0.0"); + test ({ -2.5, 12.5 }, 10, "10.0"); + test ({ -20, -10 }, -15, "-15.0"); + test ({ 0, 7.5 }, 2.5, "2.5"); + } + + beginTest ("Text can be converted to floats"); + { + const auto test = [&](NormalisableRange range, juce::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 ("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"; + auto param = proc.state.createAndAddParameter (std::make_unique (key, String(), String(), NormalisableRange(), + 0.0f, nullptr, nullptr)); + proc.state.state = ValueTree { "state" }; + + const auto newValue = 0.75f; + auto value = proc.state.getParameterAsValue (key); + 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 diff --git a/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h b/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h index 7abf65da19..52b5c84d05 100644 --- a/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h +++ b/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h @@ -28,9 +28,9 @@ namespace juce { /** - This class contains a ValueTree which is used to manage an AudioProcessor's entire state. + This class contains a ValueTree that is used to manage an AudioProcessor's entire state. - It has its own internal class of parameter object which are linked to values + It has its own internal class of parameter object that is linked to values within its ValueTree, and which are each identified by a string ID. You can get access to the underlying ValueTree object via the state member variable, @@ -39,127 +39,231 @@ namespace juce It also provides some utility child classes for connecting parameters directly to GUI controls like sliders. - To use: + The favoured constructor of this class takes a collection of RangedAudioParameters or + AudioProcessorParameterGroups of RangedAudioParameters and adds them to the attached + AudioProcessor directly. + + The deprecated way of using this class is as follows: + 1) Create an AudioProcessorValueTreeState, and give it some parameters using createAndAddParameter(). 2) Initialise the state member variable with a type name. + The deprecated constructor will be removed from the API in a future version of JUCE! + @tags{Audio} */ -class JUCE_API AudioProcessorValueTreeState : private Timer, +class JUCE_API AudioProcessorValueTreeState : private Timer, private ValueTree::Listener { public: - /** Creates a state object for a given processor. + //============================================================================== + /** A class to contain a set of RangedAudioParameters and AudioProcessorParameterGroups + containing RangedAudioParameters. - The UndoManager is optional and can be a nullptr. - After creating your state object, you should add parameters with the - createAndAddParameter() method. Note that each AudioProcessorValueTreeState - should be attached to only one processor, and must have the same lifetime as the - processor, as they will have dependencies on each other. + This class is used in the AudioProcessorValueTreeState constructor to allow + arbitrarily grouped RangedAudioParameters to be passed to an AudioProcessor. + */ + class JUCE_API ParameterLayout final + { + private: + //============================================================================== + template + using ValidIfIterator = decltype (std::next (std::declval())); + + public: + //============================================================================== + template + ParameterLayout (std::unique_ptr... items) { add (std::move (items)...); } + + template > + ParameterLayout (It begin, It end) { add (begin, end); } + + template + void add (std::unique_ptr... items) + { + parameters.reserve (parameters.size() + sizeof... (items)); + + // We can replace this with some nicer code once generic lambdas become available. A + // sequential context like an array initialiser is required to ensure we get the correct + // order from the parameter pack. + int unused[] { (parameters.emplace_back (MakeContents() (std::move (items))), 0)... }; + ignoreUnused (unused); + } + + template > + void add (It begin, It end) + { + parameters.reserve (parameters.size() + std::size_t (std::distance (begin, end))); + std::transform (std::make_move_iterator (begin), + std::make_move_iterator (end), + std::back_inserter (parameters), + MakeContents()); + } + + ParameterLayout (const ParameterLayout& other) = delete; + ParameterLayout (ParameterLayout&& other) noexcept { swap (other); } + + ParameterLayout& operator= (const ParameterLayout& other) = delete; + ParameterLayout& operator= (ParameterLayout&& other) noexcept { swap (other); return *this; } + + void swap (ParameterLayout& other) noexcept { std::swap (other.parameters, parameters); } + + private: + //============================================================================== + struct Visitor + { + virtual ~Visitor() = default; + + // If you have a compiler error telling you that there is no matching + // member function to call for 'visit', then you are probably attempting + // to add a parameter that is not derived from RangedAudioParameter to + // the AudioProcessorValueTreeState. + virtual void visit (std::unique_ptr) const = 0; + virtual void visit (std::unique_ptr) const = 0; + }; + + struct ParameterStorageBase + { + virtual ~ParameterStorageBase() = default; + virtual void accept (const Visitor& visitor) = 0; + }; + + template + struct ParameterStorage : ParameterStorageBase + { + explicit ParameterStorage (std::unique_ptr input) : contents (std::move (input)) {} + + void accept (const Visitor& visitor) override { visitor.visit (std::move (contents)); } + + std::unique_ptr contents; + }; + + struct MakeContents final + { + template + std::unique_ptr operator() (std::unique_ptr item) const + { + return std::unique_ptr (new ParameterStorage (std::move (item))); + } + }; + + void add() {} + + friend class AudioProcessorValueTreeState; + + std::vector> parameters; + }; + + //============================================================================== + /** Creates a state object for a given processor, and sets up all the parameters + that will control that processor. + + You should *not* assign a new ValueTree to the state, or call + createAndAddParameter, after using this constructor. + + Note that each AudioProcessorValueTreeState should be attached to only one + processor, and must have the same lifetime as the processor, as they will + have dependencies on each other. + + The ParameterLayout parameter has a set of constructors that allow you to + add multiple RangedAudioParameters and AudioProcessorParameterGroups containing + RangedAudioParameters to the AudioProcessorValueTreeState inside this constructor. + + @code + YourAudioProcessor() + : apvts (*this, &undoManager, "PARAMETERS", + { std::make_unique ("a", "Parameter A", NormalisableRange (-100.0f, 100.0f), 0), + std::make_unique ("b", "Parameter B", 0, 5, 2) }) + @endcode + + @param processorToConnectTo The Processor that will be managed by this object + @param undoManagerToUse An optional UndoManager to use; pass nullptr if no UndoManager is required + @param valueTreeType The identifier used to initialise the internal ValueTree + @param parameterLayout An object that holds all parameters and parameter groups that the + AudioProcessor should use. */ AudioProcessorValueTreeState (AudioProcessor& processorToConnectTo, - UndoManager* undoManagerToUse); + UndoManager* undoManagerToUse, + const juce::Identifier& valueTreeType, + ParameterLayout parameterLayout); + + /** This constructor is discouraged and will be deprecated in a future version of JUCE! + + Creates a state object for a given processor. + + The UndoManager is optional and can be a nullptr. After creating your state object, + you should add parameters with the createAndAddParameter() method. Note that each + AudioProcessorValueTreeState should be attached to only one processor, and must have + the same lifetime as the processor, as they will have dependencies on each other. + */ + AudioProcessorValueTreeState (AudioProcessor& processorToConnectTo, UndoManager* undoManagerToUse); /** Destructor. */ ~AudioProcessorValueTreeState(); - /** Creates a new parameter object for controlling a parameter with the given ID. + //============================================================================== + /** This function is deprecated and will be removed in a future version of JUCE! - The parameter returned by this function should be added to the AudioProcessor - managed by this AudioProcessorValueTreeState via AudioProcessor::addParameter - or AudioProcessor::addParameterGroup. + Previous calls to + @code + createAndAddParameter (paramID1, paramName1, ...); + @endcode + can be replaced with + @code + using Parameter = AudioProcessorValueTreeState::Parameter; + createAndAddParameter (std::make_unique (paramID1, paramName1, ...)); + @endcode - @param parameterID A unique string ID for the new parameter - @param parameterName The name that the parameter will return from AudioProcessorParameter::getName() - @param labelText The label that the parameter will return from AudioProcessorParameter::getLabel() - @param valueRange A mapping that will be used to determine the value range which this parameter uses - @param defaultValue A default value for the parameter (in non-normalised units) - @param valueToTextFunction A function that will convert a non-normalised value to a string for the - AudioProcessorParameter::getText() method. This can be nullptr to use the - default implementation - @param textToValueFunction The inverse of valueToTextFunction - @param isMetaParameter Set this value to true if this should be a meta parameter - @param isAutomatableParameter Set this value to false if this parameter should not be automatable - @param isDiscrete Set this value to true to make this parameter take discrete values in a host. - @see AudioProcessorParameter::isDiscrete - @param category Which category the parameter should use. - @see AudioProcessorParameter::Category - @param isBoolean Set this value to true to make this parameter appear as a boolean toggle in - a hosts view of your plug-ins parameters - @see AudioProcessorParameter::isBoolean + However, a much better approach is to use the AudioProcessorValueTreeState + constructor directly + @code + using Parameter = AudioProcessorValueTreeState::Parameter; + YourAudioProcessor() + : apvts (*this, &undoManager, "PARAMETERS", { std::make_unique (paramID1, paramName1, ...), + std::make_unique (paramID2, paramName2, ...), + ... }) + @endcode - @returns the parameter object that was created - */ - std::unique_ptr createParameter (const String& parameterID, - const String& parameterName, - const String& labelText, - NormalisableRange valueRange, - float defaultValue, - std::function valueToTextFunction, - std::function textToValueFunction, - bool isMetaParameter = false, - bool isAutomatableParameter = true, - bool isDiscrete = false, - AudioProcessorParameter::Category category - = AudioProcessorParameter::genericParameter, - bool isBoolean = false); - - /** Creates and returns a new parameter object for controlling a parameter - with the given ID. + This function creates and returns a new parameter object for controlling a + parameter with the given ID. Calling this will create and add a special type of AudioProcessorParameter to the AudioProcessor to which this state is attached. - - @param parameterID A unique string ID for the new parameter - @param parameterName The name that the parameter will return from AudioProcessorParameter::getName() - @param labelText The label that the parameter will return from AudioProcessorParameter::getLabel() - @param valueRange A mapping that will be used to determine the value range which this parameter uses - @param defaultValue A default value for the parameter (in non-normalised units) - @param valueToTextFunction A function that will convert a non-normalised value to a string for the - AudioProcessorParameter::getText() method. This can be nullptr to use the - default implementation - @param textToValueFunction The inverse of valueToTextFunction - @param isMetaParameter Set this value to true if this should be a meta parameter - @param isAutomatableParameter Set this value to false if this parameter should not be automatable - @param isDiscrete Set this value to true to make this parameter take discrete values in a host. - @see AudioProcessorParameter::isDiscrete - @param category Which category the parameter should use. - @see AudioProcessorParameter::Category - @param isBoolean Set this value to true to make this parameter appear as a boolean toggle in - a hosts view of your plug-ins parameters - @see AudioProcessorParameter::isBoolean - - @returns the parameter object that was created */ - AudioProcessorParameterWithID* createAndAddParameter (const String& parameterID, - const String& parameterName, - const String& labelText, - NormalisableRange valueRange, - float defaultValue, - std::function valueToTextFunction, - std::function textToValueFunction, - bool isMetaParameter = false, - bool isAutomatableParameter = true, - bool isDiscrete = false, - AudioProcessorParameter::Category category - = AudioProcessorParameter::genericParameter, - bool isBoolean = false); + JUCE_DEPRECATED (RangedAudioParameter* createAndAddParameter (const String& parameterID, + const String& parameterName, + const String& labelText, + NormalisableRange valueRange, + float defaultValue, + std::function valueToTextFunction, + std::function textToValueFunction, + bool isMetaParameter = false, + bool isAutomatableParameter = true, + bool isDiscrete = false, + AudioProcessorParameter::Category category = AudioProcessorParameter::genericParameter, + bool isBoolean = false)); + /** This function adds a parameter to the attached AudioProcessor and that parameter will + be managed by this AudioProcessorValueTreeState object. + */ + RangedAudioParameter* createAndAddParameter (std::unique_ptr parameter); + + //============================================================================== /** Returns a parameter by its ID string. */ - AudioProcessorParameterWithID* getParameter (StringRef parameterID) const noexcept; + RangedAudioParameter* getParameter (StringRef parameterID) const noexcept; /** Returns a pointer to a floating point representation of a particular parameter which a realtime process can read to find out its current value. */ float* getRawParameterValue (StringRef parameterID) const noexcept; + //============================================================================== /** A listener class that can be attached to an AudioProcessorValueTreeState. Use AudioProcessorValueTreeState::addParameterListener() to register a callback. */ struct JUCE_API Listener { - Listener(); - virtual ~Listener(); + virtual ~Listener() = default; /** This callback method is called by the AudioProcessorValueTreeState when a parameter changes. */ virtual void parameterChanged (const String& parameterID, float newValue) = 0; @@ -171,12 +275,14 @@ public: /** Removes a callback that was previously added with addParameterCallback(). */ void removeParameterListener (StringRef parameterID, Listener* listener); + //============================================================================== /** Returns a Value object that can be used to control a particular parameter. */ Value getParameterAsValue (StringRef parameterID) const; /** Returns the range that was set when the given parameter was created. */ NormalisableRange getParameterRange (StringRef parameterID) const noexcept; + //============================================================================== /** Returns a copy of the state value tree. The AudioProcessorValueTreeState's ValueTree is updated internally on the @@ -204,6 +310,7 @@ public: */ void replaceState (const ValueTree& newState); + //============================================================================== /** A reference to the processor with which this state is associated. */ AudioProcessor& processor; @@ -219,6 +326,57 @@ public: /** Provides access to the undo manager that this object is using. */ UndoManager* const undoManager; + //============================================================================== + /** A parameter class that maintains backwards compatibility with deprecated + AudioProcessorValueTreeState functionality. + + Previous calls to + @code + createAndAddParameter (paramID1, paramName1, ...); + @endcode + can be replaced with + @code + using Parameter = AudioProcessorValueTreeState::Parameter; + createAndAddParameter (std::make_unique (paramID1, paramName1, ...)); + @endcode + + However, a much better approach is to use the AudioProcessorValueTreeState + constructor directly + @code + using Parameter = AudioProcessorValueTreeState::Parameter; + YourAudioProcessor() + : apvts (*this, &undoManager, "PARAMETERS", { std::make_unique (paramID1, paramName1, ...), + std::make_unique (paramID2, paramName2, ...), + ... }) + @endcode + */ + class Parameter final : public AudioParameterFloat + { + public: + Parameter (const String& parameterID, + const String& parameterName, + const String& labelText, + NormalisableRange valueRange, + float defaultValue, + std::function valueToTextFunction, + std::function textToValueFunction, + bool isMetaParameter = false, + bool isAutomatableParameter = true, + bool isDiscrete = false, + AudioProcessorParameter::Category category = AudioProcessorParameter::genericParameter, + bool isBoolean = false); + + int getNumSteps() const override; + + bool isMetaParameter() const override; + bool isAutomatable() const override; + bool isDiscrete() const override; + bool isBoolean() const override; + + private: + const bool metaParameter, automatable, discrete, boolean; + }; + //============================================================================== /** An object of this class maintains a connection between a Slider and a parameter in an AudioProcessorValueTreeState. @@ -295,11 +453,44 @@ public: private: //============================================================================== - struct Parameter; - friend struct Parameter; + /** This method was introduced to allow you to use AudioProcessorValueTreeState parameters in + an AudioProcessorParameterGroup, but there is now a much nicer way to achieve this. + Code that looks like this + @code + auto paramA = apvts.createParameter ("a", "Parameter A", {}, { -100, 100 }, ...); + auto paramB = apvts.createParameter ("b", "Parameter B", {}, { 0, 5 }, ...); + addParameterGroup (std::make_unique ("g1", "Group 1", " | ", std::move (paramA), std::move (paramB))); + apvts.state = ValueTree (Identifier ("PARAMETERS")); + @endcode + can instead create the APVTS like this, avoiding the two-step initialization process and leveraging one of JUCE's + pre-built parameter types (or your own custom type derived from RangedAudioParameter) + @code + using Parameter = AudioProcessorValueTreeState::Parameter; + YourAudioProcessor() + : apvts (*this, &undoManager, "PARAMETERS", + { std::make_unique ("g1", "Group 1", " | ", + std::make_unique ("a", "Parameter A", "", NormalisableRange (-100, 100), ...), + std::make_unique ("b", "Parameter B", "", NormalisableRange (0, 5), ...)) }) + @endcode + */ + JUCE_DEPRECATED (std::unique_ptr createParameter (const String&, const String&, const String&, NormalisableRange, + float, std::function, std::function, + bool, bool, bool, AudioProcessorParameter::Category, bool)); + + //============================================================================== + class ParameterAdapter; + + #if JUCE_UNIT_TESTS + friend struct ParameterAdapterTests; + #endif + + ParameterAdapter* getParameterAdapter (StringRef) const; + + ValueTree getChildValueTree (const String&) const; ValueTree getOrCreateChildValueTree (const String&); bool flushParameterValuesToValueTree(); + void setNewState (ParameterAdapter&); void timerCallback() override; void valueTreePropertyChanged (ValueTree&, const Identifier&) override; @@ -310,9 +501,9 @@ private: void valueTreeRedirected (ValueTree&) override; void updateParameterConnectionsToChildTrees(); - Identifier valueType { "PARAM" }, - valuePropertyID { "value" }, - idPropertyID { "id" }; + const Identifier valueType { "PARAM" }, valuePropertyID { "value" }, idPropertyID { "id" }; + + std::vector> parameters; CriticalSection valueTreeChanging; diff --git a/modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h b/modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h new file mode 100644 index 0000000000..244105ad77 --- /dev/null +++ b/modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h @@ -0,0 +1,81 @@ +/* + ============================================================================== + + 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 +{ + +/** + This abstract base class is used by some AudioProcessorParameter helper classes. + + @see AudioParameterFloat, AudioParameterInt, AudioParameterBool, AudioParameterChoice + + @tags{Audio} +*/ +class JUCE_API RangedAudioParameter : public AudioProcessorParameterWithID +{ +public: + /** The creation of this object requires providing a name and ID which will be + constant for its lifetime. + */ + RangedAudioParameter (const String& parameterID, + const String& name, + const String& label = {}, + Category category = AudioProcessorParameter::genericParameter) + : AudioProcessorParameterWithID (parameterID, name, label, category) {} + + /** Returns the range of values that the parameter can take. */ + virtual const NormalisableRange& getNormalisableRange() const = 0; + + /** Returns the number of steps for this parameter based on the normalisable range's interval. + If you are using lambda functions to define the normalisable range's snapping behaviour + then you should override this function so that it returns the number of snapping points. + */ + int getNumSteps() const override + { + const auto& range = getNormalisableRange(); + + if (range.interval > 0) + return (static_cast ((range.end - range.start) / range.interval) + 1); + + return AudioProcessor::getDefaultNumParameterSteps(); + } + + /** Normalises and snaps a value based on the normalisable range. */ + float convertTo0to1 (float v) const noexcept + { + const auto& range = getNormalisableRange(); + return range.convertTo0to1 (range.snapToLegalValue (v)); + } + + /** Denormalises and snaps a value based on the normalisable range. */ + float convertFrom0to1 (float v) const noexcept + { + const auto& range = getNormalisableRange(); + return range.snapToLegalValue (range.convertFrom0to1 (jlimit (0.0f, 1.0f, v))); + } +}; + +} // namespace juce