diff --git a/extras/AudioPluginHost/Source/GraphEditorPanel.cpp b/extras/AudioPluginHost/Source/GraphEditorPanel.cpp index 35edbbf222..7526fedbee 100644 --- a/extras/AudioPluginHost/Source/GraphEditorPanel.cpp +++ b/extras/AudioPluginHost/Source/GraphEditorPanel.cpp @@ -109,19 +109,40 @@ struct GraphEditorPanel::PinComponent : public Component, }; //============================================================================== -struct GraphEditorPanel::FilterComponent : public Component +struct GraphEditorPanel::FilterComponent : public Component, private AudioProcessorParameter::Listener { FilterComponent (GraphEditorPanel& p, uint32 id) : panel (p), graph (p.graph), pluginID (id) { shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 })); setComponentEffect (&shadow); + if (auto f = graph.graph.getNodeForId (pluginID)) + { + if (auto* processor = f->getProcessor()) + { + if (auto* bypassParam = processor->getBypassParameter()) + bypassParam->addListener (this); + } + } + setSize (150, 60); } FilterComponent (const FilterComponent&) = delete; FilterComponent& operator= (const FilterComponent&) = delete; + ~FilterComponent() + { + if (auto f = graph.graph.getNodeForId (pluginID)) + { + if (auto* processor = f->getProcessor()) + { + if (auto* bypassParam = processor->getBypassParameter()) + bypassParam->removeListener (this); + } + } + } + void mouseDown (const MouseEvent& e) override { originalPos = localPointToGlobal (Point()); @@ -177,8 +198,17 @@ struct GraphEditorPanel::FilterComponent : public Component void paint (Graphics& g) override { auto boxArea = getLocalBounds().reduced (4, pinSize); + bool isBypassed = false; - g.setColour (findColour (TextEditor::backgroundColourId)); + if (auto* f = graph.graph.getNodeForId (pluginID)) + isBypassed = f->isBypassed(); + + auto boxColour = findColour (TextEditor::backgroundColourId); + + if (isBypassed) + boxColour = boxColour.brighter(); + + g.setColour (boxColour); g.fillRect (boxArea.toFloat()); g.setColour (findColour (TextEditor::textColourId)); @@ -290,6 +320,7 @@ struct GraphEditorPanel::FilterComponent : public Component PopupMenu m; m.addItem (1, "Delete this filter"); m.addItem (2, "Disconnect all pins"); + m.addItem (3, "Toggle Bypass"); m.addSeparator(); m.addItem (10, "Show plugin GUI"); m.addItem (11, "Show all programs"); @@ -302,6 +333,15 @@ struct GraphEditorPanel::FilterComponent : public Component { case 1: graph.graph.removeNode (pluginID); break; case 2: graph.graph.disconnectNode (pluginID); break; + case 3: + { + if (auto* node = graph.graph.getNodeForId (pluginID)) + node->setBypassed (! node->isBypassed()); + + repaint(); + + break; + } case 10: showWindow (PluginWindow::Type::normal); break; case 11: showWindow (PluginWindow::Type::programs); break; case 12: showWindow (PluginWindow::Type::generic); break; @@ -329,6 +369,13 @@ struct GraphEditorPanel::FilterComponent : public Component w->toFront (true); } + void parameterValueChanged (int, float) override + { + repaint(); + } + + void parameterGestureChanged (int, bool) override {} + GraphEditorPanel& panel; FilterGraph& graph; const AudioProcessorGraph::NodeID pluginID; diff --git a/modules/juce_audio_plugin_client/AAX/juce_AAX_Wrapper.cpp b/modules/juce_audio_plugin_client/AAX/juce_AAX_Wrapper.cpp index 077623b720..90bef787af 100644 --- a/modules/juce_audio_plugin_client/AAX/juce_AAX_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/AAX/juce_AAX_Wrapper.cpp @@ -133,11 +133,6 @@ namespace AAXClasses jassert (result == AAX_SUCCESS); ignoreUnused (result); } - static bool isBypassParam (AAX_CParamID paramID) noexcept - { - return AAX::IsParameterIDEqual (paramID, cDefaultMasterBypassID) != 0; - } - // maps a channel index of an AAX format to an index of a juce format struct AAXChannelStreamOrder { @@ -483,10 +478,12 @@ namespace AAXClasses { if (component != nullptr && component->pluginEditor != nullptr) { - if (! isBypassParam (paramID)) + auto index = getParamIndexFromID (paramID); + + if (index >= 0) { AudioProcessorEditor::ParameterControlHighlightInfo info; - info.parameterIndex = getParamIndexFromID (paramID); + info.parameterIndex = index; info.isHighlighted = (isHighlighted != 0); info.suggestedColour = getColourFromHighlightEnum (colour); @@ -660,7 +657,6 @@ namespace AAXClasses if (err != AAX_SUCCESS) return err; - addBypassParameter(); addAudioProcessorParameters(); return AAX_SUCCESS; @@ -804,21 +800,13 @@ namespace AAXClasses AAX_Result UpdateParameterNormalizedValue (AAX_CParamID paramID, double value, AAX_EUpdateSource source) override { auto result = AAX_CEffectParameters::UpdateParameterNormalizedValue (paramID, value, source); - - if (! isBypassParam (paramID)) - setAudioProcessorParameter (paramID, value); + setAudioProcessorParameter (paramID, value); return result; } AAX_Result GetParameterValueFromString (AAX_CParamID paramID, double* result, const AAX_IString& text) const override { - if (isBypassParam (paramID)) - { - *result = (text.Get()[0] == 'B') ? 1.0 : 0.0; - return AAX_SUCCESS; - } - if (auto* param = getParameterFromID (paramID)) { if (! LegacyAudioParameter::isLegacy (param)) @@ -833,39 +821,22 @@ namespace AAXClasses AAX_Result GetParameterStringFromValue (AAX_CParamID paramID, double value, AAX_IString* result, int32_t maxLen) const override { - if (isBypassParam (paramID)) - { - result->Set (value == 0 ? "Off" : (maxLen >= 8 ? "Bypassed" : "Byp")); - } - else - { - if (auto* param = getParameterFromID (paramID)) - result->Set (param->getText ((float) value, maxLen).toRawUTF8()); - } + if (auto* param = getParameterFromID (paramID)) + result->Set (param->getText ((float) value, maxLen).toRawUTF8()); return AAX_SUCCESS; } AAX_Result GetParameterNumberofSteps (AAX_CParamID paramID, int32_t* result) const { - if (isBypassParam (paramID)) - { - *result = 2; - } - else - { - if (auto* param = getParameterFromID (paramID)) - *result = param->getNumSteps(); - } + if (auto* param = getParameterFromID (paramID)) + *result = param->getNumSteps(); return AAX_SUCCESS; } AAX_Result GetParameterNormalizedValue (AAX_CParamID paramID, double* result) const override { - if (isBypassParam (paramID)) - return AAX_CEffectParameters::GetParameterNormalizedValue (paramID, result); - if (auto* param = getParameterFromID (paramID)) *result = (double) param->getValue(); else @@ -876,9 +847,6 @@ namespace AAXClasses AAX_Result SetParameterNormalizedValue (AAX_CParamID paramID, double newValue) override { - if (isBypassParam (paramID)) - return AAX_CEffectParameters::SetParameterNormalizedValue (paramID, newValue); - if (auto* p = mParameterManager.GetParameterByID (paramID)) p->SetValueWithFloat ((float) newValue); @@ -889,9 +857,6 @@ namespace AAXClasses AAX_Result SetParameterNormalizedRelative (AAX_CParamID paramID, double newDeltaValue) override { - if (isBypassParam (paramID)) - return AAX_CEffectParameters::SetParameterNormalizedRelative (paramID, newDeltaValue); - if (auto* param = getParameterFromID (paramID)) { auto newValue = param->getValue() + (float) newDeltaValue; @@ -907,25 +872,15 @@ namespace AAXClasses AAX_Result GetParameterNameOfLength (AAX_CParamID paramID, AAX_IString* result, int32_t maxLen) const override { - if (isBypassParam (paramID)) - { - result->Set (maxLen >= 13 ? "Master Bypass" - : (maxLen >= 8 ? "Mast Byp" - : (maxLen >= 6 ? "MstByp" : "MByp"))); - } - else if (auto* param = getParameterFromID (paramID)) - { + if (auto* param = getParameterFromID (paramID)) result->Set (param->getName (maxLen).toRawUTF8()); - } return AAX_SUCCESS; } AAX_Result GetParameterName (AAX_CParamID paramID, AAX_IString* result) const override { - if (isBypassParam (paramID)) - result->Set ("Master Bypass"); - else if (auto* param = getParameterFromID (paramID)) + if (auto* param = getParameterFromID (paramID)) result->Set (param->getName (31).toRawUTF8()); return AAX_SUCCESS; @@ -933,15 +888,12 @@ namespace AAXClasses AAX_Result GetParameterDefaultNormalizedValue (AAX_CParamID paramID, double* result) const override { - if (! isBypassParam (paramID)) - { - if (auto* param = getParameterFromID (paramID)) - *result = (double) param->getDefaultValue(); - else - *result = 0.0; + if (auto* param = getParameterFromID (paramID)) + *result = (double) param->getDefaultValue(); + else + *result = 0.0; - jassert (*result >= 0 && *result <= 1.0f); - } + jassert (*result >= 0 && *result <= 1.0f); return AAX_SUCCESS; } @@ -1420,18 +1372,18 @@ namespace AAXClasses #endif } - void addBypassParameter() + bool isBypassPartOfRegularParemeters() const { - auto* masterBypass = new AAX_CParameter (cDefaultMasterBypassID, - AAX_CString ("Master Bypass"), - false, - AAX_CBinaryTaperDelegate(), - AAX_CBinaryDisplayDelegate ("bypass", "on"), - true); - masterBypass->SetNumberOfSteps (2); - masterBypass->SetType (AAX_eParameterType_Discrete); - mParameterManager.AddParameter (masterBypass); - mPacketDispatcher.RegisterPacket (cDefaultMasterBypassID, JUCEAlgorithmIDs::bypass); + auto& audioProcessor = getPluginInstance(); + + int n = juceParameters.getNumParameters(); + + if (auto* bypassParam = audioProcessor.getBypassParameter()) + for (int i = 0; i < n; ++i) + if (juceParameters.getParamForIndex (i) == bypassParam) + return true; + + return false; } void addAudioProcessorParameters() @@ -1444,14 +1396,35 @@ namespace AAXClasses const bool forceLegacyParamIDs = false; #endif + auto bypassPartOfRegularParams = isBypassPartOfRegularParemeters(); + juceParameters.update (audioProcessor, forceLegacyParamIDs); + + bool aaxWrapperProvidedBypassParam = false; + auto* bypassParameter = pluginInstance->getBypassParameter(); + + if (bypassParameter == nullptr) + { + aaxWrapperProvidedBypassParam = true; + + ownedBypassParameter = new AudioParameterBool (cDefaultMasterBypassID, "Master Bypass", false, {}, {}, {}); + + bypassParameter = ownedBypassParameter; + } + + if (! bypassPartOfRegularParams) + juceParameters.params.add (bypassParameter); + int parameterIndex = 0; for (auto* juceParam : juceParameters.params) { + auto isBypassParameter = (juceParam == bypassParameter); + auto category = juceParam->getCategory(); - auto paramID = juceParameters.getParamID (audioProcessor, parameterIndex) - .toRawUTF8(); + auto paramID = isBypassParameter ? cDefaultMasterBypassID + : juceParameters.getParamID (audioProcessor, parameterIndex) + .toRawUTF8(); aaxParamIDs.add (paramID); auto aaxParamID = aaxParamIDs.getReference (parameterIndex++).getCharPointer(); @@ -1493,6 +1466,9 @@ namespace AAXClasses | AAX_eParameterOrientation_RotarySingleDotMode | AAX_eParameterOrientation_RotaryLeftMinRightMax)); mParameterManager.AddParameter (parameter); + + if (isBypassParameter) + mPacketDispatcher.RegisterPacket (aaxParamID, JUCEAlgorithmIDs::bypass); } } @@ -1785,6 +1761,7 @@ namespace AAXClasses Array aaxParamIDs; HashMap paramMap; LegacyAudioParametersWrapper juceParameters; + ScopedPointer ownedBypassParameter; Array aaxMeters; 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 a2d77db24b..855eb6d583 100644 --- a/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm +++ b/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm @@ -121,7 +121,8 @@ class JuceAU : public AudioProcessorHolder, public MusicDeviceBase, public AudioProcessorListener, public AudioPlayHead, - public ComponentListener + public ComponentListener, + public AudioProcessorParameter::Listener { public: JuceAU (AudioUnit component) @@ -181,6 +182,9 @@ public: ~JuceAU() { + if (bypassParam != nullptr) + bypassParam->removeListener (this); + deleteActiveEditors(); juceFilter = nullptr; clearPresetsArray(); @@ -484,7 +488,10 @@ public: return noErr; case kAudioUnitProperty_BypassEffect: - *(UInt32*) outData = isBypassed ? 1 : 0; + if (bypassParam != nullptr) + *(UInt32*) outData = (bypassParam->getValue() != 0.0f ? 1 : 0); + else + *(UInt32*) outData = isBypassed ? 1 : 0; return noErr; case kAudioUnitProperty_SupportsMPE: @@ -605,12 +612,16 @@ public: return kAudioUnitErr_InvalidPropertyValue; const bool newBypass = *((UInt32*) inData) != 0; + const bool currentlyBypassed = (bypassParam != nullptr ? (bypassParam->getValue() != 0.0f) : isBypassed); - if (newBypass != isBypassed) + if (newBypass != currentlyBypassed) { - isBypassed = newBypass; + if (bypassParam != nullptr) + bypassParam->setValueNotifyingHost (newBypass ? 1.0f : 0.0f); + else + isBypassed = newBypass; - if (! isBypassed && IsInitialized()) // turning bypass off and we're initialized + if (! currentlyBypassed && IsInitialized()) // turning bypass off and we're initialized Reset (0, 0); } @@ -1117,6 +1128,15 @@ public: PropertyChanged (kAudioUnitProperty_PresentPreset, kAudioUnitScope_Global, 0); } + //============================================================================== + // this will only ever be called by the bypass parameter + void parameterValueChanged (int, float) override + { + PropertyChanged (kAudioUnitProperty_BypassEffect, kAudioUnitScope_Global, 0); + } + + void parameterGestureChanged (int, bool) override {} + //============================================================================== bool StreamFormatWritable (AudioUnitScope scope, AudioUnitElement element) override { @@ -1690,6 +1710,9 @@ private: //============================================================================== OwnedArray> parameterValueStringArrays; + //============================================================================== + AudioProcessorParameter* bypassParam = nullptr; + //============================================================================== void pullInputAudio (AudioUnitRenderActionFlags& flags, const AudioTimeStamp& timestamp, const UInt32 nFrames) noexcept { @@ -1730,7 +1753,7 @@ private: { buffer.clear(); } - else if (isBypassed) + else if (bypassParam == nullptr && isBypassed) { juceFilter->processBlockBypassed (buffer, midiBuffer); } @@ -1878,6 +1901,9 @@ private: parameterValueStringArrays.add (stringValues); } + + if ((bypassParam = juceFilter->getBypassParameter()) != nullptr) + bypassParam->addListener (this); } //============================================================================== 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 a11025afb4..31e90070fc 100644 --- a/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm +++ b/modules/juce_audio_plugin_client/AU/juce_AUv3_Wrapper.mm @@ -195,6 +195,18 @@ public: virtual bool getRenderingOffline() = 0; virtual void setRenderingOffline (bool offline) = 0; + virtual bool getShouldBypassEffect() + { + objc_super s = { getAudioUnit(), [AUAudioUnit class] }; + return (ObjCMsgSendSuper (&s, @selector (shouldBypassEffect)) == YES); + } + + virtual void setShouldBypassEffect (bool shouldBypass) + { + objc_super s = { getAudioUnit(), [AUAudioUnit class] }; + ObjCMsgSendSuper (&s, @selector (setShouldBypassEffect:), shouldBypass ? YES : NO); + } + //============================================================================== virtual NSString* getContextName() const = 0; virtual void setContextName (NSString*) = 0; @@ -274,6 +286,8 @@ private: addMethod (@selector (canProcessInPlace), getCanProcessInPlace, @encode (BOOL), "@:"); addMethod (@selector (isRenderingOffline), getRenderingOffline, @encode (BOOL), "@:"); addMethod (@selector (setRenderingOffline:), setRenderingOffline, "v@:", @encode (BOOL)); + addMethod (@selector (shouldBypassEffect), getShouldBypassEffect, @encode (BOOL), "@:"); + addMethod (@selector (setShouldBypassEffect:), setShouldBypassEffect, "v@:", @encode (BOOL)); addMethod (@selector (allocateRenderResourcesAndReturnError:), allocateRenderResourcesAndReturnError, "B@:^@"); addMethod (@selector (deallocateRenderResources), deallocateRenderResources, "v@:"); @@ -388,6 +402,8 @@ private: static void setRenderingOffline (id self, SEL, BOOL renderingOffline) { _this (self)->setRenderingOffline (renderingOffline); } static BOOL allocateRenderResourcesAndReturnError (id self, SEL, NSError** error) { return _this (self)->allocateRenderResourcesAndReturnError (error) ? YES : NO; } static void deallocateRenderResources (id self, SEL) { _this (self)->deallocateRenderResources(); } + static BOOL getShouldBypassEffect (id self, SEL) { return _this (self)->getShouldBypassEffect() ? YES : NO; } + static void setShouldBypassEffect (id self, SEL, BOOL shouldBypass) { _this (self)->setShouldBypassEffect (shouldBypass); } //============================================================================== static NSString* getContextName (id self, SEL) { return _this (self)->getContextName(); } @@ -417,7 +433,8 @@ JuceAudioUnitv3Base::Class JuceAudioUnitv3Base::audioUnitObjCClass; //============================================================================== class JuceAudioUnitv3 : public JuceAudioUnitv3Base, public AudioProcessorListener, - public AudioPlayHead + public AudioPlayHead, + private AudioProcessorParameter::Listener { public: JuceAudioUnitv3 (const AudioProcessorHolder::Ptr& processor, @@ -444,6 +461,9 @@ public: auto& processor = getAudioProcessor(); processor.removeListener (this); + if (bypassParam != nullptr) + bypassParam->removeListener (this); + removeEditor (processor); if (editorObserverToken != nullptr) @@ -735,6 +755,22 @@ public: } } + bool getShouldBypassEffect() override + { + if (bypassParam != nullptr) + return (bypassParam->getValue() != 0.0f); + + return JuceAudioUnitv3Base::getShouldBypassEffect(); + } + + void setShouldBypassEffect (bool shouldBypass) override + { + if (bypassParam != nullptr) + bypassParam->setValue (shouldBypass ? 1.0f : 0.0f); + + JuceAudioUnitv3Base::setShouldBypassEffect (shouldBypass); + } + //============================================================================== NSString* getContextName() const override { return juceStringToNS (contextName); } void setContextName (NSString* str) override @@ -1216,7 +1252,6 @@ private: #endif // create methods in AUParameterTree return unretained objects (!) -> see Apple header AUAudioUnitImplementation.h - ScopedPointer param = [[AUParameterTree createParameterWithIdentifier: juceStringToNS (identifier) name: juceStringToNS (name) address: address @@ -1252,6 +1287,9 @@ private: editorParamObserver = CreateObjCBlock (this, &JuceAudioUnitv3::valueChangedForObserver); editorObserverToken = [paramTree tokenByAddingParameterObserver: editorParamObserver]; } + + if ((bypassParam = processor.getBypassParameter()) != nullptr) + bypassParam->addListener (this); } void setAudioProcessorParameter (AudioProcessorParameter* juceParam, float value) @@ -1454,7 +1492,7 @@ private: if (processor.isSuspended()) buffer.clear(); - else if ([au shouldBypassEffect]) + else if (bypassParam != nullptr && [au shouldBypassEffect]) processor.processBlockBypassed (buffer, midiBuffer); else processor.processBlock (buffer, midiBuffer); @@ -1526,6 +1564,14 @@ private: } //============================================================================== + // this is only ever called for the bypass parameter + void parameterValueChanged (int, float newValue) override + { + JuceAudioUnitv3Base::setShouldBypassEffect (newValue != 0.0f); + } + + void parameterGestureChanged (int, bool) override {} + //============================================================================== #if JUCE_FORCE_USE_LEGACY_PARAM_IDS inline AUParameterAddress getAUParameterAddressForIndex (int paramIndex) const noexcept { return static_cast (paramIndex); } inline int getJuceParameterIndexForAUAddress (AUParameterAddress address) const noexcept { return static_cast (address); } @@ -1610,6 +1656,7 @@ private: #else static constexpr bool forceLegacyParamIDs = false; #endif + AudioProcessorParameter* bypassParam = nullptr; }; const double JuceAudioUnitv3::kDefaultSampleRate = 44100.0; diff --git a/modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp b/modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp index be79a94052..52c364c41f 100644 --- a/modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp @@ -223,7 +223,8 @@ struct AbletonLiveHostSpecific class JuceVSTWrapper : public AudioProcessorListener, public AudioPlayHead, private Timer, - private AsyncUpdater + private AsyncUpdater, + private AudioProcessorParameter::Listener { private: //============================================================================== @@ -282,6 +283,9 @@ public: processor->setPlayHead (this); processor->addListener (this); + if (auto* juceParam = processor->getBypassParameter()) + juceParam->addListener (this); + juceParameters.update (*processor, false); memset (&vstEffect, 0, sizeof (vstEffect)); @@ -760,6 +764,14 @@ public: hostCallback (&vstEffect, hostOpcodeParameterChangeGestureEnd, index, 0, 0, 0); } + void parameterValueChanged (int, float newValue) override + { + // this can only come from the bypass parameter + isBypassed = (newValue != 0.0f); + } + + void parameterGestureChanged (int, bool) override {} + void audioProcessorChanged (AudioProcessor*) override { vstEffect.latency = processor->getLatencySamples(); @@ -1921,6 +1933,10 @@ private: pointer_sized_int handleSetBypass (VstOpCodeArguments args) { isBypassed = (args.value != 0); + + if (auto* bypass = processor->getBypassParameter()) + bypass->setValueNotifyingHost (isBypassed ? 1.0f : 0.0f); + return 1; } 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 2b4186a260..b0d250f999 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -119,6 +119,11 @@ public: return paramMap[static_cast (paramID)]; } + AudioProcessorParameter* getBypassParameter() const noexcept + { + return getParamForVSTParamID (bypassParamID); + } + int getNumParameters() const noexcept { return vstParamIDs.size(); } bool isUsingManagedParameters() const noexcept { return juceParameters.isUsingManagedParameters(); } @@ -126,7 +131,7 @@ public: static const FUID iid; Array vstParamIDs; Vst::ParamID bypassParamID = 0; - bool isBypassed = false; + bool bypassIsRegularParameter = false; private: enum InternalParameters @@ -135,6 +140,18 @@ private: }; //============================================================================== + bool isBypassPartOfRegularParemeters() const + { + int n = juceParameters.getNumParameters(); + + if (auto* bypassParam = audioProcessor->getBypassParameter()) + for (int i = 0; i < n; ++i) + if (juceParameters.getParamForIndex (i) == bypassParam) + return true; + + return false; + } + void setupParameters() { #if JUCE_FORCE_USE_LEGACY_PARAM_IDS @@ -146,17 +163,42 @@ private: juceParameters.update (*audioProcessor, forceLegacyParamIDs); auto numParameters = juceParameters.getNumParameters(); + bool vst3WrapperProvidedBypassParam = false; + auto* bypassParameter = audioProcessor->getBypassParameter(); + + if (bypassParameter == nullptr) + { + vst3WrapperProvidedBypassParam = true; + bypassParameter = ownedBypassParameter = new AudioParameterBool ("byps", "Bypass", false, {}, {}, {}); + } + + // if the bypass parameter is not part of the exported parameters that the plug-in supports + // then add it to the end of the list as VST3 requires the bypass parameter to be exported! + bypassIsRegularParameter = isBypassPartOfRegularParemeters(); + + if (! bypassIsRegularParameter) + juceParameters.params.add (bypassParameter); + int i = 0; for (auto* juceParam : juceParameters.params) { + bool isBypassParameter = (juceParam == bypassParameter); + Vst::ParamID vstParamID = forceLegacyParamIDs ? static_cast (i++) : generateVSTParamIDForParam (juceParam); + if (isBypassParameter) + { + // we need to remain backward compatible with the old bypass id + if (vst3WrapperProvidedBypassParam) + vstParamID = static_cast (isUsingManagedParameters() ? paramBypass : numParameters); + + bypassParamID = vstParamID; + } + vstParamIDs.add (vstParamID); paramMap.set (static_cast (vstParamID), juceParam); } - - bypassParamID = static_cast (isUsingManagedParameters() ? paramBypass : numParameters); } Vst::ParamID generateVSTParamIDForParam (AudioProcessorParameter* param) @@ -181,6 +223,7 @@ private: //============================================================================== LegacyAudioParametersWrapper juceParameters; HashMap paramMap; + ScopedPointer ownedBypassParameter; JuceAudioProcessor() = delete; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceAudioProcessor) @@ -194,7 +237,8 @@ static ThreadLocalValue inParameterChangedCallback; class JuceVST3EditController : public Vst::EditController, public Vst::IMidiMapping, public Vst::ChannelContext::IInfoListener, - public AudioProcessorListener + public AudioProcessorListener, + private AudioProcessorParameter::Listener { public: JuceVST3EditController (Vst::IHostApplication* host) @@ -279,7 +323,7 @@ public: struct Param : public Vst::Parameter { Param (JuceVST3EditController& editController, AudioProcessorParameter& p, - Vst::ParamID vstParamID, bool forceLegacyParamIDs) + Vst::ParamID vstParamID, bool isBypassParameter, bool forceLegacyParamIDs) : owner (editController), param (p) { info.id = vstParamID; @@ -306,6 +350,9 @@ public: else info.flags = param.isAutomatable() ? Vst::ParameterInfo::kCanAutomate : 0; + if (isBypassParameter) + info.flags |= Vst::ParameterInfo::kIsBypass; + valueNormalized = info.defaultNormalizedValue; } @@ -370,88 +417,6 @@ public: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Param) }; - //============================================================================== - struct BypassParam : public Vst::Parameter - { - BypassParam (Vst::ParamID vstParamID) - { - info.id = vstParamID; - toString128 (info.title, "Bypass"); - toString128 (info.shortTitle, "Bypass"); - toString128 (info.units, ""); - info.stepCount = 1; - info.defaultNormalizedValue = 0.0f; - info.unitId = Vst::kRootUnitId; - info.flags = Vst::ParameterInfo::kIsBypass | Vst::ParameterInfo::kCanAutomate; - } - - virtual ~BypassParam() {} - - bool setNormalized (Vst::ParamValue v) override - { - bool bypass = (v != 0.0f); - v = (bypass ? 1.0f : 0.0f); - - if (valueNormalized != v) - { - valueNormalized = v; - changed(); - return true; - } - - return false; - } - - void toString (Vst::ParamValue value, Vst::String128 result) const override - { - bool bypass = (value != 0.0f); - toString128 (result, bypass ? "On" : "Off"); - } - - bool fromString (const Vst::TChar* text, Vst::ParamValue& outValueNormalized) const override - { - auto paramValueString = getStringFromVstTChars (text); - - if (paramValueString.equalsIgnoreCase ("on") - || paramValueString.equalsIgnoreCase ("yes") - || paramValueString.equalsIgnoreCase ("true")) - { - outValueNormalized = 1.0f; - return true; - } - - if (paramValueString.equalsIgnoreCase ("off") - || paramValueString.equalsIgnoreCase ("no") - || paramValueString.equalsIgnoreCase ("false")) - { - outValueNormalized = 0.0f; - return true; - } - - var varValue = JSON::fromString (paramValueString); - - if (varValue.isDouble() || varValue.isInt() - || varValue.isInt64() || varValue.isBool()) - { - double value = varValue; - outValueNormalized = (value != 0.0) ? 1.0f : 0.0f; - return true; - } - - return false; - } - - static String getStringFromVstTChars (const Vst::TChar* text) - { - return juce::String (juce::CharPointer_UTF16 (reinterpret_cast (text))); - } - - Vst::ParamValue toPlain (Vst::ParamValue v) const override { return v; } - Vst::ParamValue toNormalized (Vst::ParamValue v) const override { return v; } - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BypassParam) - }; - //============================================================================== struct ProgramChangeParameter : public Vst::Parameter { @@ -696,6 +661,19 @@ public: componentHandler->restartComponent (Vst::kLatencyChanged | Vst::kParamValuesChanged); } + void parameterValueChanged (int, float newValue) override + { + // this can only come from the bypass parameter + paramChanged (audioProcessor->bypassParamID, newValue); + } + + void parameterGestureChanged (int, bool gestureIsStarting) override + { + // this can only come from the bypass parameter + if (gestureIsStarting) beginEdit (audioProcessor->bypassParamID); + else endEdit (audioProcessor->bypassParamID); + } + //============================================================================== AudioProcessor* getPluginInstance() const noexcept { @@ -732,6 +710,11 @@ private: { pluginInstance->addListener (this); + // as the bypass is not part of the regular parameters + // we need to listen for it explicitly + if (! audioProcessor->bypassIsRegularParameter) + audioProcessor->getBypassParameter()->addListener (this); + if (parameters.getParameterCount() <= 0) { #if JUCE_FORCE_USE_LEGACY_PARAM_IDS @@ -747,11 +730,10 @@ private: auto vstParamID = audioProcessor->getVSTParamIDForIndex (i); auto* juceParam = audioProcessor->getParamForVSTParamID (vstParamID); - parameters.addParameter (new Param (*this, *juceParam, vstParamID, forceLegacyParamIDs)); + parameters.addParameter (new Param (*this, *juceParam, vstParamID, + (vstParamID == audioProcessor->bypassParamID), forceLegacyParamIDs)); } - parameters.addParameter (new BypassParam (audioProcessor->bypassParamID)); - if (pluginInstance->getNumPrograms() > 1) parameters.addParameter (new ProgramChangeParameter (*pluginInstance)); } @@ -1331,24 +1313,50 @@ public: tresult PLUGIN_API setIoMode (Vst::IoMode) override { return kNotImplemented; } tresult PLUGIN_API getRoutingInfo (Vst::RoutingInfo&, Vst::RoutingInfo&) override { return kNotImplemented; } - bool isBypassed() { return comPluginInstance->isBypassed; } - void setBypassed (bool bypassed) { comPluginInstance->isBypassed = bypassed; } + //============================================================================== + bool isBypassed() + { + if (auto* bypassParam = comPluginInstance->getBypassParameter()) + return (bypassParam->getValue() != 0.0f); + + return false; + } + + void setBypassed (bool shouldBeBypassed) + { + if (auto* bypassParam = comPluginInstance->getBypassParameter()) + { + auto floatValue = (shouldBeBypassed ? 1.0f : 0.0f); + bypassParam->setValue (floatValue); + + inParameterChangedCallback = true; + bypassParam->sendValueChangedMessageToListeners (floatValue); + } + } //============================================================================== void writeJucePrivateStateInformation (MemoryOutputStream& out) { - ValueTree privateData (kJucePrivateDataIdentifier); + if (pluginInstance->getBypassParameter() == nullptr) + { + ValueTree privateData (kJucePrivateDataIdentifier); - // for now we only store the bypass value - privateData.setProperty ("Bypass", var (isBypassed()), nullptr); - - privateData.writeToStream (out); + // for now we only store the bypass value + privateData.setProperty ("Bypass", var (isBypassed()), nullptr); + privateData.writeToStream (out); + } } void setJucePrivateStateInformation (const void* data, int sizeInBytes) { - auto privateData = ValueTree::readFromData (data, static_cast (sizeInBytes)); - setBypassed (static_cast (privateData.getProperty ("Bypass", var (false)))); + if (pluginInstance->getBypassParameter() == nullptr) + { + auto privateData = ValueTree::readFromData (data, static_cast (sizeInBytes)); + auto isBypassed = static_cast (privateData.getProperty ("Bypass", var (false))); + + if (auto* bypassParam = comPluginInstance->getBypassParameter()) + setBypassed (isBypassed ? 1.0f : 0.0f); + } } void getStateInformation (MemoryBlock& destData) @@ -2040,13 +2048,7 @@ public: { auto vstParamID = paramQueue->getParameterId(); - if (vstParamID == comPluginInstance->bypassParamID) - setBypassed (static_cast (value) != 0.0f); - #if JUCE_VST3_EMULATE_MIDI_CC_WITH_PARAMETERS - else if (juceVST3EditController->isMidiControllerParamID (vstParamID)) - addParameterChangeToMidiBuffer (offsetSamples, vstParamID, value); - #endif - else if (vstParamID == JuceVST3EditController::paramPreset) + if (vstParamID == JuceVST3EditController::paramPreset) { auto numPrograms = pluginInstance->getNumPrograms(); auto programValue = roundToInt (value * (jmax (0, numPrograms - 1))); @@ -2055,6 +2057,10 @@ public: && programValue != pluginInstance->getCurrentProgram()) pluginInstance->setCurrentProgram (programValue); } + #if JUCE_VST3_EMULATE_MIDI_CC_WITH_PARAMETERS + else if (juceVST3EditController->isMidiControllerParamID (vstParamID)) + addParameterChangeToMidiBuffer (offsetSamples, vstParamID, value); + #endif else { auto floatValue = static_cast (value); diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index 938a7d0fd1..8dbdd6e87a 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -1010,10 +1010,20 @@ public: for (int i = 0; i < getBusCount (false); ++i) AudioUnitReset (audioUnit, kAudioUnitScope_Output, static_cast (i)); } - void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override + void processAudio (AudioBuffer& buffer, MidiBuffer& midiMessages, bool processBlockBypassedCalled) { auto numSamples = buffer.getNumSamples(); + if (auSupportsBypass) + { + updateBypass (processBlockBypassedCalled); + } + else if (processBlockBypassedCalled) + { + AudioProcessor::processBlockBypassed (buffer, midiMessages); + return; + } + if (prepared) { timeStamp.mHostTime = GetCurrentHostTime (numSamples, getSampleRate(), isAUv3); @@ -1111,7 +1121,18 @@ public: } } + void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + processAudio (buffer, midiMessages, false); + } + + void processBlockBypassed (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + processAudio (buffer, midiMessages, true); + } + //============================================================================== + AudioProcessorParameter* getBypassParameter() const override { return auSupportsBypass ? bypassParam.get() : nullptr; } bool hasEditor() const override { return true; } AudioProcessorEditor* createEditor() override; @@ -1408,6 +1429,14 @@ public: } } } + + UInt32 propertySize = 0; + Boolean writable = false; + + auSupportsBypass = (AudioUnitGetPropertyInfo (audioUnit, kAudioUnitProperty_BypassEffect, + kAudioUnitScope_Global, 0, &propertySize, &writable) == noErr + && propertySize >= sizeof (UInt32) && writable); + bypassParam = new AUBypassParameter (*this); } void updateLatency() @@ -1439,7 +1468,7 @@ private: String pluginName, manufacturer, version; String fileOrIdentifier; CriticalSection lock; - bool wantsMidiMessages, producesMidiMessages, wasPlaying, prepared, isAUv3, isMidiEffectPlugin; + bool wantsMidiMessages, producesMidiMessages, wasPlaying, prepared, isAUv3, isMidiEffectPlugin, lastBypassValue = false; struct AUBuffer { @@ -1459,6 +1488,99 @@ private: HeapBlock bufferList; }; + //============================================================================== + struct AUBypassParameter : Parameter + { + AUBypassParameter (AudioUnitPluginInstance& effectToUse) + : parent (effectToUse), currentValue (getCurrentHostValue()) + {} + + bool getCurrentHostValue() + { + if (parent.auSupportsBypass) + { + UInt32 dataSize = sizeof (UInt32); + UInt32 value = 0; + + if (AudioUnitGetProperty (parent.audioUnit, kAudioUnitProperty_BypassEffect, + kAudioUnitScope_Global, 0, &value, &dataSize) == noErr + && dataSize == sizeof (UInt32)) + return (value != 0); + } + + return false; + } + + float getValue() const override + { + return currentValue ? 1.0f : 0.0f; + } + + void setValue (float newValue) override + { + auto newBypassValue = (newValue != 0.0f); + + const ScopedLock sl (parent.lock); + + if (newBypassValue != currentValue) + { + currentValue = newBypassValue; + + if (parent.auSupportsBypass) + { + UInt32 value = (newValue != 0.0f ? 1 : 0); + AudioUnitSetProperty (parent.audioUnit, kAudioUnitProperty_BypassEffect, + kAudioUnitScope_Global, 0, &value, sizeof (UInt32)); + + #if JUCE_MAC + jassert (parent.audioUnit != nullptr); + + AudioUnitEvent ev; + ev.mEventType = kAudioUnitEvent_PropertyChange; + ev.mArgument.mProperty.mAudioUnit = parent.audioUnit; + ev.mArgument.mProperty.mPropertyID = kAudioUnitProperty_BypassEffect; + ev.mArgument.mProperty.mScope = kAudioUnitScope_Global; + ev.mArgument.mProperty.mElement = 0; + + AUEventListenerNotify (parent.eventListenerRef, nullptr, &ev); + #endif + } + } + } + + float getValueForText (const String& text) const override + { + String lowercaseText (text.toLowerCase()); + + for (auto& testText : onStrings) + if (lowercaseText == testText) + return 1.0f; + + for (auto& testText : offStrings) + if (lowercaseText == testText) + return 0.0f; + + return text.getIntValue() != 0 ? 1.0f : 0.0f; + } + + float getDefaultValue() const override { return 0.0f; } + String getName (int /*maximumStringLength*/) const override { return "Bypass"; } + String getText (float value, int) const override { return (value != 0.0f ? TRANS("On") : TRANS("Off")); } + bool isAutomatable() const override { return true; } + bool isDiscrete() const override { return true; } + bool isBoolean() const override { return true; } + int getNumSteps() const override { return 2; } + StringArray getAllValueStrings() const override { return values; } + String getLabel() const override { return {}; } + + AudioUnitPluginInstance& parent; + const StringArray onStrings { TRANS("on"), TRANS("yes"), TRANS("true") }; + const StringArray offStrings { TRANS("off"), TRANS("no"), TRANS("false") }; + const StringArray values { TRANS("Off"), TRANS("On") }; + + bool currentValue = false; + }; + OwnedArray outputBufferList; AudioTimeStamp timeStamp; AudioBuffer* currentBuffer; @@ -1477,6 +1599,8 @@ private: MidiDataConcatenator midiConcatenator; CriticalSection midiInLock; MidiBuffer incomingMidi; + ScopedPointer bypassParam; + bool lastProcessBlockCallWasBypass = false, auSupportsBypass = false; void createPluginCallbacks() { @@ -1536,6 +1660,7 @@ private: addPropertyChangeListener (kAudioUnitProperty_PresentPreset); addPropertyChangeListener (kAudioUnitProperty_ParameterList); addPropertyChangeListener (kAudioUnitProperty_Latency); + addPropertyChangeListener (kAudioUnitProperty_BypassEffect); #endif } } @@ -1607,6 +1732,9 @@ private: sendAllParametersChangedEvents(); else if (event.mArgument.mProperty.mPropertyID == kAudioUnitProperty_Latency) updateLatency(); + else if (event.mArgument.mProperty.mPropertyID == kAudioUnitProperty_BypassEffect) + if (bypassParam != nullptr) + bypassParam->setValueNotifyingHost (bypassParam->getValue()); break; } @@ -2038,6 +2166,23 @@ private: return false; } + //============================================================================== + void updateBypass (bool processBlockBypassedCalled) + { + if (processBlockBypassedCalled && bypassParam != nullptr) + { + if (bypassParam->getValue() == 0.0f || ! lastProcessBlockCallWasBypass) + bypassParam->setValue (1.0f); + } + else + { + if (lastProcessBlockCallWasBypass && bypassParam != nullptr) + bypassParam->setValue (0.0f); + } + + lastProcessBlockCallWasBypass = processBlockBypassedCalled; + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioUnitPluginInstance) }; diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index bf68a9e7cf..466478d62a 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -1883,14 +1883,18 @@ struct VST3PluginInstance : public AudioPluginInstance int numSteps = isDiscrete ? paramInfo.stepCount + 1 : AudioProcessor::getDefaultNumParameterSteps(); - addParameter (new VST3Parameter (*this, - paramInfo.id, - toString (paramInfo.title), - toString (paramInfo.units), - paramInfo.defaultNormalizedValue, - (paramInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0, - isDiscrete, - numSteps)); + VST3Parameter* p = new VST3Parameter (*this, + paramInfo.id, + toString (paramInfo.title), + toString (paramInfo.units), + paramInfo.defaultNormalizedValue, + (paramInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0, + isDiscrete, + numSteps); + addParameter (p); + + if ((paramInfo.flags & Vst::ParameterInfo::kIsBypass) != 0) + bypassParam = p; } synchroniseStates(); @@ -2015,12 +2019,13 @@ struct VST3PluginInstance : public AudioPluginInstance return (processor->canProcessSampleSize (Vst::kSample64) == kResultTrue); } + //============================================================================== void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override { jassert (! isUsingDoublePrecision()); if (isActive && processor != nullptr) - processAudio (buffer, midiMessages, Vst::kSample32); + processAudio (buffer, midiMessages, Vst::kSample32, false); } void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override @@ -2028,12 +2033,43 @@ struct VST3PluginInstance : public AudioPluginInstance jassert (isUsingDoublePrecision()); if (isActive && processor != nullptr) - processAudio (buffer, midiMessages, Vst::kSample64); + processAudio (buffer, midiMessages, Vst::kSample64, false); } + void processBlockBypassed (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + jassert (! isUsingDoublePrecision()); + + if (bypassParam != nullptr) + { + if (isActive && processor != nullptr) + processAudio (buffer, midiMessages, Vst::kSample32, true); + } + else + { + AudioProcessor::processBlockBypassed (buffer, midiMessages); + } + } + + void processBlockBypassed (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + jassert (isUsingDoublePrecision()); + + if (bypassParam != nullptr) + { + if (isActive && processor != nullptr) + processAudio (buffer, midiMessages, Vst::kSample64, true); + } + else + { + AudioProcessor::processBlockBypassed (buffer, midiMessages); + } + } + + //============================================================================== template void processAudio (AudioBuffer& buffer, MidiBuffer& midiMessages, - Vst::SymbolicSampleSizes sampleSize) + Vst::SymbolicSampleSizes sampleSize, bool isProcessBlockBypassedCall) { using namespace Vst; auto numSamples = buffer.getNumSamples(); @@ -2041,6 +2077,8 @@ struct VST3PluginInstance : public AudioPluginInstance auto numInputAudioBuses = getBusCount (true); auto numOutputAudioBuses = getBusCount (false); + updateBypass (isProcessBlockBypassedCall); + ProcessData data; data.processMode = isNonRealtime() ? kOffline : kRealtime; data.symbolicSampleSize = sampleSize; @@ -2266,6 +2304,9 @@ struct VST3PluginInstance : public AudioPluginInstance bool acceptsMidi() const override { return getNumSingleDirectionBusesFor (holder->component, true, false) > 0; } bool producesMidi() const override { return getNumSingleDirectionBusesFor (holder->component, false, false) > 0; } + //============================================================================== + AudioProcessorParameter* getBypassParameter() const override { return bypassParam; } + //============================================================================== /** May return a negative value as a means of informing us that the plugin has "infinite tail," or 0 for "no tail." */ double getTailLengthSeconds() const override @@ -2588,7 +2629,8 @@ private: ComSmartPtr inputParameterChanges, outputParameterChanges; ComSmartPtr midiInputs, midiOutputs; Vst::ProcessContext timingInfo; //< Only use this in processBlock()! - bool isControllerInitialised = false, isActive = false; + bool isControllerInitialised = false, isActive = false, lastProcessBlockCallWasBypass = false; + VST3Parameter* bypassParam = nullptr; //============================================================================== /** Some plugins need to be "connected" to intercommunicate between their implemented classes */ @@ -2705,6 +2747,28 @@ private: return busInfo; } + //============================================================================== + void updateBypass (bool processBlockBypassedCalled) + { + // to remain backward compatible, the logic needs to be the following: + // - if processBlockBypassed was called then definitely bypass the VST3 + // - if processBlock was called then only un-bypass the VST3 if the previous + // call was processBlockBypassed, otherwise do nothing + if (processBlockBypassedCalled) + { + if (bypassParam != nullptr && (bypassParam->getValue() == 0.0f || ! lastProcessBlockCallWasBypass)) + bypassParam->setValue (1.0f); + } + else + { + if (lastProcessBlockCallWasBypass && bypassParam != nullptr) + bypassParam->setValue (0.0f); + + } + + lastProcessBlockCallWasBypass = processBlockBypassedCalled; + } + //============================================================================== /** @note An IPlugView, when first created, should start with a ref-count of 1! */ IPlugView* tryCreatingView() const diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index 8b19413b82..c3c5e31511 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -979,7 +979,8 @@ struct VSTPluginInstance : public AudioPluginInstance, : AudioPluginInstance (ioConfig), vstEffect (effect), vstModule (mh), - name (mh->pluginName) + name (mh->pluginName), + bypassParam (new VST2BypassParameter (*this)) { jassert (vstEffect != nullptr); @@ -1063,6 +1064,7 @@ struct VSTPluginInstance : public AudioPluginInstance, valueType)); } + vstSupportsBypass = pluginCanDo ("bypass"); setRateAndBufferSizeDetails (sampleRateToUse, blockSizeToUse); } @@ -1399,24 +1401,40 @@ struct VSTPluginInstance : public AudioPluginInstance, } } + //============================================================================== void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override { jassert (! isUsingDoublePrecision()); - processAudio (buffer, midiMessages, tmpBufferFloat, channelBufferFloat); + processAudio (buffer, midiMessages, tmpBufferFloat, channelBufferFloat, false); } void processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages) override { jassert (isUsingDoublePrecision()); - processAudio (buffer, midiMessages, tmpBufferDouble, channelBufferDouble); + processAudio (buffer, midiMessages, tmpBufferDouble, channelBufferDouble, false); } + void processBlockBypassed (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + jassert (! isUsingDoublePrecision()); + processAudio (buffer, midiMessages, tmpBufferFloat, channelBufferFloat, true); + } + + void processBlockBypassed (AudioBuffer& buffer, MidiBuffer& midiMessages) override + { + jassert (isUsingDoublePrecision()); + processAudio (buffer, midiMessages, tmpBufferDouble, channelBufferDouble, true); + } + + //============================================================================== bool supportsDoublePrecisionProcessing() const override { return ((vstEffect->flags & vstEffectFlagInplaceAudio) != 0 && (vstEffect->flags & vstEffectFlagInplaceDoubleAudio) != 0); } + AudioProcessorParameter* getBypassParameter() const override { return vstSupportsBypass ? bypassParam.get() : nullptr; } + //============================================================================== bool canAddBus (bool) const override { return false; } bool canRemoveBus (bool) const override { return false; } @@ -1932,9 +1950,57 @@ struct VSTPluginInstance : public AudioPluginInstance, bool usesCocoaNSView = false; private: + //============================================================================== + struct VST2BypassParameter : Parameter + { + VST2BypassParameter (VSTPluginInstance& effectToUse) : parent (effectToUse) {} + + void setValue (float newValue) override + { + currentValue = (newValue != 0.0f); + + if (parent.vstSupportsBypass) + parent.dispatch (plugInOpcodeSetBypass, 0, currentValue ? 1 : 0, nullptr, 0.0f); + } + + float getValueForText (const String& text) const override + { + String lowercaseText (text.toLowerCase()); + + for (auto& testText : onStrings) + if (lowercaseText == testText) + return 1.0f; + + for (auto& testText : offStrings) + if (lowercaseText == testText) + return 0.0f; + + return text.getIntValue() != 0 ? 1.0f : 0.0f; + } + + float getValue() const override { return currentValue; } + float getDefaultValue() const override { return 0.0f; } + String getName (int /*maximumStringLength*/) const override { return "Bypass"; } + String getText (float value, int) const override { return (value != 0.0f ? TRANS("On") : TRANS("Off")); } + bool isAutomatable() const override { return true; } + bool isDiscrete() const override { return true; } + bool isBoolean() const override { return true; } + int getNumSteps() const override { return 2; } + StringArray getAllValueStrings() const override { return values; } + String getLabel() const override { return {}; } + + VSTPluginInstance& parent; + bool currentValue = false; + StringArray onStrings { TRANS("on"), TRANS("yes"), TRANS("true") }; + StringArray offStrings { TRANS("off"), TRANS("no"), TRANS("false") }; + StringArray values { TRANS("Off"), TRANS("On") }; + }; + + //============================================================================== String name; CriticalSection lock; bool wantsMidiMessages = false, initialised = false, isPowerOn = false; + bool lastProcessBlockCallWasBypass = false, vstSupportsBypass = false; mutable StringArray programNames; AudioBuffer outOfPlaceBuffer; @@ -1948,6 +2014,7 @@ private: AudioBuffer tmpBufferDouble; HeapBlock channelBufferDouble; + ScopedPointer bypassParam; ScopedPointer xmlInfo; @@ -2202,8 +2269,20 @@ private: template void processAudio (AudioBuffer& buffer, MidiBuffer& midiMessages, AudioBuffer& tmpBuffer, - HeapBlock& channelBuffer) + HeapBlock& channelBuffer, + bool processBlockBypassedCalled) { + if (vstSupportsBypass) + { + updateBypass (processBlockBypassedCalled); + } + else if (processBlockBypassedCalled) + { + // if this vst does not support bypass then we will have to do this ourselves + AudioProcessor::processBlockBypassed (buffer, midiMessages); + return; + } + auto numSamples = buffer.getNumSamples(); auto numChannels = buffer.getNumChannels(); @@ -2588,6 +2667,23 @@ private: isPowerOn = on; } + //============================================================================== + void updateBypass (bool processBlockBypassedCalled) + { + if (processBlockBypassedCalled) + { + if (bypassParam->getValue() == 0.0f || ! lastProcessBlockCallWasBypass) + bypassParam->setValue (1.0f); + } + else + { + if (lastProcessBlockCallWasBypass) + bypassParam->setValue (0.0f); + } + + lastProcessBlockCallWasBypass = processBlockBypassedCalled; + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VSTPluginInstance) }; diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.h b/modules/juce_audio_processors/processors/juce_AudioProcessor.h index d44cd86b83..11122fc048 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessor.h +++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.h @@ -188,6 +188,13 @@ public: be the processor's MIDI output. This means that your processor should be careful to clear any incoming messages from the array if it doesn't want them to be passed-on. + If you have implemented the getBypassParameter method, then you need to check the + value of this parameter in this callback and bypass your processing if the parameter + has a non-zero value. + + Note that when calling this method as a host, the result may still be bypassed as + the parameter that controls the bypass may be non-zero. + Be very careful about what you do in this callback - it's going to be called by the audio thread, so any kind of interaction with the UI is absolutely out of the question. If you change a parameter in here and need to tell your UI to @@ -252,6 +259,13 @@ public: be the processor's MIDI output. This means that your processor should be careful to clear any incoming messages from the array if it doesn't want them to be passed-on. + If you have implemented the getBypassParameter method, then you need to check the + value of this parameter in this callback and bypass your processing if the parameter + has a non-zero value. + + Note that when calling this method as a host, the result may still be bypassed as + the parameter that controls the bypass may be non-zero. + Be very careful about what you do in this callback - it's going to be called by the audio thread, so any kind of interaction with the UI is absolutely out of the question. If you change a parameter in here and need to tell your UI to @@ -890,6 +904,21 @@ public: */ virtual void reset(); + //============================================================================== + /** Returns the parameter that controls the AudioProcessor's bypass state. + + If this method returns a nullptr then you can still control the bypass by + calling processBlockBypassed instaed of processBlock. On the other hand, + if this method returns a non-null value, you should never call + processBlockBypassed but use the returned parameter to conrol the bypass + state instead. + + A plug-in can override this function to return a parameter which control's your + plug-in's bypass. You should always check the value of this parameter in your + processBlock callback and bypass any effects if it is non-zero. + */ + virtual AudioProcessorParameter* getBypassParameter() const { return nullptr; } + //============================================================================== /** Returns true if the processor is being run in an offline mode for rendering. diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp index 336038dc17..8eedcb8ecf 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.cpp @@ -270,12 +270,20 @@ private: if (processor.isUsingDoublePrecision()) { tempBufferDouble.makeCopyOf (buffer, true); - processor.processBlock (tempBufferDouble, midiMessages); + + if (node->isBypassed()) + processor.processBlockBypassed (tempBufferDouble, midiMessages); + else + processor.processBlock (tempBufferDouble, midiMessages); + buffer.makeCopyOf (tempBufferDouble, true); } else { - processor.processBlock (buffer, midiMessages); + if (node->isBypassed()) + processor.processBlockBypassed (buffer, midiMessages); + else + processor.processBlock (buffer, midiMessages); } } @@ -283,12 +291,20 @@ private: { if (processor.isUsingDoublePrecision()) { - processor.processBlock (buffer, midiMessages); + if (node->isBypassed()) + processor.processBlockBypassed (buffer, midiMessages); + else + processor.processBlock (buffer, midiMessages); } else { tempBufferFloat.makeCopyOf (buffer, true); - processor.processBlock (tempBufferFloat, midiMessages); + + if (node->isBypassed()) + processor.processBlockBypassed (tempBufferFloat, midiMessages); + else + processor.processBlock (tempBufferFloat, midiMessages); + buffer.makeCopyOf (tempBufferFloat, true); } } @@ -826,6 +842,29 @@ bool AudioProcessorGraph::Node::Connection::operator== (const Connection& other) && otherChannel == other.otherChannel; } +//============================================================================== +bool AudioProcessorGraph::Node::isBypassed() const noexcept +{ + if (processor != nullptr) + { + if (auto* bypassParam = processor->getBypassParameter()) + return (bypassParam->getValue() != 0.0f); + } + + return bypassed; +} + +void AudioProcessorGraph::Node::setBypassed (bool shouldBeBypassed) noexcept +{ + if (processor != nullptr) + { + if (auto* bypassParam = processor->getBypassParameter()) + bypassParam->setValueNotifyingHost (shouldBeBypassed ? 1.0f : 0.0f); + } + + bypassed = shouldBeBypassed; +} + //============================================================================== struct AudioProcessorGraph::RenderSequenceFloat : public GraphRenderSequence {}; struct AudioProcessorGraph::RenderSequenceDouble : public GraphRenderSequence {}; diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h index 0fac99ced1..15983d3081 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h +++ b/modules/juce_audio_processors/processors/juce_AudioProcessorGraph.h @@ -108,6 +108,13 @@ public: */ NamedValueSet properties; + //============================================================================== + /** Returns if the node is bypassed or not. */ + bool isBypassed() const noexcept; + + /** Tell this node to bypass processing. */ + void setBypassed (bool shouldBeBypassed) noexcept; + //============================================================================== /** A convenient typedef for referring to a pointer to a node object. */ typedef ReferenceCountedObjectPtr Ptr; @@ -126,7 +133,7 @@ public: const ScopedPointer processor; Array inputs, outputs; - bool isPrepared = false; + bool isPrepared = false, bypassed = false; Node (NodeID, AudioProcessor*) noexcept; diff --git a/modules/juce_audio_processors/processors/juce_GenericAudioProcessorEditor.cpp b/modules/juce_audio_processors/processors/juce_GenericAudioProcessorEditor.cpp index f0d737713d..3a302b2e1e 100644 --- a/modules/juce_audio_processors/processors/juce_GenericAudioProcessorEditor.cpp +++ b/modules/juce_audio_processors/processors/juce_GenericAudioProcessorEditor.cpp @@ -503,8 +503,6 @@ struct GenericAudioProcessorEditor::Pimpl owner.addAndMakeVisible (view); view.setScrollBarsShown (true, false); - owner.setSize (view.getViewedComponent()->getWidth() + view.getVerticalScrollBar().getWidth(), - jmin (view.getViewedComponent()->getHeight(), 400)); } @@ -520,7 +518,10 @@ struct GenericAudioProcessorEditor::Pimpl //============================================================================== GenericAudioProcessorEditor::GenericAudioProcessorEditor (AudioProcessor* const p) : AudioProcessorEditor (p), pimpl (new Pimpl (*this)) -{} +{ + setSize (pimpl->view.getViewedComponent()->getWidth() + pimpl->view.getVerticalScrollBar().getWidth(), + jmin (pimpl->view.getViewedComponent()->getHeight(), 400)); +} GenericAudioProcessorEditor::~GenericAudioProcessorEditor() {}