diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h b/source/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h new file mode 100644 index 000000000..740f95c7e --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioParameterBool.h @@ -0,0 +1,63 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +/** + Provides a class of AudioProcessorParameter that can be used as a boolean value. + + @see AudioParameterFloat, AudioParameterInt, AudioParameterChoice +*/ +class JUCE_API AudioParameterBool : public AudioProcessorParameterWithID +{ +public: + /** Creates a AudioParameterBool with an ID and name. + On creation, its value is set to the default value. + */ + AudioParameterBool (String parameterID, String name, bool defaultValue); + + /** Destructor. */ + ~AudioParameterBool(); + + /** 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); + + +private: + //============================================================================== + float value, defaultValue; + + float getValue() const override; + void setValue (float newValue) override; + float getDefaultValue() const override; + int getNumSteps() const override; + String getText (float, int) const override; + float getValueForText (const String&) const override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioParameterBool) +}; diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h b/source/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h new file mode 100644 index 000000000..3ad01a88a --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioParameterChoice.h @@ -0,0 +1,78 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +/** + Provides a class of AudioProcessorParameter that can be used to select + an indexed, named choice from a list. + + @see AudioParameterFloat, AudioParameterInt, AudioParameterBool +*/ +class JUCE_API AudioParameterChoice : public AudioProcessorParameterWithID +{ +public: + /** Creates a AudioParameterChoice with an ID, name, and list of items. + On creation, its value is set to the default index. + */ + AudioParameterChoice (String parameterID, String name, + const StringArray& choices, + int defaultItemIndex); + + /** Destructor. */ + ~AudioParameterChoice(); + + /** 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); + + /** Provides access to the list of choices that this parameter is working with. */ + const StringArray choices; + + +private: + //============================================================================== + float value, defaultValue; + + float getValue() const override; + void setValue (float newValue) override; + float getDefaultValue() const override; + int getNumSteps() const override; + 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; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioParameterChoice) +}; diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h b/source/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h new file mode 100644 index 000000000..58498d4a8 --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioParameterFloat.h @@ -0,0 +1,79 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +/** + A subclass of AudioProcessorParameter that provides an easy way to create a + parameter which maps onto a given NormalisableRange. + + @see AudioParameterInt, AudioParameterBool, AudioParameterChoice +*/ +class JUCE_API AudioParameterFloat : public AudioProcessorParameterWithID +{ +public: + /** Creates a AudioParameterFloat with an ID, name, and range. + On creation, its value is set to the default value. + */ + AudioParameterFloat (String parameterID, String name, + NormalisableRange normalisableRange, + float defaultValue); + + /** Creates a AudioParameterFloat with an ID, name, and range. + On creation, its value is set to the default value. + For control over skew factors, you can use the other + constructor and provide a NormalisableRange. + */ + AudioParameterFloat (String parameterID, String name, + float minValue, + float maxValue, + float defaultValue); + + /** Destructor. */ + ~AudioParameterFloat(); + + /** 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); + + /** Provides access to the parameter's range. */ + NormalisableRange range; + + +private: + //============================================================================== + float value, defaultValue; + + float getValue() const override; + void setValue (float newValue) override; + float getDefaultValue() const override; + int getNumSteps() const override; + String getText (float, int) const override; + float getValueForText (const String&) const override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioParameterFloat) +}; diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h b/source/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h new file mode 100644 index 000000000..aa7e38f2d --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioParameterInt.h @@ -0,0 +1,77 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +/** + Provides a class of AudioProcessorParameter that can be used as an + integer value with a given range. + + @see AudioParameterFloat, AudioParameterBool, AudioParameterChoice +*/ +class JUCE_API AudioParameterInt : public AudioProcessorParameterWithID +{ +public: + /** Creates an AudioParameterInt with an ID, name, and range. + Note that the min and max range values are inclusive. + On creation, its value is set to the default value. + */ + AudioParameterInt (String parameterID, String name, + int minValue, int maxValue, + int defaultValue); + + /** Destructor. */ + ~AudioParameterInt(); + + /** 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(); } + + /** Changes the parameter's current value to a new integer. + The value passed in will be snapped to the permitted range before being used. + */ + AudioParameterInt& operator= (int newValue); + + /** Returns the parameter's range. */ + Range getRange() const noexcept { return Range (minValue, maxValue); } + + +private: + //============================================================================== + int minValue, maxValue; + float value, defaultValue; + + float getValue() const override; + void setValue (float newValue) override; + float getDefaultValue() const override; + int getNumSteps() const override; + 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; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioParameterInt) +}; diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h new file mode 100644 index 000000000..4687e18b1 --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameterWithID.h @@ -0,0 +1,55 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +/** + This abstract base class is used by some AudioProcessorParameter helper classes. + + @see AudioParameterFloat, AudioParameterInt, AudioParameterBool, AudioParameterChoice +*/ +class JUCE_API AudioProcessorParameterWithID : public AudioProcessorParameter +{ +public: + /** Creation of this object requires providing a name and ID which will be + constant for its lifetime. + */ + AudioProcessorParameterWithID (String parameterID, String name); + + /** Destructor. */ + ~AudioProcessorParameterWithID(); + + /** Provides access to the parameter's ID string. */ + const String paramID; + + /** Provides access to the parameter's name. */ + const String name; + +private: + String label; + + String getName (int) const override; + String getLabel() const override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorParameterWithID) +}; diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp new file mode 100644 index 000000000..4ee06b8b9 --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorParameters.cpp @@ -0,0 +1,157 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +// This file contains the implementations of the various AudioParameter[XYZ] classes.. + + +AudioProcessorParameterWithID::AudioProcessorParameterWithID (String pid, String nm) : paramID (pid), name (nm) {} +AudioProcessorParameterWithID::~AudioProcessorParameterWithID() {} + +String AudioProcessorParameterWithID::getName (int maximumStringLength) const { return name.substring (0, maximumStringLength); } +String AudioProcessorParameterWithID::getLabel() const { return label; } + + +//============================================================================== +AudioParameterFloat::AudioParameterFloat (String pid, String nm, NormalisableRange r, float def) + : AudioProcessorParameterWithID (pid, nm), range (r), value (def), defaultValue (def) +{ +} + +AudioParameterFloat::AudioParameterFloat (String pid, String nm, float minValue, float maxValue, float def) + : AudioProcessorParameterWithID (pid, nm), range (minValue, maxValue), value (def), defaultValue (def) +{ +} + +AudioParameterFloat::~AudioParameterFloat() {} + +float AudioParameterFloat::getValue() const { return range.convertTo0to1 (value); } +void AudioParameterFloat::setValue (float newValue) { value = range.convertFrom0to1 (newValue); } +float AudioParameterFloat::getDefaultValue() const { return range.convertTo0to1 (defaultValue); } +int AudioParameterFloat::getNumSteps() const { return AudioProcessorParameterWithID::getNumSteps(); } +float AudioParameterFloat::getValueForText (const String& text) const { return range.convertTo0to1 (text.getFloatValue()); } +String AudioParameterFloat::getText (float v, int length) const { return String (range.convertFrom0to1 (v), 2).substring (0, length); } + +AudioParameterFloat& AudioParameterFloat::operator= (float newValue) +{ + const float normalisedValue = range.convertTo0to1 (newValue); + + if (value != normalisedValue) + setValueNotifyingHost (normalisedValue); + + return *this; +} + +//============================================================================== +AudioParameterInt::AudioParameterInt (String pid, String nm, int mn, int mx, int def) + : AudioProcessorParameterWithID (pid, nm), + minValue (mn), maxValue (mx), + value ((float) def), + defaultValue (convertTo0to1 (def)) +{ + jassert (minValue < maxValue); // must have a non-zero range of values! +} + +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) (maxValue - minValue); } +int AudioParameterInt::convertFrom0to1 (float v) const noexcept { return limitRange (roundToInt ((v * (float) (maxValue - minValue)) + minValue)); } + +float AudioParameterInt::getValue() const { return convertTo0to1 (roundToInt (value)); } +void AudioParameterInt::setValue (float newValue) { value = (float) convertFrom0to1 (newValue); } +float AudioParameterInt::getDefaultValue() const { return defaultValue; } +int AudioParameterInt::getNumSteps() const { return AudioProcessorParameterWithID::getNumSteps(); } +float AudioParameterInt::getValueForText (const String& text) const { return convertTo0to1 (text.getIntValue()); } +String AudioParameterInt::getText (float v, int /*length*/) const { return String (convertFrom0to1 (v)); } + +AudioParameterInt& AudioParameterInt::operator= (int newValue) +{ + const float normalisedValue = convertTo0to1 (newValue); + + if (value != normalisedValue) + setValueNotifyingHost (normalisedValue); + + return *this; +} + + +//============================================================================== +AudioParameterBool::AudioParameterBool (String pid, String nm, bool def) + : AudioProcessorParameterWithID (pid, nm), + value (def ? 1.0f : 0.0f), + defaultValue (value) +{ +} + +AudioParameterBool::~AudioParameterBool() {} + +float AudioParameterBool::getValue() const { return value; } +void AudioParameterBool::setValue (float newValue) { value = newValue; } +float AudioParameterBool::getDefaultValue() const { return defaultValue; } +int AudioParameterBool::getNumSteps() const { return 2; } +float AudioParameterBool::getValueForText (const String& text) const { return text.getIntValue() != 0 ? 1.0f : 0.0f; } +String AudioParameterBool::getText (float v, int /*length*/) const { return String ((int) (v > 0.5f ? 1 : 0)); } + +AudioParameterBool& AudioParameterBool::operator= (bool newValue) +{ + const float normalisedValue = newValue ? 1.0f : 0.0f; + + if (value != normalisedValue) + setValueNotifyingHost (normalisedValue); + + return *this; +} + + +//============================================================================== +AudioParameterChoice::AudioParameterChoice (String pid, String nm, const StringArray& c, int def) + : AudioProcessorParameterWithID (pid, nm), choices (c), + value ((float) def), + defaultValue (convertTo0to1 (def)) +{ + jassert (choices.size() > 0); // you must supply an actual set of items to choose from! +} + +AudioParameterChoice::~AudioParameterChoice() {} + +int AudioParameterChoice::limitRange (int v) const noexcept { return jlimit (0, choices.size() - 1, v); } +float AudioParameterChoice::convertTo0to1 (int v) const noexcept { return jlimit (0.0f, 1.0f, (v + 0.5f) / (float) choices.size()); } +int AudioParameterChoice::convertFrom0to1 (float v) const noexcept { return limitRange ((int) (v * (float) choices.size())); } + +float AudioParameterChoice::getValue() const { return convertTo0to1 (roundToInt (value)); } +void AudioParameterChoice::setValue (float newValue) { value = (float) convertFrom0to1 (newValue); } +float AudioParameterChoice::getDefaultValue() const { return defaultValue; } +int AudioParameterChoice::getNumSteps() const { return choices.size(); } +float AudioParameterChoice::getValueForText (const String& text) const { return convertTo0to1 (choices.indexOf (text)); } +String AudioParameterChoice::getText (float v, int /*length*/) const { return choices [convertFrom0to1 (v)]; } + +AudioParameterChoice& AudioParameterChoice::operator= (int newValue) +{ + const float normalisedValue = convertTo0to1 (newValue); + + if (value != normalisedValue) + setValueNotifyingHost (normalisedValue); + + return *this; +} diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp new file mode 100644 index 000000000..76063062c --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.cpp @@ -0,0 +1,512 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + + +#if JUCE_COMPILER_SUPPORTS_LAMBDAS + +//============================================================================== +struct AudioProcessorValueTreeState::Parameter : public AudioProcessorParameter, + private ValueTree::Listener +{ + Parameter (AudioProcessorValueTreeState& s, + String parameterID, String paramName, String labelText, + NormalisableRange r, float defaultVal, + std::function valueToText, + std::function textToValue) + : owner (s), paramID (parameterID), name (paramName), label (labelText), + valueToTextFunction (valueToText), + textToValueFunction (textToValue), + range (r), value (defaultVal), defaultValue (defaultVal), + listenersNeedCalling (true) + { + state.addListener (this); + needsUpdate.set (1); + } + + ~Parameter() + { + // should have detached all callbacks before destroying the parameters! + jassert (listeners.size() <= 1); + } + + float getValue() const override { return range.convertTo0to1 (value); } + float getDefaultValue() const override { return range.convertTo0to1 (defaultValue); } + String getName (int maximumStringLength) const override { return name.substring (0, maximumStringLength); } + String getLabel() const override { return label; } + + float getValueForText (const String& text) const override + { + return range.convertTo0to1 (textToValueFunction != nullptr ? textToValueFunction (text) + : text.getFloatValue()); + } + + String getText (float v, int length) const override + { + return valueToTextFunction != nullptr ? valueToTextFunction (range.convertFrom0to1 (v)) + : AudioProcessorParameter::getText (v, length); + } + + void setValue (float newValue) override + { + newValue = range.snapToLegalValue (range.convertFrom0to1 (newValue)); + + if (value != newValue || listenersNeedCalling) + { + value = newValue; + + listeners.call (&AudioProcessorValueTreeState::Listener::parameterChanged, paramID, value); + listenersNeedCalling = false; + + needsUpdate.set (1); + } + } + + 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 (state.isValid()) + state.setProperty (owner.valuePropertyID, value, owner.undoManager); + } + + void valueTreePropertyChanged (ValueTree&, const Identifier& property) override + { + if (property == owner.valuePropertyID) + updateFromValueTree(); + } + + 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 + { + const int numParams = processor.getParameters().size(); + + for (int i = 0; i < numParams; ++i) + { + AudioProcessorParameter* const ap = processor.getParameters().getUnchecked(i); + + // 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); + + Parameter* const p = static_cast (ap); + + if (paramID == p->paramID) + return p; + } + + return nullptr; + } + + AudioProcessorValueTreeState& owner; + ValueTree state; + String paramID, name, label; + ListenerList listeners; + std::function valueToTextFunction; + std::function textToValueFunction; + NormalisableRange range; + float value, defaultValue; + Atomic needsUpdate; + bool listenersNeedCalling; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Parameter) +}; + +//============================================================================== +AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& p, UndoManager* um) + : processor (p), + undoManager (um), + valueType ("PARAM"), + valuePropertyID ("value"), + idPropertyID ("id"), + updatingConnections (false) +{ + startTimerHz (10); + state.addListener (this); +} + +AudioProcessorValueTreeState::~AudioProcessorValueTreeState() {} + +AudioProcessorParameter* AudioProcessorValueTreeState::createAndAddParameter (String paramID, String paramName, String labelText, + NormalisableRange r, float defaultVal, + std::function valueToTextFunction, + std::function textToValueFunction) +{ + // All parameters must be created before giving this manager a ValueTree state! + jassert (! state.isValid()); + jassert (MessageManager::getInstance()->isThisTheMessageThread()); + + Parameter* p = new Parameter (*this, paramID, paramName, labelText, r, + defaultVal, valueToTextFunction, textToValueFunction); + processor.addParameter (p); + return p; +} + +void AudioProcessorValueTreeState::addParameterListener (StringRef paramID, Listener* listener) +{ + if (Parameter* p = Parameter::getParameterForID (processor, paramID)) + p->listeners.add (listener); +} + +void AudioProcessorValueTreeState::removeParameterListener (StringRef paramID, Listener* listener) +{ + if (Parameter* p = Parameter::getParameterForID (processor, paramID)) + p->listeners.remove (listener); +} + +Value AudioProcessorValueTreeState::getParameterAsValue (StringRef paramID) const +{ + if (Parameter* p = Parameter::getParameterForID (processor, paramID)) + return p->state.getPropertyAsValue (valuePropertyID, undoManager); + + return Value(); +} + +NormalisableRange AudioProcessorValueTreeState::getParameterRange (StringRef paramID) const noexcept +{ + if (Parameter* p = Parameter::getParameterForID (processor, paramID)) + return p->range; + + return NormalisableRange(); +} + +AudioProcessorParameter* AudioProcessorValueTreeState::getParameter (StringRef paramID) const noexcept +{ + return Parameter::getParameterForID (processor, paramID); +} + +float* AudioProcessorValueTreeState::getRawParameterValue (StringRef paramID) const noexcept +{ + if (Parameter* p = Parameter::getParameterForID (processor, paramID)) + return &(p->value); + + return nullptr; +} + +ValueTree AudioProcessorValueTreeState::getOrCreateChildValueTree (const String& paramID) +{ + ValueTree v (state.getChildWithProperty (idPropertyID, paramID)); + + if (! v.isValid()) + { + v = ValueTree (valueType); + v.setProperty (idPropertyID, paramID, undoManager); + state.addChild (v, -1, undoManager); + } + + return v; +} + +void AudioProcessorValueTreeState::updateParameterConnectionsToChildTrees() +{ + if (! updatingConnections) + { + ScopedValueSetter svs (updatingConnections, true, false); + + const int numParams = processor.getParameters().size(); + + for (int i = 0; i < numParams; ++i) + { + AudioProcessorParameter* const ap = processor.getParameters().getUnchecked(i); + jassert (dynamic_cast (ap) != nullptr); + + Parameter* p = static_cast (ap); + p->setNewState (getOrCreateChildValueTree (p->paramID)); + } + } +} + +void AudioProcessorValueTreeState::valueTreePropertyChanged (ValueTree&, const Identifier& property) +{ + if (property == idPropertyID) + updateParameterConnectionsToChildTrees(); +} + +void AudioProcessorValueTreeState::valueTreeChildAdded (ValueTree& parent, ValueTree&) +{ + if (parent == state) + updateParameterConnectionsToChildTrees(); +} + +void AudioProcessorValueTreeState::valueTreeChildRemoved (ValueTree& parent, ValueTree&, int) +{ + if (parent == state) + updateParameterConnectionsToChildTrees(); +} + +void AudioProcessorValueTreeState::valueTreeRedirected (ValueTree& v) +{ + if (v == state) + updateParameterConnectionsToChildTrees(); +} + +void AudioProcessorValueTreeState::valueTreeChildOrderChanged (ValueTree&, int, int) {} +void AudioProcessorValueTreeState::valueTreeParentChanged (ValueTree&) {} + +void AudioProcessorValueTreeState::timerCallback() +{ + const int numParams = processor.getParameters().size(); + bool anythingUpdated = false; + + for (int i = 0; i < numParams; ++i) + { + AudioProcessorParameter* const ap = processor.getParameters().getUnchecked(i); + jassert (dynamic_cast (ap) != nullptr); + + Parameter* p = static_cast (ap); + + if (p->needsUpdate.compareAndSetBool (0, 1)) + { + p->copyValueToValueTree(); + anythingUpdated = true; + } + } + + startTimer (anythingUpdated ? 1000 / 50 + : jlimit (50, 500, getTimerInterval() + 20)); +} + +AudioProcessorValueTreeState::Listener::Listener() {} +AudioProcessorValueTreeState::Listener::~Listener() {} + +//============================================================================== +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 setNewUnnormalisedValue (float newUnnormalisedValue) + { + if (AudioProcessorParameter* p = state.getParameter (paramID)) + { + const float newValue = state.getParameterRange (paramID) + .convertTo0to1 (newUnnormalisedValue); + + if (p->getValue() != newValue) + p->setValueNotifyingHost (newValue); + } + } + + void sendInitialUpdate() + { + if (float* 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 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) + { + NormalisableRange range (s.getParameterRange (paramID)); + slider.setRange (range.start, range.end, range.interval); + + if (AudioProcessorParameter* param = state.getParameter (paramID)) + slider.setDoubleClickReturnValue (true, range.convertFrom0to1 (param->getDefaultValue())); + + sendInitialUpdate(); + slider.addListener (this); + } + + ~Pimpl() + { + slider.removeListener (this); + removeListener(); + } + + void setValue (float newValue) override + { + slider.setValue (newValue, sendNotificationSync); + } + + void sliderValueChanged (Slider* s) override + { + if (! ModifierKeys::getCurrentModifiers().isRightButtonDown()) + setNewUnnormalisedValue ((float) s->getValue()); + } + + void sliderDragStarted (Slider*) override + { + if (AudioProcessorParameter* p = state.getParameter (paramID)) + p->beginChangeGesture(); + } + + void sliderDragEnded (Slider*) override + { + if (AudioProcessorParameter* p = state.getParameter (paramID)) + p->endChangeGesture(); + } + + Slider& slider; + + 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) + { + sendInitialUpdate(); + combo.addListener (this); + } + + ~Pimpl() + { + combo.removeListener (this); + removeListener(); + } + + void setValue (float newValue) override + { + combo.setSelectedItemIndex (roundToInt (newValue), sendNotificationSync); + } + + void comboBoxChanged (ComboBox* comboBox) override + { + setNewUnnormalisedValue ((float) comboBox->getSelectedId() - 1.0f); + } + + ComboBox& combo; + + 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) + { + sendInitialUpdate(); + button.addListener (this); + } + + ~Pimpl() + { + button.removeListener (this); + removeListener(); + } + + void setValue (float newValue) override + { + button.setToggleState (newValue >= 0.5f, sendNotificationSync); + } + + void buttonClicked (Button* b) override + { + setNewUnnormalisedValue (b->getToggleState() ? 1.0f : 0.0f); + } + + Button& button; + + 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() {} + +#endif diff --git a/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h new file mode 100644 index 000000000..5928d5a8e --- /dev/null +++ b/source/modules/juce_audio_processors/utilities/juce_AudioProcessorValueTreeState.h @@ -0,0 +1,226 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef JUCE_AUDIOPROCESSORVALUETREESTATE_H_INCLUDED +#define JUCE_AUDIOPROCESSORVALUETREESTATE_H_INCLUDED + +#if JUCE_COMPILER_SUPPORTS_LAMBDAS || defined (DOXYGEN) + +/** + This class contains a ValueTree which is used to manage an AudioProcessor's entire state. + + It has its own internal class of parameter object which are linked to values + within its ValueTree, and which are each identified by a string ID. + + To use: Create a AudioProcessorValueTreeState, and give it some parameters + using createParameter(). + + You can get access to the underlying ValueTree object via the state member variable, + so you can add extra properties to it as necessary. + + It also provides some utility child classes for connecting parameters directly to + GUI controls like sliders. +*/ +class JUCE_API AudioProcessorValueTreeState : private Timer, + private ValueTree::Listener +{ +public: + /** 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 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 + @returns the parameter object that was created + */ + AudioProcessorParameter* createAndAddParameter (String parameterID, + String parameterName, + String labelText, + NormalisableRange valueRange, + float defaultValue, + std::function valueToTextFunction, + std::function textToValueFunction); + + /** Returns a parameter by its ID string. */ + AudioProcessorParameter* 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(); + + /** This callback method is called by the AudioProcessorValueTreeState when a parameter changes. */ + virtual void parameterChanged (const String& parameterID, float newValue) = 0; + }; + + /** Attaches a callback to one of the parameters, which will be called when the parameter changes. */ + void addParameterListener (StringRef parameterID, Listener* listener); + + /** 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; + + /** A reference to the processor with which this state is associated. */ + AudioProcessor& processor; + + /** The state of the whole processor. + You can replace this with your own ValueTree object, and can add properties and + children to the tree. This class will automatically add children for each of the + parameter objects that are created by createParameter(). + */ + ValueTree state; + + /** Provides access to the undo manager that this object is using. */ + UndoManager* const undoManager; + + //============================================================================== + /** An object of this class maintains a connection between a Slider and a parameter + in an AudioProcessorValueTreeState. + + During the lifetime of this SliderAttachment object, it keeps the two things in + sync, making it easy to connect a slider to a parameter. When this object is + deleted, the connection is broken. Make sure that your AudioProcessorValueTreeState + and Slider aren't deleted before this object! + */ + class JUCE_API SliderAttachment + { + public: + SliderAttachment (AudioProcessorValueTreeState& stateToControl, + const String& parameterID, + Slider& sliderToControl); + ~SliderAttachment(); + + private: + struct Pimpl; + ScopedPointer pimpl; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SliderAttachment) + }; + + //============================================================================== + /** An object of this class maintains a connection between a ComboBox and a parameter + in an AudioProcessorValueTreeState. + + During the lifetime of this SliderAttachment object, it keeps the two things in + sync, making it easy to connect a combo box to a parameter. When this object is + deleted, the connection is broken. Make sure that your AudioProcessorValueTreeState + and ComboBox aren't deleted before this object! + */ + class JUCE_API ComboBoxAttachment + { + public: + ComboBoxAttachment (AudioProcessorValueTreeState& stateToControl, + const String& parameterID, + ComboBox& comboBoxToControl); + ~ComboBoxAttachment(); + + private: + struct Pimpl; + ScopedPointer pimpl; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComboBoxAttachment) + }; + + //============================================================================== + /** An object of this class maintains a connection between a Button and a parameter + in an AudioProcessorValueTreeState. + + During the lifetime of this SliderAttachment object, it keeps the two things in + sync, making it easy to connect a button to a parameter. When this object is + deleted, the connection is broken. Make sure that your AudioProcessorValueTreeState + and Button aren't deleted before this object! + */ + class JUCE_API ButtonAttachment + { + public: + ButtonAttachment (AudioProcessorValueTreeState& stateToControl, + const String& parameterID, + Button& buttonToControl); + ~ButtonAttachment(); + + private: + struct Pimpl; + ScopedPointer pimpl; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonAttachment) + }; + +private: + //============================================================================== + struct Parameter; + friend struct Parameter; + + ValueTree getOrCreateChildValueTree (const String&); + void timerCallback() override; + + void valueTreePropertyChanged (ValueTree&, const Identifier&) override; + void valueTreeChildAdded (ValueTree&, ValueTree&) override; + void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override; + void valueTreeChildOrderChanged (ValueTree&, int, int) override; + void valueTreeParentChanged (ValueTree&) override; + void valueTreeRedirected (ValueTree&) override; + void updateParameterConnectionsToChildTrees(); + + Identifier valueType, valuePropertyID, idPropertyID; + bool updatingConnections; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorValueTreeState) +}; + +#endif + +#endif // JUCE_AUDIOPROCESSORVALUETREESTATE_H_INCLUDED diff --git a/source/modules/juce_core/native/java/AndroidMidi.java b/source/modules/juce_core/native/java/AndroidMidi.java new file mode 100644 index 000000000..9ad86ecce --- /dev/null +++ b/source/modules/juce_core/native/java/AndroidMidi.java @@ -0,0 +1,835 @@ + //============================================================================== + public class BluetoothManager extends ScanCallback + { + BluetoothManager() + { + ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); + scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID)); + + ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); + scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setScanMode (ScanSettings.SCAN_MODE_LOW_POWER) + .setScanMode (ScanSettings.MATCH_MODE_STICKY); + + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (bluetoothAdapter == null) + { + Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter"); + return; + } + + BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); + + if (bluetoothLeScanner == null) + { + Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner"); + return; + } + + bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()), + scanSettingsBuilder.build(), + this); + } + + public String[] getMidiBluetoothAddresses() + { + return bluetoothMidiDevices.toArray (new String[bluetoothMidiDevices.size()]); + } + + public String getHumanReadableStringForBluetoothAddress (String address) + { + BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); + return btDevice.getName(); + } + + public boolean isBluetoothDevicePaired (String address) + { + return getAndroidMidiDeviceManager().isBluetoothDevicePaired (address); + } + + public boolean pairBluetoothMidiDevice(String address) + { + BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); + + if (btDevice == null) + { + Log.d ("JUCE", "failed to create buletooth device from address"); + return false; + } + + MidiManager mm = (MidiManager) getSystemService (MIDI_SERVICE); + + PhysicalMidiDevice midiDevice = PhysicalMidiDevice.fromBluetoothLeDevice (btDevice, mm); + + if (midiDevice != null) + { + getAndroidMidiDeviceManager().addDeviceToList (midiDevice); + return true; + } + + return false; + } + + public void unpairBluetoothMidiDevice (String address) + { + getAndroidMidiDeviceManager().unpairBluetoothDevice (address); + } + + public void onScanFailed (int errorCode) + { + } + + public void onScanResult (int callbackType, ScanResult result) + { + if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES + || callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) + { + BluetoothDevice device = result.getDevice(); + + if (device != null) + bluetoothMidiDevices.add (device.getAddress()); + } + + if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) + { + Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST"); + BluetoothDevice device = result.getDevice(); + + if (device != null) + { + bluetoothMidiDevices.remove (device.getAddress()); + unpairBluetoothMidiDevice (device.getAddress()); + } + } + } + + public void onBatchScanResults (List results) + { + for (ScanResult result : results) + onScanResult (ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result); + } + + private BluetoothLeScanner scanner; + private static final String bluetoothLEMidiServiceUUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"; + + private HashSet bluetoothMidiDevices = new HashSet(); + } + + public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort + { + private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp); + + public JuceMidiInputPort (PhysicalMidiDevice device, long host, MidiOutputPort midiPort) + { + parent = device; + juceHost = host; + port = midiPort; + } + + @Override + public boolean isInputPort() + { + return true; + } + + @Override + public void start() + { + port.connect (this); + } + + @Override + public void stop() + { + port.disconnect (this); + } + + @Override + public void close() + { + stop(); + + try + { + port.close(); + } + catch (IOException e) + { + Log.d ("JUCE", "JuceMidiInputPort::close: IOException = " + e.toString()); + } + + if (parent != null) + { + parent.removePort (port.getPortNumber(), true); + parent = null; + } + } + + public void onSend (byte[] msg, int offset, int count, long timestamp) + { + if (count > 0) + handleReceive (juceHost, msg, offset, count, timestamp); + } + + @Override + public MidiPortID getPortId() + { + return new MidiPortID (port.getPortNumber(), true); + } + + @Override + public void sendMidi (byte[] msg, int offset, int count) + { + } + + private PhysicalMidiDevice parent = null; + private long juceHost = 0; + private MidiOutputPort port; + } + + public static class JuceMidiOutputPort implements JuceMidiPort + { + public JuceMidiOutputPort (PhysicalMidiDevice device, MidiInputPort midiPort) + { + parent = device; + port = midiPort; + } + + @Override + public boolean isInputPort() + { + return false; + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + @Override + public void sendMidi (byte[] msg, int offset, int count) + { + try + { + port.send(msg, offset, count); + } + catch (IOException e) + { + Log.d ("JUCE", "JuceMidiOutputPort::sendMidi: IOException = " + e.toString()); + } + } + + @Override + public void close() + { + try + { + port.close(); + } + catch (IOException e) + { + Log.d ("JUCE", "JuceMidiOutputPort::close: IOException = " + e.toString()); + } + + if (parent != null) + { + parent.removePort (port.getPortNumber(), false); + parent = null; + } + } + + + @Override + public MidiPortID getPortId() + { + return new MidiPortID (port.getPortNumber(), false); + } + + private PhysicalMidiDevice parent = null; + private MidiInputPort port; + } + + public static class PhysicalMidiDevice + { + private static class MidiDeviceThread extends Thread + { + public Handler handler = null; + public Object sync = null; + + public MidiDeviceThread (Object syncrhonization) + { + sync = syncrhonization; + } + + public void run() + { + Looper.prepare(); + + synchronized (sync) + { + handler = new Handler(); + sync.notifyAll(); + } + + Looper.loop(); + } + } + + private static class MidiDeviceOpenCallback implements MidiManager.OnDeviceOpenedListener + { + public Object sync = null; + public boolean isWaiting = true; + public android.media.midi.MidiDevice theDevice = null; + + public MidiDeviceOpenCallback (Object waiter) + { + sync = waiter; + } + + public void onDeviceOpened (MidiDevice device) + { + synchronized (sync) + { + theDevice = device; + isWaiting = false; + sync.notifyAll(); + } + } + } + + public static PhysicalMidiDevice fromBluetoothLeDevice (BluetoothDevice bluetoothDevice, MidiManager mm) + { + Object waitForCreation = new Object(); + MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); + thread.start(); + + synchronized (waitForCreation) + { + while (thread.handler == null) + { + try + { + waitForCreation.wait(); + } + catch (InterruptedException e) + { + Log.d ("JUCE", "Wait was interrupted but we don't care"); + } + } + } + + Object waitForDevice = new Object(); + + MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); + + synchronized (waitForDevice) + { + mm.openBluetoothDevice (bluetoothDevice, openCallback, thread.handler); + + while (openCallback.isWaiting) + { + try + { + waitForDevice.wait(); + } + catch (InterruptedException e) + { + Log.d ("JUCE", "Wait was interrupted but we don't care"); + } + } + } + + if (openCallback.theDevice == null) + { + Log.d ("JUCE", "openBluetoothDevice failed"); + return null; + } + + PhysicalMidiDevice device = new PhysicalMidiDevice(); + + device.handle = openCallback.theDevice; + device.info = device.handle.getInfo(); + device.bluetoothAddress = bluetoothDevice.getAddress(); + device.midiManager = mm; + + return device; + } + + public void unpair() + { + if (! bluetoothAddress.equals ("") && handle != null) + { + JuceMidiPort ports[] = new JuceMidiPort[0]; + ports = juceOpenedPorts.values().toArray(ports); + + for (int i = 0; i < ports.length; ++i) + ports[i].close(); + + juceOpenedPorts.clear(); + + try + { + handle.close(); + } + catch (IOException e) + { + Log.d ("JUCE", "handle.close(): IOException = " + e.toString()); + } + + handle = null; + } + } + + public static PhysicalMidiDevice fromMidiDeviceInfo (MidiDeviceInfo info, MidiManager mm) + { + PhysicalMidiDevice device = new PhysicalMidiDevice(); + device.info = info; + device.midiManager = mm; + return device; + } + + public PhysicalMidiDevice() + { + bluetoothAddress = ""; + juceOpenedPorts = new Hashtable(); + handle = null; + } + + public MidiDeviceInfo.PortInfo[] getPorts() + { + return info.getPorts(); + } + + public String getHumanReadableNameForPort (MidiDeviceInfo.PortInfo port, int portIndexToUseInName) + { + String portName = port.getName(); + + if (portName.equals ("")) + portName = ((port.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) ? "Out " : "In ") + + Integer.toString (portIndexToUseInName); + + return getHumanReadableDeviceName() + " " + portName; + } + + public String getHumanReadableNameForPort (int portType, int androidPortID, int portIndexToUseInName) + { + MidiDeviceInfo.PortInfo[] ports = info.getPorts(); + + for (MidiDeviceInfo.PortInfo port : ports) + { + if (port.getType() == portType) + { + if (port.getPortNumber() == androidPortID) + return getHumanReadableNameForPort (port, portIndexToUseInName); + } + } + + return "Unknown"; + } + + public String getHumanReadableDeviceName() + { + Bundle bundle = info.getProperties(); + return bundle.getString (MidiDeviceInfo.PROPERTY_NAME , "Unknown device"); + } + + public void checkIfDeviceCanBeClosed() + { + if (juceOpenedPorts.size() == 0) + { + // never close bluetooth LE devices, otherwise they unpair and we have + // no idea how many ports they have. + // Only remove bluetooth devices when we specifically unpair + if (bluetoothAddress.equals ("")) + { + try + { + handle.close(); + handle = null; + } + catch (IOException e) + { + Log.d ("JUCE", "PhysicalMidiDevice::checkIfDeviceCanBeClosed: IOException = " + e.toString()); + } + } + } + } + + public void removePort (int portIdx, boolean isInput) + { + MidiPortID portID = new MidiPortID (portIdx, isInput); + JuceMidiPort port = juceOpenedPorts.get (portID); + + if (port != null) + { + juceOpenedPorts.remove (portID); + checkIfDeviceCanBeClosed(); + return; + } + + // tried to remove a port that was never added + assert false; + } + + public JuceMidiPort openPort (int portIdx, boolean isInput, long host) + { + open(); + + if (handle == null) + { + Log.d ("JUCE", "PhysicalMidiDevice::openPort: handle = null, device not open"); + return null; + } + + // make sure that the port is not already open + if (findPortForIdx (portIdx, isInput) != null) + { + Log.d ("JUCE", "PhysicalMidiDevice::openInputPort: port already open, not opening again!"); + return null; + } + + JuceMidiPort retval = null; + + if (isInput) + { + MidiOutputPort androidPort = handle.openOutputPort (portIdx); + + if (androidPort == null) + { + Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openOutputPort (portIdx = " + + Integer.toString (portIdx) + ") failed!"); + return null; + } + + retval = new JuceMidiInputPort (this, host, androidPort); + } + else + { + MidiInputPort androidPort = handle.openInputPort (portIdx); + + if (androidPort == null) + { + Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openInputPort (portIdx = " + + Integer.toString (portIdx) + ") failed!"); + return null; + } + + retval = new JuceMidiOutputPort (this, androidPort); + } + + juceOpenedPorts.put (new MidiPortID (portIdx, isInput), retval); + return retval; + } + + private JuceMidiPort findPortForIdx (int idx, boolean isInput) + { + return juceOpenedPorts.get (new MidiPortID (idx, isInput)); + } + + // opens the device + private synchronized void open() + { + if (handle != null) + return; + + Object waitForCreation = new Object(); + MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); + thread.start(); + + synchronized(waitForCreation) + { + while (thread.handler == null) + { + try + { + waitForCreation.wait(); + } + catch (InterruptedException e) + { + Log.d ("JUCE", "wait was interrupted but we don't care"); + } + } + } + + Object waitForDevice = new Object(); + + MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); + + synchronized (waitForDevice) + { + midiManager.openDevice (info, openCallback, thread.handler); + + while (openCallback.isWaiting) + { + try + { + waitForDevice.wait(); + } + catch (InterruptedException e) + { + Log.d ("JUCE", "wait was interrupted but we don't care"); + } + } + } + + handle = openCallback.theDevice; + } + + private MidiDeviceInfo info; + private Hashtable juceOpenedPorts; + public MidiDevice handle; + public String bluetoothAddress; + private MidiManager midiManager; + } + + //============================================================================== + public class MidiDeviceManager extends MidiManager.DeviceCallback + { + public class MidiPortPath + { + public PhysicalMidiDevice midiDevice; + public int androidMidiPortID; + public int portIndexToUseInName; + } + + public class JuceDeviceList + { + public ArrayList inPorts = new ArrayList(); + public ArrayList outPorts = new ArrayList(); + } + + // We need to keep a thread local copy of the devices + // which we returned the last time + // getJuceAndroidMidiIn/OutputDevices() was called + private final ThreadLocal lastDevicesReturned = + new ThreadLocal() + { + @Override protected JuceDeviceList initialValue() + { + return new JuceDeviceList(); + } + }; + + public MidiDeviceManager() + { + physicalMidiDevices = new ArrayList(); + manager = (MidiManager) getSystemService (MIDI_SERVICE); + + if (manager == null) + { + Log.d ("JUCE", "MidiDeviceManager error: could not get MidiManager system service"); + return; + } + + manager.registerDeviceCallback (this, null); + + MidiDeviceInfo[] foundDevices = manager.getDevices(); + + for (MidiDeviceInfo info : foundDevices) + physicalMidiDevices.add (PhysicalMidiDevice.fromMidiDeviceInfo (info, manager)); + } + + // specifically add a device to the list + public void addDeviceToList (PhysicalMidiDevice device) + { + physicalMidiDevices.add (device); + } + + public void unpairBluetoothDevice (String address) + { + for (int i = 0; i < physicalMidiDevices.size(); ++i) + { + PhysicalMidiDevice device = physicalMidiDevices.get(i); + + if (device.bluetoothAddress.equals (address)) + { + physicalMidiDevices.remove (i); + device.unpair(); + return; + } + } + } + + public boolean isBluetoothDevicePaired (String address) + { + for (int i = 0; i < physicalMidiDevices.size(); ++i) + { + PhysicalMidiDevice device = physicalMidiDevices.get(i); + + if (device.bluetoothAddress.equals (address)) + return true; + } + + return false; + } + + public String[] getJuceAndroidMidiInputDevices() + { + return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT); + } + + public String[] getJuceAndroidMidiOutputDevices() + { + return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); + } + + private String[] getJuceAndroidMidiDevices (int portType) + { + ArrayList listOfReturnedDevices = new ArrayList(); + List deviceNames = new ArrayList(); + + for (PhysicalMidiDevice physicalMidiDevice : physicalMidiDevices) + { + int portIdx = 0; + MidiDeviceInfo.PortInfo[] ports = physicalMidiDevice.getPorts(); + + for (MidiDeviceInfo.PortInfo port : ports) + { + if (port.getType() == portType) + { + MidiPortPath path = new MidiPortPath(); + path.midiDevice = physicalMidiDevice; + path.androidMidiPortID = port.getPortNumber(); + path.portIndexToUseInName = ++portIdx; + listOfReturnedDevices.add (path); + + deviceNames.add (physicalMidiDevice.getHumanReadableNameForPort (port, + path.portIndexToUseInName)); + } + } + } + + String[] deviceNamesArray = new String[deviceNames.size()]; + + if (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT) + { + lastDevicesReturned.get().inPorts.clear(); + lastDevicesReturned.get().inPorts.addAll (listOfReturnedDevices); + } + else + { + lastDevicesReturned.get().outPorts.clear(); + lastDevicesReturned.get().outPorts.addAll (listOfReturnedDevices); + } + + return deviceNames.toArray(deviceNamesArray); + } + + public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host) + { + ArrayList lastDevices = lastDevicesReturned.get().inPorts; + + if (index >= lastDevices.size() || index < 0) + return null; + + MidiPortPath path = lastDevices.get (index); + return path.midiDevice.openPort (path.androidMidiPortID, true, host); + } + + public JuceMidiPort openMidiOutputPortWithJuceIndex (int index) + { + ArrayList lastDevices = lastDevicesReturned.get().outPorts; + + if (index >= lastDevices.size() || index < 0) + return null; + + MidiPortPath path = lastDevices.get (index); + return path.midiDevice.openPort (path.androidMidiPortID, false, 0); + } + + public String getInputPortNameForJuceIndex (int index) + { + ArrayList lastDevices = lastDevicesReturned.get().inPorts; + + if (index >= lastDevices.size() || index < 0) + return ""; + + MidiPortPath path = lastDevices.get (index); + + return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_INPUT, + path.androidMidiPortID, + path.portIndexToUseInName); + } + + public String getOutputPortNameForJuceIndex (int index) + { + ArrayList lastDevices = lastDevicesReturned.get().outPorts; + + if (index >= lastDevices.size() || index < 0) + return ""; + + MidiPortPath path = lastDevices.get (index); + + return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_OUTPUT, + path.androidMidiPortID, + path.portIndexToUseInName); + } + + public void onDeviceAdded (MidiDeviceInfo info) + { + PhysicalMidiDevice device = PhysicalMidiDevice.fromMidiDeviceInfo (info, manager); + + // Do not add bluetooth devices as they are already added by the native bluetooth dialog + if (info.getType() != MidiDeviceInfo.TYPE_BLUETOOTH) + physicalMidiDevices.add (device); + } + + public void onDeviceRemoved (MidiDeviceInfo info) + { + for (int i = 0; i < physicalMidiDevices.size(); ++i) + { + if (physicalMidiDevices.get(i).info.getId() == info.getId()) + { + physicalMidiDevices.remove (i); + return; + } + } + // Don't assert here as this may be called again after a bluetooth device is unpaired + } + + public void onDeviceStatusChanged (MidiDeviceStatus status) + { + } + + private ArrayList physicalMidiDevices; + private MidiManager manager; + } + + public MidiDeviceManager getAndroidMidiDeviceManager() + { + if (getSystemService (MIDI_SERVICE) == null) + return null; + + synchronized (JuceAppActivity.class) + { + if (midiDeviceManager == null) + midiDeviceManager = new MidiDeviceManager(); + } + + return midiDeviceManager; + } + + public BluetoothManager getAndroidBluetoothManager() + { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + if (adapter == null) + return null; + + if (adapter.getBluetoothLeScanner() == null) + return null; + + synchronized (JuceAppActivity.class) + { + if (bluetoothManager == null) + bluetoothManager = new BluetoothManager(); + } + + return bluetoothManager; + } diff --git a/source/modules/juce_core/native/java/AndroidMidiFallback.java b/source/modules/juce_core/native/java/AndroidMidiFallback.java new file mode 100644 index 000000000..416b7348c --- /dev/null +++ b/source/modules/juce_core/native/java/AndroidMidiFallback.java @@ -0,0 +1,81 @@ + //============================================================================== + public class BluetoothManager + { + BluetoothManager() + { + } + + public String[] getMidiBluetoothAddresses() + { + String[] bluetoothAddresses = new String[0]; + return bluetoothAddresses; + } + + public String getHumanReadableStringForBluetoothAddress (String address) + { + return address; + } + + public boolean isBluetoothDevicePaired (String address) + { + return false; + } + + public boolean pairBluetoothMidiDevice(String address) + { + return false; + } + + public void unpairBluetoothMidiDevice (String address) + { + } + } + + //============================================================================== + public class MidiDeviceManager + { + public MidiDeviceManager() + { + } + + public String[] getJuceAndroidMidiInputDevices() + { + return new String[0]; + } + + public String[] getJuceAndroidMidiOutputDevices() + { + return new String[0]; + } + + public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host) + { + return null; + } + + public JuceMidiPort openMidiOutputPortWithJuceIndex (int index) + { + return null; + } + + public String getInputPortNameForJuceIndex (int index) + { + return ""; + } + + public String getOutputPortNameForJuceIndex (int index) + { + return ""; + } + } + + + public MidiDeviceManager getAndroidMidiDeviceManager() + { + return null; + } + + public BluetoothManager getAndroidBluetoothManager() + { + return null; + }