/* ============================================================================== 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. ============================================================================== */ #if JucePlugin_Build_Unity #include "../../juce_core/system/juce_TargetPlatform.h" #include "../utility/juce_IncludeModuleHeaders.h" #include "../../juce_audio_processors/format_types/juce_LegacyAudioParameter.cpp" #if JUCE_WINDOWS #include "../utility/juce_IncludeSystemHeaders.h" #endif #include "juce_UnityPluginInterface.h" //============================================================================== namespace juce { typedef ComponentPeer* (*createUnityPeerFunctionType) (Component&); extern createUnityPeerFunctionType juce_createUnityPeerFn; //============================================================================== class UnityPeer : public ComponentPeer, public AsyncUpdater { public: UnityPeer (Component& ed) : ComponentPeer (ed, 0), mouseWatcher (*this) { getEditor().setResizable (false, false); } //============================================================================== Rectangle getBounds() const override { return bounds; } Point localToGlobal (Point relativePosition) override { return relativePosition + getBounds().getPosition().toFloat(); } Point globalToLocal (Point screenPosition) override { return screenPosition - getBounds().getPosition().toFloat(); } StringArray getAvailableRenderingEngines() override { return StringArray ("Software Renderer"); } void setBounds (const Rectangle& newBounds, bool) override { bounds = newBounds; mouseWatcher.setBoundsToWatch (bounds); } bool contains (Point localPos, bool) const override { if (isPositiveAndBelow (localPos.getX(), getBounds().getWidth()) && isPositiveAndBelow (localPos.getY(), getBounds().getHeight())) return true; return false; } void handleAsyncUpdate() override { fillPixels(); } //============================================================================== AudioProcessorEditor& getEditor() { return *dynamic_cast (&getComponent()); } void setPixelDataHandle (uint8* handle, int width, int height) { pixelData = handle; textureWidth = width; textureHeight = height; renderImage = Image (new UnityBitmapImage (pixelData, width, height)); } // N.B. This is NOT an efficient way to do this and you shouldn't use this method in your own code. // It works for our purposes here but a much more efficient way would be to use a GL texture. void fillPixels() { if (pixelData == nullptr) return; LowLevelGraphicsSoftwareRenderer renderer (renderImage); renderer.addTransform (AffineTransform::verticalFlip ((float) getComponent().getHeight())); handlePaint (renderer); for (int i = 0; i < textureWidth * textureHeight * 4; i += 4) { auto r = pixelData[i + 2]; auto g = pixelData[i + 1]; auto b = pixelData[i + 0]; pixelData[i + 0] = r; pixelData[i + 1] = g; pixelData[i + 2] = b; } } void forwardMouseEvent (Point position, ModifierKeys mods) { ModifierKeys::currentModifiers = mods; handleMouseEvent (juce::MouseInputSource::mouse, position, mods, juce::MouseInputSource::invalidPressure, juce::MouseInputSource::invalidOrientation, juce::Time::currentTimeMillis()); } void forwardKeyPress (int code, String name, ModifierKeys mods) { ModifierKeys::currentModifiers = mods; handleKeyPress (getKeyPress (code, name)); } private: //============================================================================== struct UnityBitmapImage : public ImagePixelData { UnityBitmapImage (uint8* data, int w, int h) : ImagePixelData (Image::PixelFormat::ARGB, w, h), imageData (data), lineStride (width * pixelStride) { } ImageType* createType() const override { return new SoftwareImageType(); } LowLevelGraphicsContext* createLowLevelContext() override { return new LowLevelGraphicsSoftwareRenderer (Image (this)); } void initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) override { ignoreUnused (mode); bitmap.data = imageData + x * pixelStride + y * lineStride; bitmap.pixelFormat = pixelFormat; bitmap.lineStride = lineStride; bitmap.pixelStride = pixelStride; } ImagePixelData::Ptr clone() override { auto im = new UnityBitmapImage (imageData, width, height); for (int i = 0; i < height; ++i) memcpy (im->imageData + i * lineStride, imageData + i * lineStride, (size_t) lineStride); return im; } uint8* imageData; int pixelStride = 4, lineStride; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UnityBitmapImage) }; //============================================================================== struct MouseWatcher : public Timer { MouseWatcher (ComponentPeer& o) : owner (o) {} void timerCallback() override { auto pos = Desktop::getMousePosition(); if (boundsToWatch.contains (pos) && pos != lastMousePos) { auto ms = Desktop::getInstance().getMainMouseSource(); if (! ms.getCurrentModifiers().isLeftButtonDown()) owner.handleMouseEvent (juce::MouseInputSource::mouse, owner.globalToLocal (pos.toFloat()), {}, juce::MouseInputSource::invalidPressure, juce::MouseInputSource::invalidOrientation, juce::Time::currentTimeMillis()); lastMousePos = pos; } } void setBoundsToWatch (Rectangle b) { if (boundsToWatch != b) boundsToWatch = b; startTimer (250); } ComponentPeer& owner; Rectangle boundsToWatch; Point lastMousePos; }; //============================================================================== KeyPress getKeyPress (int keyCode, String name) { if (keyCode >= 32 && keyCode <= 64) return { keyCode, ModifierKeys::currentModifiers, juce::juce_wchar (keyCode) }; if (keyCode >= 91 && keyCode <= 122) return { keyCode, ModifierKeys::currentModifiers, name[0] }; if (keyCode >= 256 && keyCode <= 265) return { juce::KeyPress::numberPad0 + (keyCode - 256), ModifierKeys::currentModifiers, juce::String (keyCode - 256).getCharPointer()[0] }; if (keyCode == 8) return { juce::KeyPress::backspaceKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 127) return { juce::KeyPress::deleteKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 9) return { juce::KeyPress::tabKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 13) return { juce::KeyPress::returnKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 27) return { juce::KeyPress::escapeKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 32) return { juce::KeyPress::spaceKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 266) return { juce::KeyPress::numberPadDecimalPoint, ModifierKeys::currentModifiers, {} }; if (keyCode == 267) return { juce::KeyPress::numberPadDivide, ModifierKeys::currentModifiers, {} }; if (keyCode == 268) return { juce::KeyPress::numberPadMultiply, ModifierKeys::currentModifiers, {} }; if (keyCode == 269) return { juce::KeyPress::numberPadSubtract, ModifierKeys::currentModifiers, {} }; if (keyCode == 270) return { juce::KeyPress::numberPadAdd, ModifierKeys::currentModifiers, {} }; if (keyCode == 272) return { juce::KeyPress::numberPadEquals, ModifierKeys::currentModifiers, {} }; if (keyCode == 273) return { juce::KeyPress::upKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 274) return { juce::KeyPress::downKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 275) return { juce::KeyPress::rightKey, ModifierKeys::currentModifiers, {} }; if (keyCode == 276) return { juce::KeyPress::leftKey, ModifierKeys::currentModifiers, {} }; return {}; } //============================================================================== Rectangle bounds; MouseWatcher mouseWatcher; uint8* pixelData = nullptr; int textureWidth, textureHeight; Image renderImage; //============================================================================== void setMinimised (bool) override {} bool isMinimised() const override { return false; } void setFullScreen (bool) override {} bool isFullScreen() const override { return false; } bool setAlwaysOnTop (bool) override { return false; } void toFront (bool) override {} void toBehind (ComponentPeer*) override {} bool isFocused() const override { return true; } void grabFocus() override {} void* getNativeHandle() const override { return nullptr; } BorderSize getFrameSize() const override { return {}; } void setVisible (bool) override {} void setTitle (const String&) override {} void setIcon (const Image&) override {} void textInputRequired (Point, TextInputTarget&) override {} void setAlpha (float) override {} void performAnyPendingRepaintsNow() override {} void repaint (const Rectangle&) override {} //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UnityPeer) }; ComponentPeer* createUnityPeer (Component& c) { return new UnityPeer (c); } //============================================================================== class AudioProcessorUnityWrapper { public: AudioProcessorUnityWrapper (bool isTemporary) { pluginInstance.reset (createPluginFilterOfType (AudioProcessor::wrapperType_Unity)); if (! isTemporary && pluginInstance->hasEditor()) { pluginInstanceEditor.reset (pluginInstance->createEditorIfNeeded()); pluginInstanceEditor->setVisible (true); pluginInstanceEditor->addToDesktop (0); } juceParameters.update (*pluginInstance, false); } ~AudioProcessorUnityWrapper() { if (pluginInstanceEditor != nullptr) { pluginInstanceEditor->removeFromDesktop(); PopupMenu::dismissAllActiveMenus(); pluginInstanceEditor->processor.editorBeingDeleted (pluginInstanceEditor.get()); pluginInstanceEditor = nullptr; } } void create (UnityAudioEffectState* state) { // only supported in Unity plugin API > 1.0 if (state->structSize >= sizeof (UnityAudioEffectState)) samplesPerBlock = static_cast (state->dspBufferSize); #ifdef JucePlugin_PreferredChannelConfigurations short configs[][2] = { JucePlugin_PreferredChannelConfigurations }; const int numConfigs = sizeof (configs) / sizeof (short[2]); jassert (numConfigs > 0 && (configs[0][0] > 0 || configs[0][1] > 0)); pluginInstance->setPlayConfigDetails (configs[0][0], configs[0][1], state->sampleRate, samplesPerBlock); #else pluginInstance->setRateAndBufferSizeDetails (state->sampleRate, samplesPerBlock); #endif pluginInstance->prepareToPlay (state->sampleRate, samplesPerBlock); scratchBuffer.setSize (jmax (pluginInstance->getTotalNumInputChannels(), pluginInstance->getTotalNumOutputChannels()), samplesPerBlock); } void release() { pluginInstance->releaseResources(); } void reset() { pluginInstance->reset(); } void process (float* inBuffer, float* outBuffer, int bufferSize, int numInChannels, int numOutChannels, bool isBypassed) { for (int pos = 0; pos < bufferSize;) { auto max = jmin (bufferSize - pos, samplesPerBlock); processBuffers (inBuffer + (pos * numInChannels), outBuffer + (pos * numOutChannels), max, numInChannels, numOutChannels, isBypassed); pos += max; } } void declareParameters (UnityAudioEffectDefinition& definition) { static std::unique_ptr parametersPtr; static int numParams = 0; if (parametersPtr == nullptr) { numParams = juceParameters.params.size(); parametersPtr.reset (static_cast (std::calloc (static_cast (numParams), sizeof (UnityAudioParameterDefinition)))); parameterDescriptions.clear(); for (int i = 0; i < numParams; ++i) { auto* parameter = juceParameters.params[i]; auto& paramDef = parametersPtr.get()[i]; strncpy (paramDef.name, parameter->getName (15).toRawUTF8(), 15); if (parameter->getLabel().isNotEmpty()) strncpy (paramDef.unit, parameter->getLabel().toRawUTF8(), 15); parameterDescriptions.add (parameter->getName (15)); paramDef.description = parameterDescriptions[i].toRawUTF8(); paramDef.defaultVal = parameter->getDefaultValue(); paramDef.min = 0.0f; paramDef.max = 1.0f; paramDef.displayScale = 1.0f; paramDef.displayExponent = 1.0f; } } definition.numParameters = static_cast (numParams); definition.parameterDefintions = parametersPtr.get(); } void setParameter (int index, float value) { juceParameters.getParamForIndex (index)->setValueNotifyingHost (value); } float getParameter (int index) const noexcept { return juceParameters.getParamForIndex (index)->getValue(); } String getParameterString (int index) const noexcept { auto* param = juceParameters.getParamForIndex (index); return param->getText (param->getValue(), 16); } int getNumInputChannels() const noexcept { return pluginInstance->getTotalNumInputChannels(); } int getNumOutputChannels() const noexcept { return pluginInstance->getTotalNumOutputChannels(); } bool hasEditor() const noexcept { return pluginInstance->hasEditor(); } UnityPeer& getEditorPeer() const { auto* peer = dynamic_cast (pluginInstanceEditor->getPeer()); jassert (peer != nullptr); return *peer; } private: //============================================================================== void processBuffers (float* inBuffer, float* outBuffer, int bufferSize, int numInChannels, int numOutChannels, bool isBypassed) { int ch; for (ch = 0; ch < numInChannels; ++ch) { using DstSampleType = AudioData::Pointer; using SrcSampleType = AudioData::Pointer; DstSampleType dstData (scratchBuffer.getWritePointer (ch)); SrcSampleType srcData (inBuffer + ch, numInChannels); dstData.convertSamples (srcData, bufferSize); } for (; ch < numOutChannels; ++ch) scratchBuffer.clear (ch, 0, bufferSize); { const ScopedLock sl (pluginInstance->getCallbackLock()); if (pluginInstance->isSuspended()) { scratchBuffer.clear(); } else { MidiBuffer mb; if (isBypassed) pluginInstance->processBlockBypassed (scratchBuffer, mb); else pluginInstance->processBlock (scratchBuffer, mb); } } for (ch = 0; ch < numOutChannels; ++ch) { using DstSampleType = AudioData::Pointer; using SrcSampleType = AudioData::Pointer; DstSampleType dstData (outBuffer + ch, numOutChannels); SrcSampleType srcData (scratchBuffer.getReadPointer (ch)); dstData.convertSamples (srcData, bufferSize); } } //============================================================================== std::unique_ptr pluginInstance; std::unique_ptr pluginInstanceEditor; int samplesPerBlock = 1024; StringArray parameterDescriptions; AudioBuffer scratchBuffer; LegacyAudioParametersWrapper juceParameters; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorUnityWrapper) }; //============================================================================== HashMap& getWrapperMap() { static HashMap wrapperMap; return wrapperMap; } static void onWrapperCreation (AudioProcessorUnityWrapper* wrapperToAdd) { getWrapperMap().set (std::abs (Random::getSystemRandom().nextInt (65536)), wrapperToAdd); } static void onWrapperDeletion (AudioProcessorUnityWrapper* wrapperToRemove) { getWrapperMap().removeValue (wrapperToRemove); } //============================================================================== namespace UnityCallbacks { int UNITY_INTERFACE_API createCallback (UnityAudioEffectState* state) { auto* pluginInstance = new AudioProcessorUnityWrapper (false); pluginInstance->create (state); state->effectData = pluginInstance; onWrapperCreation (pluginInstance); return 0; } int UNITY_INTERFACE_API releaseCallback (UnityAudioEffectState* state) { auto* pluginInstance = state->getEffectData(); pluginInstance->release(); onWrapperDeletion (pluginInstance); delete pluginInstance; if (getWrapperMap().size() == 0) shutdownJuce_GUI(); return 0; } int UNITY_INTERFACE_API resetCallback (UnityAudioEffectState* state) { auto* pluginInstance = state->getEffectData(); pluginInstance->reset(); return 0; } int UNITY_INTERFACE_API setPositionCallback (UnityAudioEffectState* state, unsigned int pos) { ignoreUnused (state, pos); return 0; } int UNITY_INTERFACE_API setFloatParameterCallback (UnityAudioEffectState* state, int index, float value) { auto* pluginInstance = state->getEffectData(); pluginInstance->setParameter (index, value); return 0; } int UNITY_INTERFACE_API getFloatParameterCallback (UnityAudioEffectState* state, int index, float* value, char* valueStr) { auto* pluginInstance = state->getEffectData(); *value = pluginInstance->getParameter (index); strncpy (valueStr, pluginInstance->getParameterString (index).toRawUTF8(), 15); return 0; } int UNITY_INTERFACE_API getFloatBufferCallback (UnityAudioEffectState* state, const char* name, float* buffer, int numSamples) { ignoreUnused (numSamples); auto nameStr = String (name); if (nameStr == "Editor") { auto* pluginInstance = state->getEffectData(); buffer[0] = pluginInstance->hasEditor() ? 1.0f : 0.0f; } else if (nameStr == "ID") { auto* pluginInstance = state->getEffectData(); for (HashMap::Iterator i (getWrapperMap()); i.next();) { if (i.getValue() == pluginInstance) { buffer[0] = (float) i.getKey(); break; } } return 0; } else if (nameStr == "Size") { auto* pluginInstance = state->getEffectData(); auto& editor = pluginInstance->getEditorPeer().getEditor(); buffer[0] = (float) editor.getBounds().getWidth(); buffer[1] = (float) editor.getBounds().getHeight(); buffer[2] = (float) editor.getConstrainer()->getMinimumWidth(); buffer[3] = (float) editor.getConstrainer()->getMinimumHeight(); buffer[4] = (float) editor.getConstrainer()->getMaximumWidth(); buffer[5] = (float) editor.getConstrainer()->getMaximumHeight(); } return 0; } int UNITY_INTERFACE_API processCallback (UnityAudioEffectState* state, float* inBuffer, float* outBuffer, unsigned int bufferSize, int numInChannels, int numOutChannels) { auto* pluginInstance = state->getEffectData(); if (pluginInstance != nullptr) { auto isPlaying = ((state->flags & stateIsPlaying) != 0); auto isMuted = ((state->flags & stateIsMuted) != 0); auto isPaused = ((state->flags & stateIsPaused) != 0); auto bypassed = ! isPlaying || (isMuted || isPaused); pluginInstance->process (inBuffer, outBuffer, static_cast (bufferSize), numInChannels, numOutChannels, bypassed); } else { FloatVectorOperations::clear (outBuffer, static_cast (bufferSize) * numOutChannels); } return 0; } } //============================================================================== static void declareEffect (UnityAudioEffectDefinition& definition) { memset (&definition, 0, sizeof (definition)); std::unique_ptr wrapper = std::make_unique (true); String name (JucePlugin_Name); if (! name.startsWithIgnoreCase ("audioplugin")) name = "audioplugin_" + name; strcpy (definition.name, name.toRawUTF8()); definition.structSize = sizeof (UnityAudioEffectDefinition); definition.parameterStructSize = sizeof (UnityAudioParameterDefinition); definition.apiVersion = UNITY_AUDIO_PLUGIN_API_VERSION; definition.pluginVersion = JucePlugin_VersionCode; // effects must set this to 0, generators > 0 definition.channels = (wrapper->getNumInputChannels() != 0 ? 0 : static_cast (wrapper->getNumOutputChannels())); wrapper->declareParameters (definition); definition.create = UnityCallbacks::createCallback; definition.release = UnityCallbacks::releaseCallback; definition.reset = UnityCallbacks::resetCallback; definition.setPosition = UnityCallbacks::setPositionCallback; definition.process = UnityCallbacks::processCallback; definition.setFloatParameter = UnityCallbacks::setFloatParameterCallback; definition.getFloatParameter = UnityCallbacks::getFloatParameterCallback; definition.getFloatBuffer = UnityCallbacks::getFloatBufferCallback; } } // namespace juce UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API UnityGetAudioEffectDefinitions (UnityAudioEffectDefinition*** definitionsPtr) { if (juce::getWrapperMap().size() == 0) juce::initialiseJuce_GUI(); static bool hasInitialised = false; if (! hasInitialised) { juce::PluginHostType::jucePlugInClientCurrentWrapperType = juce::AudioProcessor::wrapperType_Unity; juce::juce_createUnityPeerFn = juce::createUnityPeer; hasInitialised = true; } auto* definition = new UnityAudioEffectDefinition(); juce::declareEffect (*definition); *definitionsPtr = &definition; return 1; } //============================================================================== static juce::ModifierKeys unityModifiersToJUCE (UnityEventModifiers mods, bool mouseDown, int mouseButton = -1) { int flags = 0; if (mouseDown) { if (mouseButton == 0) flags |= juce::ModifierKeys::leftButtonModifier; else if (mouseButton == 1) flags |= juce::ModifierKeys::rightButtonModifier; else if (mouseButton == 2) flags |= juce::ModifierKeys::middleButtonModifier; } if (mods == 0) return flags; if ((mods & UnityEventModifiers::shift) != 0) flags |= juce::ModifierKeys::shiftModifier; if ((mods & UnityEventModifiers::control) != 0) flags |= juce::ModifierKeys::ctrlModifier; if ((mods & UnityEventModifiers::alt) != 0) flags |= juce::ModifierKeys::altModifier; if ((mods & UnityEventModifiers::command) != 0) flags |= juce::ModifierKeys::commandModifier; return { flags }; } //============================================================================== static juce::AudioProcessorUnityWrapper* getWrapperChecked (int id) { auto* wrapper = juce::getWrapperMap()[id]; jassert (wrapper != nullptr); return wrapper; } //============================================================================== static void UNITY_INTERFACE_API onRenderEvent (int id) { getWrapperChecked (id)->getEditorPeer().triggerAsyncUpdate(); } UNITY_INTERFACE_EXPORT renderCallback UNITY_INTERFACE_API getRenderCallback() { return onRenderEvent; } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unityInitialiseTexture (int id, void* data, int w, int h) { getWrapperChecked (id)->getEditorPeer().setPixelDataHandle (reinterpret_cast (data), w, h); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unityMouseDown (int id, float x, float y, UnityEventModifiers unityMods, int button) { getWrapperChecked (id)->getEditorPeer().forwardMouseEvent ({ x, y }, unityModifiersToJUCE (unityMods, true, button)); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unityMouseDrag (int id, float x, float y, UnityEventModifiers unityMods, int button) { getWrapperChecked (id)->getEditorPeer().forwardMouseEvent ({ x, y }, unityModifiersToJUCE (unityMods, true, button)); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unityMouseUp (int id, float x, float y, UnityEventModifiers unityMods) { getWrapperChecked (id)->getEditorPeer().forwardMouseEvent ({ x, y }, unityModifiersToJUCE (unityMods, false)); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unityKeyEvent (int id, int code, UnityEventModifiers mods, const char* name) { getWrapperChecked (id)->getEditorPeer().forwardKeyPress (code, name, unityModifiersToJUCE (mods, false)); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API unitySetScreenBounds (int id, float x, float y, float w, float h) { getWrapperChecked (id)->getEditorPeer().getEditor().setBounds ({ (int) x, (int) y, (int) w, (int) h }); } //============================================================================== #if JUCE_WINDOWS extern "C" BOOL WINAPI DllMain (HINSTANCE instance, DWORD reason, LPVOID) { if (reason == DLL_PROCESS_ATTACH) juce::Process::setCurrentModuleInstanceHandle (instance); return true; } #endif #endif