424 lines
16 KiB
C
424 lines
16 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
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
|
||
|
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wshadow-field-in-constructor",
|
||
|
"-Wnon-virtual-dtor")
|
||
|
|
||
|
#include <pluginterfaces/base/ftypes.h>
|
||
|
#include <pluginterfaces/base/funknown.h>
|
||
|
#include <pluginterfaces/vst/ivsthostapplication.h>
|
||
|
#include <pluginterfaces/vst2.x/aeffect.h>
|
||
|
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
|
||
|
namespace reaper
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-as-null-pointer-constant",
|
||
|
"-Wunused-parameter",
|
||
|
"-Wnon-virtual-dtor")
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100)
|
||
|
|
||
|
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_MSVC
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
struct EmbeddedViewListener
|
||
|
{
|
||
|
virtual ~EmbeddedViewListener() = default;
|
||
|
virtual Steinberg::TPtrInt handledEmbeddedUIMessage (int msg,
|
||
|
Steinberg::TPtrInt parm2,
|
||
|
Steinberg::TPtrInt parm3) = 0;
|
||
|
};
|
||
|
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnon-virtual-dtor")
|
||
|
|
||
|
//==============================================================================
|
||
|
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 ++refCount; }
|
||
|
Steinberg::uint32 PLUGIN_API release() override { return --refCount; }
|
||
|
|
||
|
Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID tuid, void** obj) override
|
||
|
{
|
||
|
if (std::memcmp (tuid, iid, sizeof (Steinberg::TUID)) == 0)
|
||
|
{
|
||
|
++refCount;
|
||
|
*obj = this;
|
||
|
return Steinberg::kResultOk;
|
||
|
}
|
||
|
|
||
|
*obj = nullptr;
|
||
|
return Steinberg::kNoInterface;
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
EmbeddedViewListener& listener;
|
||
|
std::atomic<Steinberg::uint32> refCount { 1 };
|
||
|
};
|
||
|
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
|
||
|
//==============================================================================
|
||
|
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<float>& audio, MidiBuffer&) override { processBlockImpl (audio); }
|
||
|
void processBlock (AudioBuffer<double>& 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 "None"; }
|
||
|
|
||
|
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<size_t> (sizeInBytes),
|
||
|
false).readFloat());
|
||
|
}
|
||
|
|
||
|
int32_t queryIEditController (const Steinberg::TUID tuid, void** obj) override
|
||
|
{
|
||
|
if (embeddedUi.queryInterface (tuid, obj) == Steinberg::kResultOk)
|
||
|
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<reaper::IReaperHostApplication*> (objPtr)->getReaperApi ("BypassFxAllTracks"))
|
||
|
globalBypassFn = reinterpret_cast<void (*) (int)> (fnPtr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pointer_sized_int handleVstPluginCanDo (int32, pointer_sized_int, void* ptr, float) override
|
||
|
{
|
||
|
if (auto* str = static_cast<const char*> (ptr))
|
||
|
{
|
||
|
if (strcmp (str, "hasCockosEmbeddedUI") == 0)
|
||
|
return 0xbeef0000;
|
||
|
|
||
|
if (strcmp (str, "hasCockosExtensions") == 0)
|
||
|
return 0xbeef0000;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
pointer_sized_int handleVstManufacturerSpecific (int32 index,
|
||
|
pointer_sized_int value,
|
||
|
void* ptr,
|
||
|
float opt) override
|
||
|
{
|
||
|
// The docstring at the top of reaper_plugin_fx_embed.h specifies
|
||
|
// that the index will always be effEditDraw, which is now deprecated.
|
||
|
if (index != __effEditDrawDeprecated)
|
||
|
return 0;
|
||
|
|
||
|
return (pointer_sized_int) handledEmbeddedUIMessage ((int) opt,
|
||
|
(Steinberg::TPtrInt) value,
|
||
|
(Steinberg::TPtrInt) ptr);
|
||
|
}
|
||
|
|
||
|
void handleVstHostCallbackAvailable (std::function<VstHostCallbackType>&& hostcb) override
|
||
|
{
|
||
|
char functionName[] = "BypassFxAllTracks";
|
||
|
globalBypassFn = reinterpret_cast<void (*) (int)> (hostcb ((int32_t) 0xdeadbeef, (int32_t) 0xdeadf00d, 0, functionName, 0.0));
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
template <typename Float>
|
||
|
void processBlockImpl (AudioBuffer<Float>& 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 || bitmap->getWidth() <= 0 || bitmap->getHeight() <= 0)
|
||
|
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<reaper::REAPER_FXEMBED_IBitmap*> (parm2),
|
||
|
reinterpret_cast<reaper::REAPER_FXEMBED_DrawInfo*> (parm3));
|
||
|
|
||
|
case REAPER_FXEMBED_WM_GETMINMAXINFO:
|
||
|
return getSizeInfo (reinterpret_cast<reaper::REAPER_FXEMBED_SizeHints*> (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<float> storedLevel { 0.0f };
|
||
|
float levelToDraw = 0.0f;
|
||
|
};
|