/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. 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) { } std::unique_ptr createType() const override { return std::make_unique(); } std::unique_ptr createLowLevelContext() override { return std::make_unique (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)); ignoreUnused (numConfigs); 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