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 06489c4fea..d7d76aa925 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -73,7 +73,7 @@ class JuceAudioProcessor : public FUnknown { public: JuceAudioProcessor (AudioProcessor* source) noexcept - : refCount (0), audioProcessor (source) {} + : isBypassed (false), refCount (0), audioProcessor (source) {} virtual ~JuceAudioProcessor() {} @@ -84,6 +84,8 @@ public: static const FUID iid; + bool isBypassed; + private: Atomic refCount; ScopedPointer audioProcessor; @@ -230,14 +232,104 @@ public: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Param) }; + //============================================================================== + struct BypassParam : public Vst::Parameter + { + BypassParam (AudioProcessor& p, int index) : owner (p), paramIndex (index) + { + info.id = (Vst::ParamID) index; + toString128 (info.title, "Bypass"); + toString128 (info.shortTitle, "Bypass"); + toString128 (info.units, ""); + info.stepCount = 2; + info.defaultNormalizedValue = 0.0f; + info.unitId = Vst::kRootUnitId; + info.flags = Vst::ParameterInfo::kIsBypass; + } + + 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 + { + const String 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; } + + private: + AudioProcessor& owner; + int paramIndex; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BypassParam) + }; + //============================================================================== tresult PLUGIN_API setComponentState (IBStream* stream) override { // Cubase and Nuendo need to inform the host of the current parameter values if (AudioProcessor* const pluginInstance = getPluginInstance()) { - for (int i = 0; i < pluginInstance->getNumParameters(); ++i) + const int numParameters = pluginInstance->getNumParameters(); + + for (int i = 0; i < numParameters; ++i) setParamNormalized ((Vst::ParamID) i, (double) pluginInstance->getParameter (i)); + + setParamNormalized ((Vst::ParamID) numParameters, audioProcessor->isBypassed ? 1.0f : 0.0f); } return Vst::EditController::setComponentState (stream); @@ -274,7 +366,7 @@ public: tresult PLUGIN_API getMidiControllerAssignment (Steinberg::int32 /*busIndex*/, Steinberg::int16 channel, Vst::CtrlNumber midiControllerNumber, Vst::ParamID& resultID) override { - resultID = midiControllerToParameter[channel][midiControllerNumber]; + resultID = (Vst::ParamID) midiControllerToParameter[channel][midiControllerNumber]; return kResultTrue; // Returning false makes some hosts stop asking for further MIDI Controller Assignments } @@ -366,17 +458,25 @@ private: pluginInstance->addListener (this); if (parameters.getParameterCount() <= 0) - for (int i = 0; i < pluginInstance->getNumParameters(); ++i) + { + const int numParameters = pluginInstance->getNumParameters(); + + for (int i = 0; i < numParameters; ++i) parameters.addParameter (new Param (*pluginInstance, i)); - initialiseMidiControllerMappings (pluginInstance->getNumParameters()); + parameters.addParameter (new BypassParam (*pluginInstance, numParameters)); + } + + // We need to account for the bypass parameter in the numParameters passed to + // the next function + initialiseMidiControllerMappings (pluginInstance->getNumParameters() + 1); audioProcessorChanged (pluginInstance); } } - void initialiseMidiControllerMappings (const int numParameters) + void initialiseMidiControllerMappings (const int numVstParameters) { - parameterToMidiControllerOffset = numParameters; + parameterToMidiControllerOffset = numVstParameters; for (int c = 0, p = 0; c < numMIDIChannels; ++c) { @@ -387,7 +487,7 @@ private: parameterToMidiController[p].ctrlNumber = i; parameters.addParameter (new Vst::Parameter (toString ("MIDI CC " + String (c) + "|" + String (i)), - p + parameterToMidiControllerOffset, 0, 0, 0, + (Vst::ParamID) (p + parameterToMidiControllerOffset), 0, 0, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); } } @@ -655,6 +755,8 @@ public: processSetup.sampleRate = 44100.0; processSetup.symbolicSampleSize = Vst::kSample32; + vstBypassParameterId = pluginInstance->getNumParameters(); + pluginInstance->setPlayHead (this); } @@ -852,12 +954,92 @@ 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; } + + //============================================================================== + void writeJucePrivateStateInformation (MemoryOutputStream& out) + { + ValueTree privateData (kJucePrivateDataIdentifier); + + // for now we only store the bypass value + privateData.setProperty ("Bypass", var (isBypassed()), nullptr); + + privateData.writeToStream (out); + } + + void setJucePrivateStateInformation (const void* data, int sizeInBytes) + { + ValueTree privateData = ValueTree::readFromData (data, static_cast (sizeInBytes)); + setBypassed (static_cast (privateData.getProperty ("Bypass", var (false)))); + } + + void getStateInformation (MemoryBlock& destData) + { + pluginInstance->getStateInformation (destData); + + // With bypass support, JUCE now needs to store private state data. + // Put this at the end of the plug-in state and add a few null characters + // so that plug-ins built with older versions of JUCE will hopefully ignore + // this data. Additionally, we need to add some sort of magic identifier + // at the very end of the private data so that JUCE has some sort of + // way to figure out if the data was stored with a newer JUCE version. + MemoryOutputStream extraData; + + extraData.writeInt64 (0); + writeJucePrivateStateInformation (extraData); + const int64 privateDataSize = (int64) (extraData.getDataSize() - sizeof (int64)); + extraData.writeInt64 (privateDataSize); + extraData << kJucePrivateDataIdentifier; + + // write magic string + destData.append (extraData.getData(), extraData.getDataSize()); + } + + void setStateInformation (const void* data, int sizeAsInt) + { + size_t size = static_cast (sizeAsInt); + + // Check if this data was written with a newer JUCE version + // and if it has the JUCE private data magic code at the end + const size_t jucePrivDataIdentifierSize = std::strlen (kJucePrivateDataIdentifier); + + if (size >= (jucePrivDataIdentifierSize + sizeof (int64))) + { + const char* buffer = static_cast (data); + + String magic (CharPointer_UTF8 (buffer + size - jucePrivDataIdentifierSize), + CharPointer_UTF8 (buffer + size)); + + if (magic == kJucePrivateDataIdentifier) + { + // found a JUCE private data section + uint64 privateDataSize; + + std::memcpy (&privateDataSize, + buffer + (size - jucePrivDataIdentifierSize - sizeof (uint64)), + sizeof (uint64)); + + privateDataSize = ByteOrder::swapIfBigEndian (privateDataSize); + size -= privateDataSize + jucePrivDataIdentifierSize + sizeof (uint64); + + if (privateDataSize > 0) + setJucePrivateStateInformation (buffer + size, static_cast (privateDataSize)); + + size -= sizeof (uint64); + } + } + + pluginInstance->setStateInformation (data, static_cast (size)); + } + + //============================================================================== #if JUCE_VST3_CAN_REPLACE_VST2 void loadVST2VstWBlock (const char* data, int size) { - const int headerLen = htonl (*(juce::int32*) (data + 4)); + const int headerLen = static_cast (htonl (*(juce::int32*) (data + 4))); const struct fxBank* bank = (const struct fxBank*) (data + (8 + headerLen)); - const int version = htonl (bank->version); (void) version; + const int version = static_cast (htonl (bank->version)); (void) version; jassert ('VstW' == htonl (*(juce::int32*) data)); jassert (1 == htonl (*(juce::int32*) (data + 8))); // version should be 1 according to Steinberg's docs @@ -866,9 +1048,9 @@ public: jassert (version == 1 || version == 2); jassert (JucePlugin_VSTUniqueID == htonl (bank->fxID)); - pluginInstance->setStateInformation (bank->content.data.chunk, - jmin ((int) (size - (bank->content.data.chunk - data)), - (int) htonl (bank->content.data.size))); + setStateInformation (bank->content.data.chunk, + jmin ((int) (size - (bank->content.data.chunk - data)), + (int) htonl (bank->content.data.size))); } bool loadVST3PresetFile (const char* data, int size) @@ -896,7 +1078,7 @@ public: juce::uint64 chunkOffset = ByteOrder::littleEndianInt64 (data + entryOffset + 4); juce::uint64 chunkSize = ByteOrder::littleEndianInt64 (data + entryOffset + 12); - if (chunkOffset + chunkSize > size) + if (chunkOffset + chunkSize > static_cast (size)) { jassertfalse; return false; @@ -938,7 +1120,7 @@ public: #if JUCE_VST3_CAN_REPLACE_VST2 return loadVST2CompatibleState ((const char*) data, size); #else - pluginInstance->setStateInformation (data, size); + setStateInformation (data, size); return true; #endif } @@ -979,7 +1161,7 @@ public: if (bytesRead <= 0 || (status != kResultTrue && ! getHostType().isWavelab())) break; - allData.write (buffer, bytesRead); + allData.write (buffer, static_cast (bytesRead)); } } @@ -1010,13 +1192,13 @@ public: return state->write (&t, 4); } - static tresult writeVST2Header (IBStream* state) + static tresult writeVST2Header (IBStream* state, bool bypassed) { tresult status = writeVST2Int (state, 'VstW'); if (status == kResultOk) status = writeVST2Int (state, 8); // header size if (status == kResultOk) status = writeVST2Int (state, 1); // version - if (status == kResultOk) status = writeVST2Int (state, 0); // bypass + if (status == kResultOk) status = writeVST2Int (state, bypassed ? 1 : 0); // bypass return status; } @@ -1028,10 +1210,10 @@ public: return kInvalidArgument; juce::MemoryBlock mem; - pluginInstance->getStateInformation (mem); + getStateInformation (mem); #if JUCE_VST3_CAN_REPLACE_VST2 - tresult status = writeVST2Header (state); + tresult status = writeVST2Header (state, isBypassed()); if (status != kResultOk) return status; @@ -1040,13 +1222,13 @@ public: struct fxBank bank; zerostruct (bank); - bank.chunkMagic = htonl (cMagic); - bank.byteSize = htonl (bankBlockSize - 8 + (unsigned int) mem.getSize()); - bank.fxMagic = htonl (chunkBankMagic); - bank.version = htonl (2); - bank.fxID = htonl (JucePlugin_VSTUniqueID); - bank.fxVersion = htonl (JucePlugin_VersionCode); - bank.content.data.size = htonl ((unsigned int) mem.getSize()); + bank.chunkMagic = (VstInt32) htonl (cMagic); + bank.byteSize = (VstInt32) htonl (bankBlockSize - 8 + (unsigned int) mem.getSize()); + bank.fxMagic = (VstInt32) htonl (chunkBankMagic); + bank.version = (VstInt32) htonl (2); + bank.fxID = (VstInt32) htonl (JucePlugin_VSTUniqueID); + bank.fxVersion = (VstInt32) htonl (JucePlugin_VersionCode); + bank.content.data.size = (VstInt32) htonl ((unsigned int) mem.getSize()); status = state->write (&bank, bankBlockSize); @@ -1304,6 +1486,8 @@ public: if (isPositiveAndBelow (id, pluginInstance->getNumParameters())) pluginInstance->setParameter (id, (float) value); + else if (id == vstBypassParameterId) + setBypassed (static_cast (value) != 0.0f); else addParameterChangeToMidiBuffer (offsetSamples, id, value); } @@ -1386,9 +1570,16 @@ public: processParameterChanges (*data.inputParameterChanges); if (pluginInstance->isSuspended()) + { buffer.clear(); + } else - pluginInstance->processBlock (buffer, midiBuffer); + { + if (isBypassed()) + pluginInstance->processBlockBypassed (buffer, midiBuffer); + else + pluginInstance->processBlock (buffer, midiBuffer); + } } for (int i = 0; i < numOutputChans; ++i) @@ -1446,6 +1637,10 @@ private: ScopedJuceInitialiser_GUI libraryInitialiser; + int vstBypassParameterId; + + static const char* kJucePrivateDataIdentifier; + //============================================================================== void addBusTo (Vst::BusList& busList, Vst::Bus* newBus) { @@ -1505,6 +1700,8 @@ private: Steinberg::FUID getJuceVST3ComponentIID() { return JuceVST3Component::iid; } #endif +const char* JuceVST3Component::kJucePrivateDataIdentifier = "JUCEPrivateData"; + //============================================================================== #if JUCE_MSVC #pragma warning (push, 0) @@ -1537,7 +1734,7 @@ DEF_CLASS_IID (JuceAudioProcessor) for (size_t i = 0; i <= 8; ++i) { - juce::uint8 c = i < len ? JucePlugin_Name[i] : 0; + juce::uint8 c = i < len ? static_cast (JucePlugin_Name[i]) : 0; if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; diff --git a/modules/juce_audio_processors/format_types/juce_VST3Common.h b/modules/juce_audio_processors/format_types/juce_VST3Common.h index 0017e1cae8..8a5c6f2cb2 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -67,6 +67,11 @@ static juce::String toString (const Steinberg::char16* string) noexcept { re static juce::String toString (const Steinberg::UString128& string) noexcept { return toString (static_cast (string)); } static juce::String toString (const Steinberg::UString256& string) noexcept { return toString (static_cast (string)); } +static void toString128 (Steinberg::Vst::String128 result, const char* source) +{ + Steinberg::UString (result, 128).fromAscii (source); +} + static void toString128 (Steinberg::Vst::String128 result, const juce::String& source) { Steinberg::UString (result, 128).fromAscii (source.toUTF8());