diff --git a/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm b/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm index 553d5be84d..bf0d0efc86 100644 --- a/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm +++ b/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm @@ -474,6 +474,31 @@ public: { switch (inID) { + case kAudioUnitProperty_ParameterClumpName: + + if (auto* clumpNameInfo = (AudioUnitParameterNameInfo*) outData) + { + if (juceFilter != nullptr) + { + auto clumpIndex = clumpNameInfo->inID - 1; + const auto* group = parameterGroups[(int) clumpIndex]; + auto name = group->getName(); + + while (group->getParent() != &juceFilter->getParameterTree()) + { + group = group->getParent(); + name = group->getName() + group->getSeparator() + name; + } + + clumpNameInfo->outName = name.toCFString(); + return noErr; + } + } + + // Failed to find a group corresponding to the clump ID. + jassertfalse; + break; + case juceFilterObjectPropertyID: ((void**) outData)[0] = (void*) static_cast (juceFilter.get()); ((void**) outData)[1] = (void*) this; @@ -879,6 +904,14 @@ public: if (param->isMetaParameter()) outParameterInfo.flags |= kAudioUnitParameterFlag_IsGlobalMeta; + auto parameterGroupHierarchy = juceFilter->getParameterTree().getGroupsForParameter (param); + + if (! parameterGroupHierarchy.isEmpty()) + { + outParameterInfo.flags |= kAudioUnitParameterFlag_HasClump; + outParameterInfo.clumpID = (UInt32) parameterGroups.indexOf (parameterGroupHierarchy.getLast()) + 1; + } + // Is this a meter? if (((param->getCategory() & 0xffff0000) >> 16) == 2) { @@ -1687,6 +1720,7 @@ private: LegacyAudioParametersWrapper juceParameters; HashMap paramMap; Array auParamIDs; + Array parameterGroups; //============================================================================== AudioUnitEvent auEvent; @@ -1839,6 +1873,8 @@ private: //============================================================================== void addParameters() { + parameterGroups = juceFilter->getParameterTree().getSubgroups (true); + juceParameters.update (*juceFilter, forceUseLegacyParamIDs); const int numParams = juceParameters.getNumParameters(); diff --git a/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm b/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm index 449b39607f..afc7c66e32 100644 --- a/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm +++ b/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm @@ -1157,107 +1157,175 @@ private: #endif } + std::unique_ptr createParameter (AudioProcessorParameter* parameter) + { + const String name (parameter->getName (512)); + + AudioUnitParameterUnit unit = kAudioUnitParameterUnit_Generic; + AudioUnitParameterOptions flags = (UInt32) (kAudioUnitParameterFlag_IsWritable + | kAudioUnitParameterFlag_IsReadable + | kAudioUnitParameterFlag_HasCFNameString + | kAudioUnitParameterFlag_ValuesHaveStrings); + + if (! forceLegacyParamIDs) + flags |= (UInt32) kAudioUnitParameterFlag_IsHighResolution; + + // Set whether the param is automatable (unnamed parameters aren't allowed to be automated). + if (name.isEmpty() || ! parameter->isAutomatable()) + flags |= kAudioUnitParameterFlag_NonRealTime; + + const bool isParameterDiscrete = parameter->isDiscrete(); + + if (! isParameterDiscrete) + flags |= kAudioUnitParameterFlag_CanRamp; + + if (parameter->isMetaParameter()) + flags |= kAudioUnitParameterFlag_IsGlobalMeta; + + std::unique_ptr valueStrings; + + // Is this a meter? + if (((parameter->getCategory() & 0xffff0000) >> 16) == 2) + { + flags &= ~kAudioUnitParameterFlag_IsWritable; + flags |= kAudioUnitParameterFlag_MeterReadOnly | kAudioUnitParameterFlag_DisplayLogarithmic; + unit = kAudioUnitParameterUnit_LinearGain; + } + else + { + if (! forceLegacyParamIDs) + { + if (parameter->isDiscrete()) + { + unit = parameter->isBoolean() ? kAudioUnitParameterUnit_Boolean : kAudioUnitParameterUnit_Indexed; + auto maxValue = getMaximumParameterValue (parameter); + auto numSteps = parameter->getNumSteps(); + + // Some hosts can't handle the huge numbers of discrete parameter values created when + // using the default number of steps. + jassert (numSteps != AudioProcessor::getDefaultNumParameterSteps()); + + valueStrings.reset ([NSMutableArray new]); + + for (int i = 0; i < numSteps; ++i) + [valueStrings.get() addObject: juceStringToNS (parameter->getText ((float) i / maxValue, 0))]; + } + } + } + + AUParameterAddress address = generateAUParameterAddress (parameter); + + #if ! JUCE_FORCE_USE_LEGACY_PARAM_IDS + // If you hit this assertion then you have either put a parameter in two groups or you are + // very unlucky and the hash codes of your parameter ids are not unique. + jassert (! paramMap.contains (static_cast (address))); + + paramAddresses.add (address); + paramMap.set (static_cast (address), parameter->getParameterIndex()); + #endif + + auto getParameterIdentifier = [parameter] + { + if (auto* paramWithID = dynamic_cast (parameter)) + return paramWithID->paramID; + + // This could clash if any groups have been given integer IDs! + return String (parameter->getParameterIndex()); + }; + + std::unique_ptr param; + + @try + { + // Create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h + param.reset([[AUParameterTree createParameterWithIdentifier: juceStringToNS (getParameterIdentifier()) + name: juceStringToNS (name) + address: address + min: 0.0f + max: getMaximumParameterValue (parameter) + unit: unit + unitName: nullptr + flags: flags + valueStrings: valueStrings.get() + dependentParameters: nullptr] + retain]); + } + + @catch (NSException* exception) + { + // Do you have duplicate identifiers in any of your groups or parameters, + // or do your identifiers have unusual characters in them? + jassertfalse; + } + + [param.get() setValue: parameter->getDefaultValue()]; + + [overviewParams.get() addObject: [NSNumber numberWithUnsignedLongLong: address]]; + + return param; + } + + std::unique_ptr createParameterGroup (AudioProcessorParameterGroup* group) + { + std::unique_ptr, NSObjectDeleter> children ([NSMutableArray new]); + + for (auto* node : *group) + { + if (auto* childGroup = node->getGroup()) + [children.get() addObject: createParameterGroup (childGroup).get()]; + else + [children.get() addObject: createParameter (node->getParameter()).get()]; + } + + std::unique_ptr result; + + @try + { + // Create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h + result.reset ([[AUParameterTree createGroupWithIdentifier: juceStringToNS (group->getID()) + name: juceStringToNS (group->getName()) + children: children.get()] + retain]); + } + + @catch (NSException* exception) + { + // Do you have duplicate identifiers in any of your groups or parameters, + // or do your identifiers have unusual characters in them? + jassertfalse; + } + + return result; + } + void addParameters() { - std::unique_ptr, NSObjectDeleter> params ([[NSMutableArray alloc] init]); - - overviewParams.reset ([[NSMutableArray alloc] init]); - auto& processor = getAudioProcessor(); juceParameters.update (processor, forceLegacyParamIDs); - const int n = juceParameters.getNumParameters(); + // This is updated when we build the tree. + overviewParams.reset ([NSMutableArray new]); - for (int idx = 0; idx < n; ++idx) - { - auto* juceParam = juceParameters.getParamForIndex (idx); + std::unique_ptr, NSObjectDeleter> topLevelNodes ([NSMutableArray new]); - const String identifier (idx); - const String name = juceParam->getName (512); - - AudioUnitParameterUnit unit = kAudioUnitParameterUnit_Generic; - AudioUnitParameterOptions flags = (UInt32) (kAudioUnitParameterFlag_IsWritable - | kAudioUnitParameterFlag_IsReadable - | kAudioUnitParameterFlag_HasCFNameString - | kAudioUnitParameterFlag_ValuesHaveStrings); - - if (! forceLegacyParamIDs) - flags |= (UInt32) kAudioUnitParameterFlag_IsHighResolution; - - // set whether the param is automatable (unnamed parameters aren't allowed to be automated) - if (name.isEmpty() || ! juceParam->isAutomatable()) - flags |= kAudioUnitParameterFlag_NonRealTime; - - const bool isParameterDiscrete = juceParam->isDiscrete(); - - if (! isParameterDiscrete) - flags |= kAudioUnitParameterFlag_CanRamp; - - if (juceParam->isMetaParameter()) - flags |= kAudioUnitParameterFlag_IsGlobalMeta; - - std::unique_ptr valueStrings; - - // is this a meter? - if (((juceParam->getCategory() & 0xffff0000) >> 16) == 2) - { - flags &= ~kAudioUnitParameterFlag_IsWritable; - flags |= kAudioUnitParameterFlag_MeterReadOnly | kAudioUnitParameterFlag_DisplayLogarithmic; - unit = kAudioUnitParameterUnit_LinearGain; - } + for (auto* node : processor.getParameterTree()) + if (auto* childGroup = node->getGroup()) + [topLevelNodes.get() addObject: createParameterGroup (childGroup).get()]; else - { - if (! forceLegacyParamIDs) - { - if (juceParam->isDiscrete()) - { - unit = juceParam->isBoolean() ? kAudioUnitParameterUnit_Boolean : kAudioUnitParameterUnit_Indexed; - auto maxValue = getMaximumParameterValue (juceParam); - auto numSteps = juceParam->getNumSteps(); + [topLevelNodes.get() addObject: createParameter (node->getParameter()).get()]; - // Some hosts can't handle the huge numbers of discrete parameter values created when - // using the default number of steps. - jassert (numSteps != AudioProcessor::getDefaultNumParameterSteps()); - - valueStrings.reset ([NSMutableArray new]); - - for (int i = 0; i < numSteps; ++i) - [valueStrings.get() addObject: juceStringToNS (juceParam->getText ((float) i / maxValue, 0))]; - } - } - } - - AUParameterAddress address = generateAUParameterAddress (juceParam); - - #if ! JUCE_FORCE_USE_LEGACY_PARAM_IDS - // Consider yourself very unlucky if you hit this assertion. The hash codes of your - // parameter ids are not unique. - jassert (! paramMap.contains (static_cast (address))); - - paramAddresses.add (address); - paramMap.set (static_cast (address), idx); - #endif - - // create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h - std::unique_ptr param ([[AUParameterTree createParameterWithIdentifier: juceStringToNS (identifier) - name: juceStringToNS (name) - address: address - min: 0.0f - max: getMaximumParameterValue (juceParam) - unit: unit - unitName: nullptr - flags: flags - valueStrings: valueStrings.get() - dependentParameters: nullptr] retain]); - - [param.get() setValue: juceParam->getDefaultValue()]; - - [params.get() addObject: param.get()]; - [overviewParams.get() addObject: [NSNumber numberWithUnsignedLongLong:address]]; + @try + { + // Create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h + paramTree.reset ([[AUParameterTree createTreeWithChildren: topLevelNodes.get()] retain]); } - // create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h - paramTree.reset ([[AUParameterTree createTreeWithChildren: params.get()] retain]); + @catch (NSException* exception) + { + // Do you have duplicate identifiers in any of your groups or parameters, + // or do your identifiers have unusual characters in them? + jassertfalse; + } paramObserver = CreateObjCBlock (this, &JuceAudioUnitv3::valueChangedFromHost); paramProvider = CreateObjCBlock (this, &JuceAudioUnitv3::getValue); @@ -1582,13 +1650,8 @@ private: { const String& juceParamID = LegacyAudioParameter::getParamID (param, forceLegacyParamIDs); - #if JUCE_FORCE_USE_LEGACY_PARAM_IDS - auto result = juceParamID.getIntValue(); - #else - auto result = juceParamID.hashCode64(); - #endif - - return static_cast (result); + return static_cast (forceLegacyParamIDs ? juceParamID.getIntValue() + : juceParamID.hashCode64()); } AudioProcessorParameter* getJuceParameterForAUAddress (AUParameterAddress address) const noexcept diff --git a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp index 3e94c1ea72..968a458a57 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -126,6 +126,11 @@ public: return getParamForVSTParamID (bypassParamID); } + static Vst::UnitID getUnitID (const AudioProcessorParameterGroup* group) + { + return group == nullptr ? Vst::kRootUnitId : group->getID().hashCode(); + } + int getNumParameters() const noexcept { return vstParamIDs.size(); } bool isUsingManagedParameters() const noexcept { return juceParameters.isUsingManagedParameters(); } @@ -330,10 +335,12 @@ public: struct Param : public Vst::Parameter { Param (JuceVST3EditController& editController, AudioProcessorParameter& p, - Vst::ParamID vstParamID, bool isBypassParameter, bool forceLegacyParamIDs) + Vst::ParamID vstParamID, Vst::UnitID vstUnitID, + bool isBypassParameter, bool forceLegacyParamIDs) : owner (editController), param (p) { info.id = vstParamID; + info.unitId = vstUnitID; toString128 (info.title, param.getName (128)); toString128 (info.shortTitle, param.getName (8)); @@ -349,7 +356,6 @@ public: info.defaultNormalizedValue = param.getDefaultValue(); jassert (info.defaultNormalizedValue >= 0 && info.defaultNormalizedValue <= 1.0f); - info.unitId = Vst::kRootUnitId; // Is this a meter? if (((param.getCategory() & 0xffff0000) >> 16) == 2) @@ -740,8 +746,10 @@ private: { auto vstParamID = audioProcessor->getVSTParamIDForIndex (i); auto* juceParam = audioProcessor->getParamForVSTParamID (vstParamID); + auto* parameterGroup = pluginInstance->parameterTree.getGroupsForParameter (juceParam).getLast(); + auto unitID = JuceAudioProcessor::getUnitID (parameterGroup); - parameters.addParameter (new Param (*this, *juceParam, vstParamID, + parameters.addParameter (new Param (*this, *juceParam, vstParamID, unitID, (vstParamID == audioProcessor->bypassParamID), forceLegacyParamIDs)); } @@ -1271,6 +1279,8 @@ public: // and not AudioChannelSet::discreteChannels (2) etc. jassert (checkBusFormatsAreNotDiscrete()); + parameterGroups = pluginInstance->parameterTree.getSubgroups (true); + comPluginInstance = new JuceAudioProcessor (pluginInstance); zerostruct (processContext); @@ -1763,7 +1773,7 @@ public: //============================================================================== Steinberg::int32 PLUGIN_API getUnitCount() override { - return 1; + return parameterGroups.size() + 1; } tresult PLUGIN_API getUnitInfo (Steinberg::int32 unitIndex, Vst::UnitInfo& info) override @@ -1779,7 +1789,17 @@ public: return kResultTrue; } - zerostruct (info); + if (auto* group = parameterGroups[unitIndex - 1]) + { + info.id = JuceAudioProcessor::getUnitID (group); + info.parentUnitId = JuceAudioProcessor::getUnitID (group->getParent()); + info.programListId = Vst::kNoProgramListId; + + toString128 (info.name, group->getName()); + + return kResultTrue; + } + return kResultFalse; } @@ -2263,43 +2283,6 @@ public: } private: - //============================================================================== - Atomic refCount { 1 }; - - AudioProcessor* pluginInstance; - ComSmartPtr host; - ComSmartPtr comPluginInstance; - ComSmartPtr juceVST3EditController; - - /** - Since VST3 does not provide a way of knowing the buffer size and sample rate at any point, - this object needs to be copied on every call to process() to be up-to-date... - */ - Vst::ProcessContext processContext; - Vst::ProcessSetup processSetup; - - MidiBuffer midiBuffer; - Array channelListFloat; - Array channelListDouble; - - AudioBuffer emptyBufferFloat; - AudioBuffer emptyBufferDouble; - - #if JucePlugin_WantsMidiInput - bool isMidiInputBusEnabled = true; - #else - bool isMidiInputBusEnabled = false; - #endif - - #if JucePlugin_ProducesMidiOutput - bool isMidiOutputBusEnabled = true; - #else - bool isMidiOutputBusEnabled = false; - #endif - - ScopedJuceInitialiser_GUI libraryInitialiser; - static const char* kJucePrivateDataIdentifier; - //============================================================================== template void processAudio (Vst::ProcessData& data, Array& channelList) @@ -2517,6 +2500,45 @@ private: } //============================================================================== + Atomic refCount { 1 }; + + AudioProcessor* pluginInstance; + ComSmartPtr host; + ComSmartPtr comPluginInstance; + ComSmartPtr juceVST3EditController; + + /** + Since VST3 does not provide a way of knowing the buffer size and sample rate at any point, + this object needs to be copied on every call to process() to be up-to-date... + */ + Vst::ProcessContext processContext; + + Vst::ProcessSetup processSetup; + + MidiBuffer midiBuffer; + Array channelListFloat; + Array channelListDouble; + + AudioBuffer emptyBufferFloat; + AudioBuffer emptyBufferDouble; + + #if JucePlugin_WantsMidiInput + bool isMidiInputBusEnabled = true; + #else + bool isMidiInputBusEnabled = false; + #endif + + #if JucePlugin_ProducesMidiOutput + bool isMidiOutputBusEnabled = true; + #else + bool isMidiOutputBusEnabled = false; + #endif + + ScopedJuceInitialiser_GUI libraryInitialiser; + static const char* kJucePrivateDataIdentifier; + + Array parameterGroups; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceVST3Component) }; diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index c76c228799..8d0a72fd0a 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -1331,6 +1331,7 @@ public: { managedParameters.clear(); paramIDToIndex.clear(); + AudioProcessorParameterGroup parameterGroups ({}, {}, {}); if (audioUnit != nullptr) { @@ -1348,6 +1349,8 @@ public: AudioUnitGetProperty (audioUnit, kAudioUnitProperty_ParameterList, kAudioUnitScope_Global, 0, ids, ¶mListSize); + std::map groupIDMap; + for (size_t i = 0; i < numParams; ++i) { AudioUnitParameterInfo info; @@ -1400,23 +1403,68 @@ public: break; } - addParameter (new AUInstanceParameter (*this, - ids[i], - paramName, - info.minValue, - info.maxValue, - info.defaultValue, - (info.flags & kAudioUnitParameterFlag_NonRealTime) == 0, - isDiscrete, - isDiscrete ? (int) (info.maxValue + 1.0f) : AudioProcessor::getDefaultNumParameterSteps(), - isBoolean, - label, - (info.flags & kAudioUnitParameterFlag_ValuesHaveStrings) != 0)); + auto* parameter = new AUInstanceParameter (*this, + ids[i], + paramName, + info.minValue, + info.maxValue, + info.defaultValue, + (info.flags & kAudioUnitParameterFlag_NonRealTime) == 0, + isDiscrete, + isDiscrete ? (int) (info.maxValue + 1.0f) : AudioProcessor::getDefaultNumParameterSteps(), + isBoolean, + label, + (info.flags & kAudioUnitParameterFlag_ValuesHaveStrings) != 0); + + if (info.flags & kAudioUnitParameterFlag_HasClump) + { + auto groupInfo = groupIDMap.find (info.clumpID); + + if (groupInfo == groupIDMap.end()) + { + auto getClumpName = [this, info] + { + AudioUnitParameterNameInfo clumpNameInfo; + UInt32 sz = sizeof (clumpNameInfo); + zerostruct (clumpNameInfo); + clumpNameInfo.inID = info.clumpID; + clumpNameInfo.inDesiredLength = (SInt32) 256; + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ParameterClumpName, + kAudioUnitScope_Global, + 0, + &clumpNameInfo, + &sz) == noErr) + return String::fromCFString (clumpNameInfo.outName); + + return String (info.clumpID); + }; + + auto group = std::make_unique (String (info.clumpID), + getClumpName(), String()); + group->addChild (std::unique_ptr (parameter)); + groupIDMap[info.clumpID] = group.get(); + parameterGroups.addChild (std::move (group)); + } + else + { + groupInfo->second->addChild (std::unique_ptr (parameter)); + } + } + else + { + parameterGroups.addChild (std::unique_ptr (parameter)); + } + + addParameter (parameter); } } } } + parameterTree.swapWith (parameterGroups); + UInt32 propertySize = 0; Boolean writable = false; diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index dce0651328..4336525670 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -36,9 +36,6 @@ namespace juce using namespace Steinberg; //============================================================================== -struct VST3Classes -{ - #ifndef JUCE_VST3_DEBUGGING #define JUCE_VST3_DEBUGGING 0 #endif @@ -168,15 +165,6 @@ static void setStateForAllBusesOfType (Vst::IComponent* component, warnOnFailure (component->activateBus (mediaType, direction, i, state)); } -//============================================================================== -/** Assigns a complete AudioBuffer's channels to an AudioBusBuffers' */ -static void associateWholeBufferTo (Vst::AudioBusBuffers& vstBuffers, AudioBuffer& buffer) noexcept -{ - vstBuffers.channelBuffers32 = buffer.getArrayOfWritePointers(); - vstBuffers.numChannels = buffer.getNumChannels(); - vstBuffers.silenceFlags = 0; -} - //============================================================================== static void toProcessContext (Vst::ProcessContext& context, AudioPlayHead* playHead, double sampleRate) { @@ -247,7 +235,7 @@ static void toProcessContext (Vst::ProcessContext& context, AudioPlayHead* playH } //============================================================================== -struct VST3PluginInstance; +class VST3PluginInstance; struct VST3HostContext : public Vst::IComponentHandler, // From VST V3.0.0 public Vst::IComponentHandler2, // From VST V3.1.0 (a very well named class, of course!) @@ -274,96 +262,11 @@ struct VST3HostContext : public Vst::IComponentHandler, // From VST V3.0.0 } //============================================================================== - tresult PLUGIN_API beginEdit (Vst::ParamID paramID) override - { - if (plugin != nullptr) - { - auto index = getIndexOfParamID (paramID); + tresult PLUGIN_API beginEdit (Vst::ParamID paramID) override; + tresult PLUGIN_API performEdit (Vst::ParamID paramID, Vst::ParamValue valueNormalized) override; + tresult PLUGIN_API endEdit (Vst::ParamID paramID) override; - if (index < 0) - return kResultFalse; - - if (auto* param = plugin->getParameters()[index]) - param->beginChangeGesture(); - else - jassertfalse; // Invalid parameter index! - } - - return kResultTrue; - } - - tresult PLUGIN_API performEdit (Vst::ParamID paramID, Vst::ParamValue valueNormalized) override - { - if (plugin != nullptr) - { - auto index = getIndexOfParamID (paramID); - - if (index < 0) - return kResultFalse; - - if (auto* param = plugin->getParameters()[index]) - param->sendValueChangedMessageToListeners ((float) valueNormalized); - else - jassertfalse; // Invalid parameter index! - - { - Steinberg::int32 eventIndex; - plugin->inputParameterChanges->addParameterData (paramID, eventIndex)->addPoint (0, valueNormalized, eventIndex); - } - - // did the plug-in already update the parameter internally - if (plugin->editController->getParamNormalized (paramID) != (float) valueNormalized) - return plugin->editController->setParamNormalized (paramID, valueNormalized); - } - - return kResultTrue; - } - - tresult PLUGIN_API endEdit (Vst::ParamID paramID) override - { - if (plugin != nullptr) - { - auto index = getIndexOfParamID (paramID); - - if (index < 0) - return kResultFalse; - - if (auto* param = plugin->getParameters()[index]) - param->endChangeGesture(); - else - jassertfalse; // Invalid parameter index! - } - - return kResultTrue; - } - - tresult PLUGIN_API restartComponent (Steinberg::int32 flags) override - { - if (plugin != nullptr) - { - if (hasFlag (flags, Vst::kReloadComponent)) - plugin->reset(); - - if (hasFlag (flags, Vst::kIoChanged)) - { - auto sampleRate = plugin->getSampleRate(); - auto blockSize = plugin->getBlockSize(); - - plugin->prepareToPlay (sampleRate >= 8000 ? sampleRate : 44100.0, - blockSize > 0 ? blockSize : 1024); - } - - if (hasFlag (flags, Vst::kLatencyChanged)) - if (plugin->processor != nullptr) - plugin->setLatencySamples (jmax (0, (int) plugin->processor->getLatencySamples())); - - plugin->updateHostDisplay(); - return kResultTrue; - } - - jassertfalse; - return kResultFalse; - } + tresult PLUGIN_API restartComponent (Steinberg::int32 flags) override; //============================================================================== tresult PLUGIN_API setDirty (TBool) override @@ -453,63 +356,7 @@ struct VST3HostContext : public Vst::IComponentHandler, // From VST V3.0.0 return kResultFalse; } - tresult PLUGIN_API popup (Steinberg::UCoord x, Steinberg::UCoord y) override - { - Array subItemStack; - OwnedArray menuStack; - PopupMenu* topLevelMenu = menuStack.add (new PopupMenu()); - - for (int i = 0; i < items.size(); ++i) - { - auto& item = items.getReference (i).item; - auto* menuToUse = menuStack.getLast(); - - if (hasFlag (item.flags, Item::kIsGroupStart & ~Item::kIsDisabled)) - { - subItemStack.add (&item); - menuStack.add (new PopupMenu()); - } - else if (hasFlag (item.flags, Item::kIsGroupEnd)) - { - if (auto* subItem = subItemStack.getLast()) - { - if (auto* m = menuStack [menuStack.size() - 2]) - m->addSubMenu (toString (subItem->name), *menuToUse, - ! hasFlag (subItem->flags, Item::kIsDisabled), - nullptr, - hasFlag (subItem->flags, Item::kIsChecked)); - - menuStack.removeLast (1); - subItemStack.removeLast (1); - } - } - else if (hasFlag (item.flags, Item::kIsSeparator)) - { - menuToUse->addSeparator(); - } - else - { - menuToUse->addItem (item.tag != 0 ? (int) item.tag : (int) zeroTagReplacement, - toString (item.name), - ! hasFlag (item.flags, Item::kIsDisabled), - hasFlag (item.flags, Item::kIsChecked)); - } - } - - PopupMenu::Options options; - - if (auto* ed = owner.getActiveEditor()) - options = options.withTargetScreenArea (ed->getScreenBounds().translated ((int) x, (int) y).withSize (1, 1)); - - #if JUCE_MODAL_LOOPS_PERMITTED - // Unfortunately, Steinberg's docs explicitly say this should be modal.. - handleResult (topLevelMenu->showMenu (options)); - #else - topLevelMenu->showMenuAsync (options, ModalCallbackFunction::create (menuFinished, ComSmartPtr (this))); - #endif - - return kResultOk; - } + tresult PLUGIN_API popup (Steinberg::UCoord x, Steinberg::UCoord y) override; #if ! JUCE_MODAL_LOOPS_PERMITTED static void menuFinished (int modalResult, ComSmartPtr menu) { menu->handleResult (modalResult); } @@ -613,13 +460,7 @@ struct VST3HostContext : public Vst::IComponentHandler, // From VST V3.0.0 return kResultFalse; } - tresult PLUGIN_API notifyProgramListChange (Vst::ProgramListID, Steinberg::int32) override - { - if (plugin != nullptr) - plugin->syncProgramNames(); - - return kResultTrue; - } + tresult PLUGIN_API notifyProgramListChange (Vst::ProgramListID, Steinberg::int32) override; //============================================================================== tresult PLUGIN_API queryInterface (const TUID iid, void** obj) override @@ -651,29 +492,7 @@ private: using ParamMapType = std::map; ParamMapType paramToIndexMap; - int getIndexOfParamID (Vst::ParamID paramID) - { - if (plugin == nullptr || plugin->editController == nullptr) - return -1; - - auto result = getMappedParamID (paramID); - - if (result < 0) - { - auto numParams = plugin->editController->getParameterCount(); - - for (int i = 0; i < numParams; ++i) - { - Vst::ParameterInfo paramInfo; - plugin->editController->getParameterInfo (i, paramInfo); - paramToIndexMap[paramInfo.id] = i; - } - - result = getMappedParamID (paramID); - } - - return result; - } + int getIndexOfParamID (Vst::ParamID paramID); int getMappedParamID (Vst::ParamID paramID) { @@ -1777,8 +1596,9 @@ struct VST3ComponentHolder }; //============================================================================== -struct VST3PluginInstance : public AudioPluginInstance +class VST3PluginInstance : public AudioPluginInstance { +public: struct VST3Parameter final : public Parameter { VST3Parameter (VST3PluginInstance& parent, @@ -1881,10 +1701,10 @@ struct VST3PluginInstance : public AudioPluginInstance }; VST3PluginInstance (VST3ComponentHolder* componentHolder) - : AudioPluginInstance (getBusProperties (componentHolder->component)), - holder (componentHolder), - inputParameterChanges (new ParamValueQueueList()), - outputParameterChanges (new ParamValueQueueList()), + : AudioPluginInstance (getBusProperties (componentHolder->component)), + holder (componentHolder), + inputParameterChanges (new ParamValueQueueList()), + outputParameterChanges (new ParamValueQueueList()), midiInputs (new MidiEventList()), midiOutputs (new MidiEventList()) { @@ -1948,19 +1768,7 @@ struct VST3PluginInstance : public AudioPluginInstance editController->setComponentHandler (holder->host); grabInformationObjects(); interconnectComponentAndController(); - - for (int i = 0; i < editController->getParameterCount(); ++i) - { - auto paramInfo = getParameterInfoForIndex (i); - VST3Parameter* p = new VST3Parameter (*this, - paramInfo.id, - (paramInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0); - addParameter (p); - - if ((paramInfo.flags & Vst::ParameterInfo::kIsBypass) != 0) - bypassParam = p; - } - + addParameters(); synchroniseStates(); syncProgramNames(); setupIO(); @@ -2718,6 +2526,70 @@ private: } } + void addParameters() + { + AudioProcessorParameterGroup parameterGroups ({}, {}, {}); + + // We're going to add parameter groups to the tree recursively in the same order as the + // first parameters contained within them. + std::map infoMap; + std::map groupMap; + groupMap[Vst::kRootUnitId] = ¶meterGroups; + + if (unitInfo != nullptr) + { + const auto numUnits = unitInfo->getUnitCount(); + + for (int i = 1; i < numUnits; ++i) + { + Vst::UnitInfo ui = { 0 }; + unitInfo->getUnitInfo (i, ui); + infoMap[ui.id] = std::move (ui); + } + } + + for (int i = 0; i < editController->getParameterCount(); ++i) + { + auto paramInfo = getParameterInfoForIndex (i); + auto* param = new VST3Parameter (*this, + paramInfo.id, + (paramInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0); + addParameter (param); + + if ((paramInfo.flags & Vst::ParameterInfo::kIsBypass) != 0) + bypassParam = param; + + std::function findOrCreateGroup; + findOrCreateGroup = [&groupMap, &infoMap, &findOrCreateGroup](Vst::UnitID groupID) + { + auto existingGoup = groupMap.find (groupID); + + if (existingGoup != groupMap.end()) + return existingGoup->second; + + auto groupInfo = infoMap.find (groupID); + + if (groupInfo == infoMap.end()) + return groupMap[Vst::kRootUnitId]; + + auto* group = new AudioProcessorParameterGroup (String (groupInfo->first), + toString (groupInfo->second.name), + {}); + groupMap[groupInfo->first] = group; + + auto* parentGroup = findOrCreateGroup (groupInfo->second.parentUnitId); + parentGroup->addChild (std::unique_ptr (group)); + + return group; + }; + + auto* group = findOrCreateGroup (paramInfo.unitId); + group->addChild (std::unique_ptr (param)); + } + + parameterTree.swapWith (parameterGroups); + } + void synchroniseStates() { Steinberg::MemoryStream stream; @@ -2984,10 +2856,8 @@ private: #pragma warning (pop) #endif -}; - //============================================================================== -AudioPluginInstance* VST3Classes::VST3ComponentHolder::createPluginInstance() +AudioPluginInstance* VST3ComponentHolder::createPluginInstance() { if (! initialise()) return nullptr; @@ -2997,6 +2867,190 @@ AudioPluginInstance* VST3Classes::VST3ComponentHolder::createPluginInstance() return plugin; } +//============================================================================== +tresult VST3HostContext::beginEdit (Vst::ParamID paramID) +{ + if (plugin != nullptr) + { + auto index = getIndexOfParamID (paramID); + + if (index < 0) + return kResultFalse; + + if (auto* param = plugin->getParameters()[index]) + param->beginChangeGesture(); + else + jassertfalse; // Invalid parameter index! + } + + return kResultTrue; +} + +tresult VST3HostContext::performEdit (Vst::ParamID paramID, Vst::ParamValue valueNormalized) +{ + if (plugin != nullptr) + { + auto index = getIndexOfParamID (paramID); + + if (index < 0) + return kResultFalse; + + if (auto* param = plugin->getParameters()[index]) + param->sendValueChangedMessageToListeners ((float) valueNormalized); + else + jassertfalse; // Invalid parameter index! + + { + Steinberg::int32 eventIndex; + plugin->inputParameterChanges->addParameterData (paramID, eventIndex)->addPoint (0, valueNormalized, eventIndex); + } + + // did the plug-in already update the parameter internally + if (plugin->editController->getParamNormalized (paramID) != (float) valueNormalized) + return plugin->editController->setParamNormalized (paramID, valueNormalized); + } + + return kResultTrue; +} + +tresult VST3HostContext::endEdit (Vst::ParamID paramID) +{ + if (plugin != nullptr) + { + auto index = getIndexOfParamID (paramID); + + if (index < 0) + return kResultFalse; + + if (auto* param = plugin->getParameters()[index]) + param->endChangeGesture(); + else + jassertfalse; // Invalid parameter index! + } + + return kResultTrue; +} + +tresult VST3HostContext::restartComponent (Steinberg::int32 flags) +{ + if (plugin != nullptr) + { + if (hasFlag (flags, Vst::kReloadComponent)) + plugin->reset(); + + if (hasFlag (flags, Vst::kIoChanged)) + { + auto sampleRate = plugin->getSampleRate(); + auto blockSize = plugin->getBlockSize(); + + plugin->prepareToPlay (sampleRate >= 8000 ? sampleRate : 44100.0, + blockSize > 0 ? blockSize : 1024); + } + + if (hasFlag (flags, Vst::kLatencyChanged)) + if (plugin->processor != nullptr) + plugin->setLatencySamples (jmax (0, (int) plugin->processor->getLatencySamples())); + + plugin->updateHostDisplay(); + return kResultTrue; + } + + jassertfalse; + return kResultFalse; +} + +//============================================================================== +tresult VST3HostContext::ContextMenu::popup (Steinberg::UCoord x, Steinberg::UCoord y) +{ + Array subItemStack; + OwnedArray menuStack; + PopupMenu* topLevelMenu = menuStack.add (new PopupMenu()); + + for (int i = 0; i < items.size(); ++i) + { + auto& item = items.getReference (i).item; + auto* menuToUse = menuStack.getLast(); + + if (hasFlag (item.flags, Item::kIsGroupStart & ~Item::kIsDisabled)) + { + subItemStack.add (&item); + menuStack.add (new PopupMenu()); + } + else if (hasFlag (item.flags, Item::kIsGroupEnd)) + { + if (auto* subItem = subItemStack.getLast()) + { + if (auto* m = menuStack [menuStack.size() - 2]) + m->addSubMenu (toString (subItem->name), *menuToUse, + ! hasFlag (subItem->flags, Item::kIsDisabled), + nullptr, + hasFlag (subItem->flags, Item::kIsChecked)); + + menuStack.removeLast (1); + subItemStack.removeLast (1); + } + } + else if (hasFlag (item.flags, Item::kIsSeparator)) + { + menuToUse->addSeparator(); + } + else + { + menuToUse->addItem (item.tag != 0 ? (int) item.tag : (int) zeroTagReplacement, + toString (item.name), + ! hasFlag (item.flags, Item::kIsDisabled), + hasFlag (item.flags, Item::kIsChecked)); + } + } + + PopupMenu::Options options; + + if (auto* ed = owner.getActiveEditor()) + options = options.withTargetScreenArea (ed->getScreenBounds().translated ((int) x, (int) y).withSize (1, 1)); + + #if JUCE_MODAL_LOOPS_PERMITTED + // Unfortunately, Steinberg's docs explicitly say this should be modal.. + handleResult (topLevelMenu->showMenu (options)); + #else + topLevelMenu->showMenuAsync (options, ModalCallbackFunction::create (menuFinished, ComSmartPtr (this))); + #endif + + return kResultOk; +} + +//============================================================================== +tresult VST3HostContext::notifyProgramListChange (Vst::ProgramListID, Steinberg::int32) +{ + if (plugin != nullptr) + plugin->syncProgramNames(); + + return kResultTrue; +} + +//============================================================================== +int VST3HostContext::getIndexOfParamID (Vst::ParamID paramID) +{ + if (plugin == nullptr || plugin->editController == nullptr) + return -1; + + auto result = getMappedParamID (paramID); + + if (result < 0) + { + auto numParams = plugin->editController->getParameterCount(); + + for (int i = 0; i < numParams; ++i) + { + Vst::ParameterInfo paramInfo; + plugin->editController->getParameterInfo (i, paramInfo); + paramToIndexMap[paramInfo.id] = i; + } + + result = getMappedParamID (paramID); + } + + return result; +} //============================================================================== VST3PluginFormat::VST3PluginFormat() {} @@ -3004,7 +3058,7 @@ VST3PluginFormat::~VST3PluginFormat() {} bool VST3PluginFormat::setStateFromVSTPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData) { - if (auto vst3 = dynamic_cast (api)) + if (auto vst3 = dynamic_cast (api)) return vst3->setStateFromPresetFile (rawData); return false; @@ -3013,13 +3067,13 @@ bool VST3PluginFormat::setStateFromVSTPresetFile (AudioPluginInstance* api, cons void VST3PluginFormat::findAllTypesForFile (OwnedArray& results, const String& fileOrIdentifier) { if (fileMightContainThisPluginType (fileOrIdentifier)) - VST3Classes::VST3ModuleHandle::getAllDescriptionsForFile (results, fileOrIdentifier); + VST3ModuleHandle::getAllDescriptionsForFile (results, fileOrIdentifier); } void VST3PluginFormat::createPluginInstance (const PluginDescription& description, double, int, void* userData, PluginCreationCallback callback) { - std::unique_ptr result; + std::unique_ptr result; if (fileMightContainThisPluginType (description.fileOrIdentifier)) { @@ -3028,13 +3082,13 @@ void VST3PluginFormat::createPluginInstance (const PluginDescription& descriptio auto previousWorkingDirectory = File::getCurrentWorkingDirectory(); file.getParentDirectory().setAsCurrentWorkingDirectory(); - if (const VST3Classes::VST3ModuleHandle::Ptr module = VST3Classes::VST3ModuleHandle::findOrCreateModule (file, description)) + if (const VST3ModuleHandle::Ptr module = VST3ModuleHandle::findOrCreateModule (file, description)) { - std::unique_ptr holder (new VST3Classes::VST3ComponentHolder (module)); + std::unique_ptr holder (new VST3ComponentHolder (module)); if (holder->initialise()) { - result.reset (new VST3Classes::VST3PluginInstance (holder.release())); + result.reset (new VST3PluginInstance (holder.release())); if (! result->initialise()) result.reset(); diff --git a/modules/juce_audio_processors/juce_audio_processors.cpp b/modules/juce_audio_processors/juce_audio_processors.cpp index 806e65eb6c..5caf58576b 100644 --- a/modules/juce_audio_processors/juce_audio_processors.cpp +++ b/modules/juce_audio_processors/juce_audio_processors.cpp @@ -167,4 +167,5 @@ struct AutoResizingNSViewComponentWithParent : public AutoResizingNSViewCompone #include "scanning/juce_PluginDirectoryScanner.cpp" #include "scanning/juce_PluginListComponent.cpp" #include "utilities/juce_AudioProcessorParameters.cpp" +#include "processors/juce_AudioProcessorParameterGroup.cpp" #include "utilities/juce_AudioProcessorValueTreeState.cpp" diff --git a/modules/juce_audio_processors/juce_audio_processors.h b/modules/juce_audio_processors/juce_audio_processors.h index 59ada712a1..fe6c1dc16e 100644 --- a/modules/juce_audio_processors/juce_audio_processors.h +++ b/modules/juce_audio_processors/juce_audio_processors.h @@ -110,6 +110,7 @@ #include "processors/juce_AudioProcessorEditor.h" #include "processors/juce_AudioProcessorListener.h" #include "processors/juce_AudioProcessorParameter.h" +#include "processors/juce_AudioProcessorParameterGroup.h" #include "processors/juce_AudioProcessor.h" #include "processors/juce_PluginDescription.h" #include "processors/juce_AudioPluginInstance.h" diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp index 4ca0f71434..48a1bbd4ee 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp @@ -60,6 +60,11 @@ AudioProcessor::~AudioProcessor() // or more parameters without having made a corresponding call to endParameterChangeGesture... jassert (changingParams.countNumberOfSetBits() == 0); #endif + + // The parameters are owned by an AudioProcessorParameterGroup, but we need + // to keep the managedParameters array populated to maintain backwards + // compatibility. + managedParameters.clearQuick (false); } //============================================================================== @@ -691,17 +696,36 @@ AudioProcessorParameter* AudioProcessor::getParamChecked (int index) const noexc return p; } -void AudioProcessor::addParameter (AudioProcessorParameter* p) +void AudioProcessor::addParameterInternal (AudioProcessorParameter* param) { - p->processor = this; - p->parameterIndex = managedParameters.size(); - managedParameters.add (p); + param->processor = this; + param->parameterIndex = managedParameters.size(); + managedParameters.add (param); #ifdef JUCE_DEBUG shouldCheckParamsForDupeIDs = true; #endif } +void AudioProcessor::addParameter (AudioProcessorParameter* param) +{ + addParameterInternal (param); + parameterTree.addChild (std::unique_ptr (param)); +} + +void AudioProcessor::addParameterGroup (std::unique_ptr group) +{ + for (auto* param : group->getParameters (true)) + addParameterInternal (param); + + parameterTree.addChild (std::move (group)); +} + +const AudioProcessorParameterGroup& AudioProcessor::getParameterTree() +{ + return parameterTree; +} + #ifdef JUCE_DEBUG void AudioProcessor::checkForDupedParamIDs() { diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.h b/modules/juce_audio_processors/processors/juce_AudioProcessor.h index 9d77508079..4c0366b468 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessor.h +++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.h @@ -1205,12 +1205,25 @@ public: void updateHostDisplay(); //============================================================================== - /** Adds a parameter to the list. - The parameter object will be managed and deleted automatically by the list - when no longer needed. + /** Adds a parameter to the AudioProcessor. + + The parameter object will be managed and deleted automatically by the + AudioProcessor when no longer needed. */ void addParameter (AudioProcessorParameter*); + /** Adds a group of parameters to the AudioProcessor. + + All the parameter objects contained within the group will be managed and + deleted automatically by the AudioProcessor when no longer needed. + + @see addParameter + */ + void addParameterGroup (std::unique_ptr); + + /** Returns the group of parameters managed by this AudioProcessor. */ + const AudioProcessorParameterGroup& getParameterTree(); + /** Returns the current list of parameters. */ const OwnedArray& getParameters() const noexcept; @@ -1584,6 +1597,9 @@ protected: void sendParamChangeMessageToListeners (int parameterIndex, float newValue); private: + //============================================================================== + void addParameterInternal (AudioProcessorParameter*); + //============================================================================== struct InOutChannelPair { @@ -1648,6 +1664,8 @@ private: OwnedArray managedParameters; AudioProcessorParameter* getParamChecked (int) const noexcept; + AudioProcessorParameterGroup parameterTree { {}, {}, {} }; + #if JUCE_DEBUG && ! JUCE_DISABLE_AUDIOPROCESSOR_BEGIN_END_GESTURE_CHECKING BigInteger changingParams; #endif @@ -1671,6 +1689,7 @@ private: friend class JuceVST3EditController; friend class JuceVST3Component; + friend class VST3PluginInstance; friend class AudioUnitPluginInstance; friend class LADSPAPluginInstance; diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.cpp new file mode 100644 index 0000000000..3efe8b8feb --- /dev/null +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.cpp @@ -0,0 +1,163 @@ +/* + ============================================================================== + + 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 +{ + +#if JUCE_UNIT_TESTS + +class ParameterGroupTests : public UnitTest +{ +public: + ParameterGroupTests() : UnitTest ("ParameterGroups", "Parameters") {} + + void runTest() override + { + beginTest ("ParameterGroups"); + + auto g1 = std::make_unique ("g1", "g1", " - "); + + auto* p1 = new AudioParameterFloat ("p1", "p1", { 0.0f, 2.0f }, 0.5f); + auto* p2 = new AudioParameterFloat ("p2", "p2", { 0.0f, 2.0f }, 0.5f); + auto* p3 = new AudioParameterFloat ("p3", "p3", { 0.0f, 2.0f }, 0.5f); + + g1->addChild (std::unique_ptr (p1)); + g1->addChild (std::unique_ptr (p2), + std::unique_ptr (p3)); + + auto p4 = std::make_unique ("p4", "p4", NormalisableRange (0.0f, 2.0f), 0.5f); + auto p5 = std::make_unique ("p5", "p5", NormalisableRange (0.0f, 2.0f), 0.5f); + auto p6 = std::make_unique ("p6", "p6", NormalisableRange (0.0f, 2.0f), 0.5f); + + g1->addChild (std::move (p4)); + g1->addChild (std::move (p5), + std::move (p6)); + + { + auto topLevelParams = g1->getParameters (false); + auto params = g1->getParameters (true); + expect (topLevelParams == params); + expectEquals (params.size(), 6); + + expect (params[0] == (AudioProcessorParameter*) p1); + expect (params[1] == (AudioProcessorParameter*) p2); + expect (params[2] == (AudioProcessorParameter*) p3); + + expect (dynamic_cast (params[3])->name == "p4"); + expect (dynamic_cast (params[4])->name == "p5"); + expect (dynamic_cast (params[5])->name == "p6"); + } + + auto* p7 = new AudioParameterFloat ("p7", "p7", { 0.0f, 2.0f }, 0.5f); + auto* p8 = new AudioParameterFloat ("p8", "p8", { 0.0f, 2.0f }, 0.5f); + auto* p9 = new AudioParameterFloat ("p9", "p9", { 0.0f, 2.0f }, 0.5f); + + auto p10 = std::make_unique ("p10", "p10", NormalisableRange (0.0f, 2.0f), 0.5f); + auto p11 = std::make_unique ("p11", "p11", NormalisableRange (0.0f, 2.0f), 0.5f); + auto p12 = std::make_unique ("p12", "p12", NormalisableRange (0.0f, 2.0f), 0.5f); + + auto g2 = std::make_unique ("g2", "g2", " | ", std::unique_ptr (p7)); + auto g3 = std::make_unique ("g3", "g3", " | ", std::unique_ptr (p8), std::unique_ptr (p9)); + auto g4 = std::make_unique ("g4", "g4", " | ", std::move (p10)); + auto g5 = std::make_unique ("g5", "g5", " | ", std::move (p11), std::move (p12)); + + g1->addChild (std::move (g2)); + g4->addChild (std::move (g5)); + g1->addChild (std::move (g3), std::move (g4)); + + { + auto topLevelParams = g1->getParameters (false); + auto params = g1->getParameters (true); + expectEquals (topLevelParams.size(), 6); + expectEquals (params.size(), 12); + + expect (params[0] == (AudioProcessorParameter*) p1); + expect (params[1] == (AudioProcessorParameter*) p2); + expect (params[2] == (AudioProcessorParameter*) p3); + + expect (dynamic_cast (params[3])->name == "p4"); + expect (dynamic_cast (params[4])->name == "p5"); + expect (dynamic_cast (params[5])->name == "p6"); + + expect (params[6] == (AudioProcessorParameter*) p7); + expect (params[7] == (AudioProcessorParameter*) p8); + expect (params[8] == (AudioProcessorParameter*) p9); + + expect (dynamic_cast (params[9]) ->name == "p10"); + expect (dynamic_cast (params[10])->name == "p11"); + expect (dynamic_cast (params[11])->name == "p12"); + } + + g1->addChild (std::make_unique ("g6", "g6", " | ", + std::make_unique ("p11", "p11", NormalisableRange (0.0f, 2.0f), 0.5f), + std::make_unique ("g7", "g7", " | ", + std::make_unique ("p12", "p12", NormalisableRange (0.0f, 2.0f), 0.5f)), + std::make_unique ("p13", "p13", NormalisableRange (0.0f, 2.0f), 0.5f))); + + TestAudioProcessor processor; + + processor.addParameter (new AudioParameterFloat ("pstart", "pstart", NormalisableRange (0.0f, 2.0f), 0.5f)); + auto groupParams = g1->getParameters (true); + processor.addParameterGroup (std::move (g1)); + processor.addParameter (new AudioParameterFloat ("pend", "pend", NormalisableRange (0.0f, 2.0f), 0.5f)); + + auto& processorParams = processor.getParameters(); + expect (dynamic_cast (processorParams.getFirst())->name == "pstart"); + expect (dynamic_cast (processorParams.getLast()) ->name == "pend"); + + auto numParams = processorParams.size(); + + for (int i = 1; i < numParams - 1; ++i) + expect (processorParams[i] == groupParams[i - 1]); + + } +private: + struct TestAudioProcessor : public AudioProcessor + { + const String getName() const override { return "ap"; } + void prepareToPlay (double, int) override {} + void releaseResources() override {} + void processBlock (AudioBuffer&, MidiBuffer&) override {} + double getTailLengthSeconds() const override { return 0.0; } + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + AudioProcessorEditor* createEditor() override { return nullptr; } + bool hasEditor() const override { return false; } + int getNumPrograms() override { return 0; } + int getCurrentProgram() override { return 0; } + 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 {} + }; +}; + +static ParameterGroupTests parameterGroupTests; + +#endif + +} // namespace juce diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.h b/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.h new file mode 100644 index 0000000000..8b1f44415a --- /dev/null +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorParameterGroup.h @@ -0,0 +1,299 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +/** A class encapsulating a group of AudioProcessorParameters and nested + AudioProcessorParameterGroups. + + This class is predominantly write-only; there are methods for adding group + members but none for removing them. Ultimately you will probably want to + add a fully constructed group to an AudioProcessor. + + @see AudioProcessor::addParameterGroup + + @tags{Audio} +*/ +class AudioProcessorParameterGroup +{ +public: + //============================================================================== + /** A child of an AudioProcessorParameterGroup. + + This can contain either an AudioProcessorParameter or an + AudioProcessorParameterGroup. You can query which using the + getParameter and getGroup methods. + + @code + for (auto* child : group) + if (auto* parameter = node.getParameter()) + parameter->setValueNotifyingHost (0.5f); + else + node.getGroup()->AddChild (new Parameter()); + @endcode + */ + class AudioProcessorParameterNode + { + public: + //============================================================================== + /** Returns the parent group or nullptr if this is a top-level group. */ + AudioProcessorParameterGroup* getParent() const { return parent; } + + /** Returns a pointer to a parameter if this node contains a paramater, nullptr otherwise. */ + AudioProcessorParameter* getParameter() const { return parameter.get(); } + + /** Returns a pointer to a group if this node contains a group, nullptr otherwise. */ + AudioProcessorParameterGroup* getGroup() const { return group.get(); } + + private: + //============================================================================== + AudioProcessorParameterNode (std::unique_ptr param, + AudioProcessorParameterGroup* parentGroup) + : parameter (std::move (param)), parent (parentGroup) + {} + + AudioProcessorParameterNode (std::unique_ptr grp, + AudioProcessorParameterGroup* parentGroup) + : group (std::move (grp)), parent (parentGroup) + { + group->parent = parent; + } + + std::unique_ptr group; + std::unique_ptr parameter; + AudioProcessorParameterGroup* parent = nullptr; + + friend class AudioProcessorParameterGroup; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorParameterNode) + }; + + //============================================================================== + /** Creates an empty AudioProcessorParameterGroup. + + @param groupID A unique identifier for the group. Keep it basic; don't use any special + characters like "." and avoid pure integer strings which could collide with + legacy parameter IDs. + @param groupName The group's name, which will be displayed in the host. + @param subgroupSeparator A separator string to use between the name of this group and the name of any + subgroups if this group is flattened. AUv3 and VST3 plug-ins can have multiple + layers of nested subgroups, but AU plug-ins cannot have any subgroups. + + */ + AudioProcessorParameterGroup (const String& groupID, const String& groupName, const String& subgroupSeparator) + : identifier (groupID), name (groupName), separator (subgroupSeparator) + { + } + + /** Creates an AudioProcessorParameterGroup with a single child. + + @param groupID A unique identifier for the group. Keep it basic; don't use any special + characters like "." and avoid pure integer strings which could collide with + legacy parameter IDs. + @param groupName The group's name, which will be displayed in the host. + @param subgroupSeparator A separator string to use between the name of this group and the name of any + subgroups if this group is flattened. AUv3 and VST3 plug-ins can have multiple + layers of nested subgroups, but AU plug-ins cannot have any subgroups. + @param child An AudioProcessorParameter or an AudioProcessorParameterGroup to add to the group. + */ + template + AudioProcessorParameterGroup (const String& groupID, const String& groupName, const String& subgroupSeparator, + std::unique_ptr child) + : AudioProcessorParameterGroup (groupID, groupName, subgroupSeparator) + { + addChild (std::move (child)); + } + + /** Creates an AudioProcessorParameterGroup with multiple children. + + @param groupID A unique identifier for the group. Keep it basic; don't use any special + characters like "." and avoid pure integer strings which could collide with + legacy parameter IDs. + @param groupName The group's name, which will be displayed in the host. + @param subgroupSeparator A separator string to use between the name of this group and the name of any + subgroups if this group is flattened. AUv3 and VST3 plug-ins can have multiple + layers of nested subgroups, but AU plug-ins cannot have any subgroups. + @param firstChild An AudioProcessorParameter or an AudioProcessorParameterGroup to add to the group. + @param remainingChildren A list of more AudioProcessorParameters or AudioProcessorParameterGroups to add to the group. + */ + template + AudioProcessorParameterGroup (const String& groupID, const String& groupName, const String& subgroupSeparator, + std::unique_ptr firstChild, Args&&... remainingChildren) + : AudioProcessorParameterGroup (groupID, groupName, subgroupSeparator, std::move (firstChild)) + { + addChild (std::forward (remainingChildren)...); + } + + //============================================================================== + /** Returns the group's ID. */ + String getID() const { return identifier; } + + /** Returns the group's name. */ + String getName() const { return name; } + + /** Returns the group's separator string. */ + String getSeparator() const { return separator; } + + /** Returns the parent of the group, or nullptr if this is a top-levle group. */ + const AudioProcessorParameterGroup* getParent() const noexcept { return parent; } + + //============================================================================== + const AudioProcessorParameterNode** begin() const noexcept { return children.begin(); } + const AudioProcessorParameterNode** end() const noexcept { return children.end(); } + + //============================================================================== + /** Swaps the content of this group with another. */ + void swapWith (AudioProcessorParameterGroup& other) noexcept + { + children.swapWith (other.children); + + auto refreshParentPtr = [] (AudioProcessorParameterGroup& parentGroup) + { + for (auto* child : parentGroup) + if (auto* group = child->getGroup()) + group->parent = &parentGroup; + }; + + refreshParentPtr (*this); + refreshParentPtr (other); + } + + //============================================================================== + /** Returns all subgroups of this group. + + @param recursive If this is true then this method will fetch all nested + subgroups using a depth first search. + */ + Array getSubgroups (bool recursive) const + { + Array groups; + getSubgroups (groups, recursive); + return groups; + } + + /** Returns all the parameters in this group. + + @param recursive If this is true then this method will fetch all nested + parameters using a depth first search. + */ + Array getParameters (bool recursive) const + { + Array parameters; + getParameters (parameters, recursive); + return parameters; + } + + /** Searches this group recursively for a parameter and returns a depth ordered + list of the groups it belongs to. + */ + Array getGroupsForParameter (AudioProcessorParameter* parameter) const + { + Array groups; + + if (auto* group = getGroupForParameter (parameter)) + { + while (group != this) + { + groups.insert (0, group); + group = group->getParent(); + } + } + + return groups; + } + + //============================================================================== + /** Adds a child to the group. */ + template + void addChild (std::unique_ptr child) + { + // If you hit a compiler error here then you are attempting to add a + // child that is neither a pointer to an AudioProcessorParameterGroup + // nor a pointer to an AudioProcessorParameter. + children.add (new AudioProcessorParameterNode (std::move (child), this)); + } + + /** Adds multiple children to the group. */ + template + void addChild (std::unique_ptr firstChild, Args&&... remainingChildren) + { + addChild (std::move (firstChild)); + addChild (std::forward (remainingChildren)...); + } + +private: + //============================================================================== + void getSubgroups (Array& previousGroups, bool recursive) const + { + for (auto* child : children) + { + if (auto* group = child->getGroup()) + { + previousGroups.add (group); + + if (recursive) + group->getSubgroups (previousGroups, true); + } + } + } + + void getParameters (Array& previousParameters, bool recursive) const + { + for (auto* child : children) + { + if (auto* parameter = child->getParameter()) + previousParameters.add (parameter); + else if (recursive) + child->getGroup()->getParameters (previousParameters, true); + } + } + + const AudioProcessorParameterGroup* getGroupForParameter (AudioProcessorParameter* parameter) const + { + for (auto* child : children) + { + if (child->getParameter() == parameter) + return this; + + if (auto* group = child->getGroup()) + if (auto* foundGroup = group->getGroupForParameter (parameter)) + return foundGroup; + } + + return nullptr; + } + + //============================================================================== + const String identifier, name, separator; + OwnedArray children; + AudioProcessorParameterGroup* parent = nullptr; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorParameterGroup) +}; + +} // namespace juce