diff --git a/examples/Plugins/ReaperEmbeddedViewPluginDemo.h b/examples/Plugins/ReaperEmbeddedViewPluginDemo.h new file mode 100644 index 0000000000..5800624978 --- /dev/null +++ b/examples/Plugins/ReaperEmbeddedViewPluginDemo.h @@ -0,0 +1,407 @@ +/* + ============================================================================== + + This file is part of the JUCE examples. + Copyright (c) 2020 - Raw Material Software Limited + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, + WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR + PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +/******************************************************************************* + The block below describes the properties of this PIP. A PIP is a short snippet + of code that can be read by the Projucer and used to generate a JUCE project. + + BEGIN_JUCE_PIP_METADATA + + name: ReaperEmbeddedViewDemo + version: 1.0.0 + vendor: JUCE + website: http://juce.com + description: An audio plugin which embeds a secondary view in VST2 and + VST3 formats in REAPER + + dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, + juce_audio_plugin_client, juce_audio_processors, + juce_audio_utils, juce_core, juce_data_structures, + juce_events, juce_graphics, juce_gui_basics, juce_gui_extra + exporters: xcode_mac, vs2019, linux_make + + moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 + + type: AudioProcessor + mainClass: ReaperEmbeddedViewDemo + + useLocalCopy: 1 + + END_JUCE_PIP_METADATA + +*******************************************************************************/ + +/* This demo shows how to use the VSTCallbackHandler and VST3ClientExtensions + classes to provide extended functionality in compatible VST/VST3 hosts. + + If this project is built as a VST or VST3 plugin and loaded in REAPER + 6.29 or higher, it will provide an embedded level meter in the track + control panel. To enable the embedded view, right-click on the plugin + and select "Show embedded UI in TCP". + + The plugin's editor also include a button which can be used to toggle + all inserts on and off. +*/ + +#pragma once + +#include +#include +#include + +namespace reaper +{ + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-as-null-pointer-constant", + "-Wunused-parameter") + + using namespace Steinberg; + using INT_PTR = pointer_sized_int; + using uint32 = Steinberg::uint32; + + #include "extern/reaper_plugin_fx_embed.h" + #include "extern/reaper_vst3_interfaces.h" + + //============================================================================== + /* These should live in a file which is guaranteed to be compiled only once + (i.e. a .cpp file, normally). This demo is a bit special, because we know + that this header will only be included in a single translation unit. + */ + DEF_CLASS_IID (IReaperHostApplication) + DEF_CLASS_IID (IReaperUIEmbedInterface) + + JUCE_END_IGNORE_WARNINGS_GCC_LIKE +} + +//============================================================================== +struct EmbeddedViewListener +{ + virtual ~EmbeddedViewListener() = default; + virtual Steinberg::TPtrInt handledEmbeddedUIMessage (int msg, + Steinberg::TPtrInt parm2, + Steinberg::TPtrInt parm3) = 0; +}; + +//============================================================================== +class EmbeddedUI : public reaper::IReaperUIEmbedInterface +{ +public: + explicit EmbeddedUI (EmbeddedViewListener& demo) : listener (demo) {} + + Steinberg::TPtrInt embed_message (int msg, + Steinberg::TPtrInt parm2, + Steinberg::TPtrInt parm3) override + { + return listener.handledEmbeddedUIMessage (msg, parm2, parm3); + } + + Steinberg::uint32 PLUGIN_API addRef() override { return (Steinberg::uint32) ++refCount; } + Steinberg::uint32 PLUGIN_API release() override { return (Steinberg::uint32) --refCount; } + + Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID tuid, void** obj) override + { + if (std::memcmp (tuid, iid, sizeof (Steinberg::TUID)) == 0) + { + *obj = this; + return Steinberg::kResultOk; + } + + *obj = nullptr; + return Steinberg::kNoInterface; + } + +private: + EmbeddedViewListener& listener; + std::atomic refCount { 1 }; +}; + +//============================================================================== +class Editor : public AudioProcessorEditor +{ +public: + explicit Editor (AudioProcessor& proc, + AudioParameterFloat& param, + void (*globalBypass) (int)) + : AudioProcessorEditor (proc), attachment (param, slider) + { + addAndMakeVisible (slider); + addAndMakeVisible (bypassButton); + + // Clicking will bypass *everything* + bypassButton.onClick = [globalBypass] { if (globalBypass != nullptr) globalBypass (-1); }; + + setSize (300, 80); + } + + void resized() override + { + auto b = getLocalBounds(); + slider.setBounds (b.removeFromTop (40)); + bypassButton.setBounds (b); + } + + void paint (Graphics& g) override + { + g.fillAll (Colours::darkgrey); + } + +private: + Slider slider; + TextButton bypassButton { "global bypass" }; + SliderParameterAttachment attachment; +}; + +//============================================================================== +class ReaperEmbeddedViewDemo : public AudioProcessor, + public VSTCallbackHandler, + public VST3ClientExtensions, + private EmbeddedViewListener, + private Timer +{ +public: + ReaperEmbeddedViewDemo() + { + addParameter (gain = new AudioParameterFloat ("gain", "Gain", 0.0f, 1.0f, 0.5f)); + startTimerHz (60); + } + + void prepareToPlay (double, int) override {} + void reset() override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audio, MidiBuffer&) override { processBlockImpl (audio); } + void processBlock (AudioBuffer& audio, MidiBuffer&) override { processBlockImpl (audio); } + + //============================================================================== + AudioProcessorEditor* createEditor() override { return new Editor (*this, *gain, globalBypassFn); } + bool hasEditor() const override { return true; } + + //============================================================================== + const String getName() const override { return "ReaperEmbeddedViewDemo"; } + + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + + double getTailLengthSeconds() const override { return 0.0; } + + //============================================================================== + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram (int) override {} + const String getProgramName (int) override { return {}; } + + void changeProgramName (int, const String&) override {} + + //============================================================================== + void getStateInformation (MemoryBlock& destData) override + { + MemoryOutputStream (destData, true).writeFloat (*gain); + } + + void setStateInformation (const void* data, int sizeInBytes) override + { + gain->setValueNotifyingHost (MemoryInputStream (data, + static_cast (sizeInBytes), + false).readFloat()); + } + + int32_t queryIEditController (const Steinberg::TUID tuid, void** obj) override + { + if (std::memcmp (tuid, embeddedUi.iid, sizeof (Steinberg::TUID)) == 0) + { + *obj = &embeddedUi; + return Steinberg::kResultOk; + } + + *obj = nullptr; + return Steinberg::kNoInterface; + } + + void setIHostApplication (Steinberg::FUnknown* ptr) override + { + if (ptr == nullptr) + return; + + void* objPtr = nullptr; + + if (ptr->queryInterface (reaper::IReaperHostApplication::iid, &objPtr) == Steinberg::kResultOk) + { + if (void* fnPtr = static_cast (objPtr)->getReaperApi ("BypassFxAllTracks")) + globalBypassFn = reinterpret_cast (fnPtr); + } + } + + pointer_sized_int handleVstPluginCanDo (int32, pointer_sized_int, void* ptr, float) override + { + if (auto* str = static_cast (ptr)) + { + if (strcmp (str, "hasCockosEmbeddedUI") == 0) + return 0xbeef0000; + + if (strcmp (str, "hasCockosExtensions") == 0) + return 0xbeef0000; + } + + return 0; + } + + pointer_sized_int handleVstManufacturerSpecific (int32, + pointer_sized_int value, + void* ptr, + float opt) override + { + return (pointer_sized_int) handledEmbeddedUIMessage ((int) opt, + (Steinberg::TPtrInt) value, + (Steinberg::TPtrInt) ptr); + } + + void handleVstHostCallbackAvailable (std::function&& hostcb) override + { + char functionName[] = "BypassFxAllTracks"; + globalBypassFn = reinterpret_cast (hostcb ((int32_t) 0xdeadbeef, (int32_t) 0xdeadf00d, 0, functionName, 0.0)); + } + +private: + template + void processBlockImpl (AudioBuffer& audio) + { + audio.applyGain (*gain); + + const auto minMax = audio.findMinMax (0, 0, audio.getNumSamples()); + const auto newMax = (float) std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd())); + + auto loaded = storedLevel.load(); + while (loaded < newMax && ! storedLevel.compare_exchange_weak (loaded, newMax)) {} + } + + void timerCallback() override + { + levelToDraw = std::max (levelToDraw * 0.95f, storedLevel.exchange (0.0f)); + } + + Steinberg::TPtrInt getSizeInfo (reaper::REAPER_FXEMBED_SizeHints* sizeHints) + { + if (sizeHints == nullptr) + return 0; + + sizeHints->preferred_aspect = 1 << 16; + sizeHints->minimum_aspect = 1 << 16; + sizeHints->min_height = sizeHints->min_width = 50; + sizeHints->max_height = sizeHints->max_width = 1000; + return 1; + } + + Steinberg::TPtrInt doPaint (reaper::REAPER_FXEMBED_IBitmap* bitmap, + reaper::REAPER_FXEMBED_DrawInfo* drawInfo) + { + if (bitmap == nullptr || drawInfo == nullptr) + return 0; + + Image img (juce::Image::PixelFormat::ARGB, bitmap->getWidth(), bitmap->getHeight(), true); + Graphics g (img); + + g.fillAll (Colours::black); + + const auto bounds = g.getClipBounds(); + const auto corner = 3.0f; + + g.setColour (Colours::darkgrey); + g.fillRoundedRectangle (bounds.withSizeKeepingCentre (20, bounds.getHeight() - 6).toFloat(), + corner); + + const auto minDb = -50.0f; + const auto maxDb = 6.0f; + const auto levelInDb = Decibels::gainToDecibels (levelToDraw, minDb); + const auto fractionOfHeight = jmap (levelInDb, minDb, maxDb, 0.0f, 1.0f); + const auto trackBounds = bounds.withSizeKeepingCentre (16, bounds.getHeight() - 10).toFloat(); + + g.setColour (Colours::black); + const auto zeroDbIndicatorY = trackBounds.proportionOfHeight (jmap (0.0f, + minDb, + maxDb, + 0.0f, + 1.0f)); + g.drawHorizontalLine ((int) (trackBounds.getBottom() - zeroDbIndicatorY), + trackBounds.getX(), + trackBounds.getRight()); + + g.setGradientFill (ColourGradient (Colours::darkgreen, + { 0.0f, (float) bounds.getHeight() }, + Colours::darkred, + { 0.0f, 0.0f }, + false)); + + g.fillRoundedRectangle (trackBounds.withHeight (trackBounds.proportionOfHeight (fractionOfHeight)) + .withBottomY (trackBounds.getBottom()), + corner); + + Image::BitmapData imgData { img, Image::BitmapData::readOnly }; + const auto pixelsWidth = imgData.pixelStride * imgData.width; + + auto* px = bitmap->getBits(); + const auto rowSpan = bitmap->getRowSpan(); + const auto numRows = bitmap->getHeight(); + + for (int y = 0; y < numRows; ++y) + std::memcpy (px + (y * rowSpan), imgData.getLinePointer (y), (size_t) pixelsWidth); + + return 1; + } + + Steinberg::TPtrInt handledEmbeddedUIMessage (int msg, + Steinberg::TPtrInt parm2, + Steinberg::TPtrInt parm3) override + { + switch (msg) + { + case REAPER_FXEMBED_WM_IS_SUPPORTED: + return 1; + + case REAPER_FXEMBED_WM_PAINT: + return doPaint (reinterpret_cast (parm2), + reinterpret_cast (parm3)); + + case REAPER_FXEMBED_WM_GETMINMAXINFO: + return getSizeInfo (reinterpret_cast (parm3)); + + // Implementing mouse behaviour is left as an exercise for the reaper, I mean reader + case REAPER_FXEMBED_WM_CREATE: break; + case REAPER_FXEMBED_WM_DESTROY: break; + case REAPER_FXEMBED_WM_SETCURSOR: break; + case REAPER_FXEMBED_WM_MOUSEMOVE: break; + case REAPER_FXEMBED_WM_LBUTTONDOWN: break; + case REAPER_FXEMBED_WM_LBUTTONUP: break; + case REAPER_FXEMBED_WM_LBUTTONDBLCLK: break; + case REAPER_FXEMBED_WM_RBUTTONDOWN: break; + case REAPER_FXEMBED_WM_RBUTTONUP: break; + case REAPER_FXEMBED_WM_RBUTTONDBLCLK: break; + case REAPER_FXEMBED_WM_MOUSEWHEEL: break; + } + + return 0; + } + + AudioParameterFloat* gain = nullptr; + void (*globalBypassFn) (int) = nullptr; + EmbeddedUI embeddedUi { *this }; + + std::atomic storedLevel { 0.0f }; + float levelToDraw = 0.0f; +}; diff --git a/examples/Plugins/extern/LICENSE.md b/examples/Plugins/extern/LICENSE.md new file mode 100644 index 0000000000..87f896493c --- /dev/null +++ b/examples/Plugins/extern/LICENSE.md @@ -0,0 +1,19 @@ +The files in this folder were copied from the [reaper-sdk +repository](https://github.com/justinfrankel/reaper-sdk) on Github. At the time +of writing, these files were distributed under the following license: + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/examples/Plugins/extern/reaper_plugin_fx_embed.h b/examples/Plugins/extern/reaper_plugin_fx_embed.h new file mode 100644 index 0000000000..ccbfc9728b --- /dev/null +++ b/examples/Plugins/extern/reaper_plugin_fx_embed.h @@ -0,0 +1,140 @@ +#ifndef _REAPER_PLUGIN_FX_EMBED_H_ +#define _REAPER_PLUGIN_FX_EMBED_H_ + + +/* + * to support via VST2: canDo("hasCockosEmbeddedUI") should return 0xbeef0000 + * dispatcher will be called with opcode=effVendorSpecific, index=effEditDraw, value=parm2, ptr=(void*)(INT_PTR)parm3, opt=message (REAPER_FXEMBED_WM_*) + * + * to support via VST3: IController should support IReaperUIEmbedInterface, see reaper_vst3_interfaces.h + * + * to support via LV2: todo + */ + +// these alias to win32's WM_* + + +#define REAPER_FXEMBED_WM_IS_SUPPORTED 0x0000 +/* return 1 if embedding is supported and available + * return -1 if embedding is supported and unavailable + * return 0 if embedding is not supported +*/ + +#define REAPER_FXEMBED_WM_CREATE 0x0001 // called when embedding begins (return value ignored) +#define REAPER_FXEMBED_WM_DESTROY 0x0002 // called when embedding ends (return value ignored) + + + +typedef struct REAPER_FXEMBED_DrawInfo // alias of REAPER_inline_positioninfo +{ + int context; // 0=unknown (v6.23 and earlier), 1=TCP, 2=MCP + int dpi; // 0=unknown (v6.23 and earlier), otherwise 24.8 fixed point (256=100%) + int mousewheel_amt; // for REAPER_FXEMBED_WM_MOUSEWHEEL, 120 = step, typically + double _res2; + + int width, height; + int mouse_x, mouse_y; + + int flags; // REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL etc + int _res3; + + void *spare[6]; +} REAPER_FXEMBED_DrawInfo; + +#define REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL 1 +#define REAPER_FXEMBED_DRAWINFO_FLAG_LBUTTON_CAPTURED 0x10000 +#define REAPER_FXEMBED_DRAWINFO_FLAG_RBUTTON_CAPTURED 0x20000 + +#define REAPER_FXEMBED_WM_PAINT 0x000F +/* + * draw embedded UI. + * parm2: REAPER_FXEMBED_IBitmap * to draw into. note + * parm3: REAPER_FXEMBED_DrawInfo * + * + * if flags has REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL set, update is optional. if no change since last draw, return 0. + * if flags has REAPER_FXEMBED_DRAWINFO_FLAG_LBUTTON_CAPTURED set, left mouse button is down and captured + * if flags has REAPER_FXEMBED_DRAWINFO_FLAG_RBUTTON_CAPTURED set, right mouse button is down and captured + * + * HiDPI: + * if REAPER_FXEMBED_IBitmap::Extended(REAPER_FXEMBED_EXT_GET_ADVISORY_SCALING,NULL) returns nonzero, then it is a 24.8 scalefactor for UI drawing + * + * return 1 if drawing occurred, 0 otherwise. + * + */ + +#define REAPER_FXEMBED_WM_SETCURSOR 0x0020 // parm3: REAPER_FXEMBED_DrawInfo*. set mouse cursor and return REAPER_FXEMBED_RETNOTIFY_HANDLED, or return 0. + +#define REAPER_FXEMBED_WM_GETMINMAXINFO 0x0024 +/* + * get size hints. parm3 = (REAPER_FXEMBED_SizeHints*). return 1 if supported + * note that these are just hints, the actual size may vary + */ +typedef struct REAPER_FXEMBED_SizeHints { // alias to MINMAXINFO + int preferred_aspect; // 16.16 fixed point (65536 = 1:1, 32768 = 1:2, etc) + int minimum_aspect; // 16.16 fixed point + + int _res1, _res2, _res3, _res4; + + int min_width, min_height; + int max_width, max_height; +} REAPER_FXEMBED_SizeHints; + +/* + * mouse messages + * parm3 = (REAPER_FXEMBED_DrawInfo*) + * capture is automatically set on mouse down, released on mouse up + * when not captured, will always receive a mousemove when exiting the window + */ + +#define REAPER_FXEMBED_WM_MOUSEMOVE 0x0200 +#define REAPER_FXEMBED_WM_LBUTTONDOWN 0x0201 +#define REAPER_FXEMBED_WM_LBUTTONUP 0x0202 +#define REAPER_FXEMBED_WM_LBUTTONDBLCLK 0x0203 +#define REAPER_FXEMBED_WM_RBUTTONDOWN 0x0204 +#define REAPER_FXEMBED_WM_RBUTTONUP 0x0205 +#define REAPER_FXEMBED_WM_RBUTTONDBLCLK 0x0206 +#define REAPER_FXEMBED_WM_MOUSEWHEEL 0x020A + + +/* REAPER_FXEMBED_WM_SETCURSOR should return REAPER_FXEMBED_RETNOTIFY_HANDLED if a cursor was set + */ +#define REAPER_FXEMBED_RETNOTIFY_HANDLED 0x0000001 + +/* if the mouse messages return with REAPER_FXEMBED_RETNOTIFY_INVALIDATE set, a non-optional + * redraw is initiated (generally sooner than the next timer-based redraw) + */ +#define REAPER_FXEMBED_RETNOTIFY_INVALIDATE 0x1000000 + +/* + * bitmap interface + * this is an alias of LICE_IBitmap etc from WDL/lice/lice.h + * + */ +#define REAPER_FXEMBED_RGBA(r,g,b,a) (((b)&0xff)|(((g)&0xff)<<8)|(((r)&0xff)<<16)|(((a)&0xff)<<24)) +#define REAPER_FXEMBED_GETB(v) ((v)&0xff) +#define REAPER_FXEMBED_GETG(v) (((v)>>8)&0xff) +#define REAPER_FXEMBED_GETR(v) (((v)>>16)&0xff) +#define REAPER_FXEMBED_GETA(v) (((v)>>24)&0xff) + +#ifdef __cplusplus +class REAPER_FXEMBED_IBitmap // alias of LICE_IBitmap +{ +public: + virtual ~REAPER_FXEMBED_IBitmap() { } + + virtual unsigned int *getBits()=0; + virtual int getWidth()=0; + virtual int getHeight()=0; + virtual int getRowSpan()=0; // includes any off-bitmap data. this is in sizeof(unsigned int) units, not bytes. + virtual bool isFlipped() { return false; } + virtual bool resize(int w, int h)=0; + + virtual void *getDC() { return 0; } // do not use + + virtual INT_PTR Extended(int id, void* data) { return 0; } +}; +#endif + +#define REAPER_FXEMBED_EXT_GET_ADVISORY_SCALING 0x2003 // data ignored, returns .8 fixed point. returns 0 if unscaled + +#endif diff --git a/examples/Plugins/extern/reaper_vst3_interfaces.h b/examples/Plugins/extern/reaper_vst3_interfaces.h new file mode 100644 index 0000000000..0dce806e96 --- /dev/null +++ b/examples/Plugins/extern/reaper_vst3_interfaces.h @@ -0,0 +1,31 @@ +#ifndef _REAPER_VST3_INTERFACES_H_ +#define _REAPER_VST3_INTERFACES_H_ + +class IReaperHostApplication : public FUnknown // available from IHostApplication in REAPER v5.02+ +{ +public: + // Gets a REAPER Extension API function by name, returns NULL is failed + virtual void* PLUGIN_API getReaperApi(CStringA funcname) = 0; + + virtual void* PLUGIN_API getReaperParent(uint32 w) = 0; // get parent track(=1), take(=2), project(=3), fxdsp(=4), trackchan(=5) + + // Multi-purpose function, returns NULL if unsupported + virtual void* PLUGIN_API reaperExtended(uint32 call, void *parm1, void *parm2, void *parm3) = 0; + + static const FUID iid; +}; + +DECLARE_CLASS_IID (IReaperHostApplication, 0x79655E36, 0x77EE4267, 0xA573FEF7, 0x4912C27C) + +class IReaperUIEmbedInterface : public FUnknown // supported by REAPER v6.24+, queried from plug-in IController +{ + public: + // note: VST2 uses CanDo "hasCockosEmbeddedUI"==0xbeef0000, then opcode=effVendorSpecific, index=effEditDraw, opt=(float)msg, value=parm2, ptr=parm3 + // see reaper_plugin_fx_embed.h + virtual Steinberg::TPtrInt embed_message(int msg, Steinberg::TPtrInt parm2, Steinberg::TPtrInt parm3) = 0; + + static const FUID iid; +}; + +DECLARE_CLASS_IID (IReaperUIEmbedInterface, 0x049bf9e7, 0xbc74ead0, 0xc4101e86, 0x7f725981) +#endif