The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

448 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: ReaperEmbeddedViewDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: An audio plugin which embeds a secondary view in VST2 and
  24. VST3 formats in REAPER
  25. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
  26. juce_audio_plugin_client, juce_audio_processors,
  27. juce_audio_utils, juce_core, juce_data_structures,
  28. juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
  29. exporters: xcode_mac, vs2022, linux_make
  30. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  31. type: AudioProcessor
  32. mainClass: ReaperEmbeddedViewDemo
  33. useLocalCopy: 1
  34. END_JUCE_PIP_METADATA
  35. *******************************************************************************/
  36. /* This demo shows how to use the VST2ClientExtensions and VST3ClientExtensions
  37. classes to provide extended functionality in compatible VST/VST3 hosts.
  38. If this project is built as a VST or VST3 plugin and loaded in REAPER
  39. 6.29 or higher, it will provide an embedded level meter in the track
  40. control panel. To enable the embedded view, right-click on the plugin
  41. and select "Show embedded UI in TCP".
  42. The plugin's editor also include a button which can be used to toggle
  43. all inserts on and off.
  44. */
  45. #pragma once
  46. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wshadow-field-in-constructor",
  47. "-Wnon-virtual-dtor")
  48. #include <pluginterfaces/base/ftypes.h>
  49. #include <pluginterfaces/base/funknown.h>
  50. #include <pluginterfaces/vst/ivsthostapplication.h>
  51. #include <pluginterfaces/vst2.x/aeffect.h>
  52. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  53. namespace reaper
  54. {
  55. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-as-null-pointer-constant",
  56. "-Wunused-parameter",
  57. "-Wnon-virtual-dtor")
  58. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100)
  59. using namespace Steinberg;
  60. using INT_PTR = pointer_sized_int;
  61. using uint32 = Steinberg::uint32;
  62. #include "extern/reaper_plugin_fx_embed.h"
  63. #include "extern/reaper_vst3_interfaces.h"
  64. //==============================================================================
  65. /* These should live in a file which is guaranteed to be compiled only once
  66. (i.e. a .cpp file, normally). This demo is a bit special, because we know
  67. that this header will only be included in a single translation unit.
  68. */
  69. DEF_CLASS_IID (IReaperHostApplication)
  70. DEF_CLASS_IID (IReaperUIEmbedInterface)
  71. JUCE_END_IGNORE_WARNINGS_MSVC
  72. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  73. }
  74. //==============================================================================
  75. struct EmbeddedViewListener
  76. {
  77. virtual ~EmbeddedViewListener() = default;
  78. virtual Steinberg::TPtrInt handledEmbeddedUIMessage (int msg,
  79. Steinberg::TPtrInt parm2,
  80. Steinberg::TPtrInt parm3) = 0;
  81. virtual void setGlobalBypassFunction (void (*) (int)) = 0;
  82. };
  83. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnon-virtual-dtor")
  84. //==============================================================================
  85. class EmbeddedUI : public reaper::IReaperUIEmbedInterface
  86. {
  87. public:
  88. explicit EmbeddedUI (EmbeddedViewListener& demo) : listener (demo) {}
  89. Steinberg::TPtrInt embed_message (int msg,
  90. Steinberg::TPtrInt parm2,
  91. Steinberg::TPtrInt parm3) override
  92. {
  93. return listener.handledEmbeddedUIMessage (msg, parm2, parm3);
  94. }
  95. Steinberg::uint32 PLUGIN_API addRef() override { return ++refCount; }
  96. Steinberg::uint32 PLUGIN_API release() override { return --refCount; }
  97. Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID tuid, void** obj) override
  98. {
  99. if (std::memcmp (tuid, iid, sizeof (Steinberg::TUID)) == 0)
  100. {
  101. ++refCount;
  102. *obj = this;
  103. return Steinberg::kResultOk;
  104. }
  105. *obj = nullptr;
  106. return Steinberg::kNoInterface;
  107. }
  108. private:
  109. EmbeddedViewListener& listener;
  110. std::atomic<Steinberg::uint32> refCount { 1 };
  111. };
  112. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  113. class VST2Extensions : public VST2ClientExtensions
  114. {
  115. public:
  116. explicit VST2Extensions (EmbeddedViewListener& l)
  117. : listener (l) {}
  118. pointer_sized_int handleVstPluginCanDo (int32, pointer_sized_int, void* ptr, float) override
  119. {
  120. if (auto* str = static_cast<const char*> (ptr))
  121. for (auto* key : { "hasCockosEmbeddedUI", "hasCockosExtensions" })
  122. if (strcmp (str, key) == 0)
  123. return (pointer_sized_int) 0xbeef0000;
  124. return 0;
  125. }
  126. pointer_sized_int handleVstManufacturerSpecific (int32 index,
  127. pointer_sized_int value,
  128. void* ptr,
  129. float opt) override
  130. {
  131. // The docstring at the top of reaper_plugin_fx_embed.h specifies
  132. // that the index will always be effEditDraw, which is now deprecated.
  133. if (index != __effEditDrawDeprecated)
  134. return 0;
  135. return (pointer_sized_int) listener.handledEmbeddedUIMessage ((int) opt,
  136. (Steinberg::TPtrInt) value,
  137. (Steinberg::TPtrInt) ptr);
  138. }
  139. void handleVstHostCallbackAvailable (std::function<VstHostCallbackType>&& hostcb) override
  140. {
  141. char functionName[] = "BypassFxAllTracks";
  142. listener.setGlobalBypassFunction (reinterpret_cast<void (*) (int)> (hostcb ((int32_t) 0xdeadbeef, (int32_t) 0xdeadf00d, 0, functionName, 0.0)));
  143. }
  144. private:
  145. EmbeddedViewListener& listener;
  146. };
  147. class VST3Extensions : public VST3ClientExtensions
  148. {
  149. public:
  150. explicit VST3Extensions (EmbeddedViewListener& l)
  151. : listener (l) {}
  152. int32_t queryIEditController (const Steinberg::TUID tuid, void** obj) override
  153. {
  154. if (embeddedUi.queryInterface (tuid, obj) == Steinberg::kResultOk)
  155. return Steinberg::kResultOk;
  156. *obj = nullptr;
  157. return Steinberg::kNoInterface;
  158. }
  159. void setIHostApplication (Steinberg::FUnknown* ptr) override
  160. {
  161. if (ptr == nullptr)
  162. return;
  163. void* objPtr = nullptr;
  164. if (ptr->queryInterface (reaper::IReaperHostApplication::iid, &objPtr) == Steinberg::kResultOk)
  165. {
  166. if (void* fnPtr = static_cast<reaper::IReaperHostApplication*> (objPtr)->getReaperApi ("BypassFxAllTracks"))
  167. listener.setGlobalBypassFunction (reinterpret_cast<void (*) (int)> (fnPtr));
  168. }
  169. }
  170. private:
  171. EmbeddedViewListener& listener;
  172. EmbeddedUI embeddedUi { listener };
  173. };
  174. //==============================================================================
  175. class Editor : public AudioProcessorEditor
  176. {
  177. public:
  178. explicit Editor (AudioProcessor& proc,
  179. AudioParameterFloat& param,
  180. void (*globalBypass) (int))
  181. : AudioProcessorEditor (proc), attachment (param, slider)
  182. {
  183. addAndMakeVisible (slider);
  184. addAndMakeVisible (bypassButton);
  185. // Clicking will bypass *everything*
  186. bypassButton.onClick = [globalBypass] { if (globalBypass != nullptr) globalBypass (-1); };
  187. setSize (300, 80);
  188. }
  189. void resized() override
  190. {
  191. auto b = getLocalBounds();
  192. slider.setBounds (b.removeFromTop (40));
  193. bypassButton.setBounds (b);
  194. }
  195. void paint (Graphics& g) override
  196. {
  197. g.fillAll (Colours::darkgrey);
  198. }
  199. private:
  200. Slider slider;
  201. TextButton bypassButton { "global bypass" };
  202. SliderParameterAttachment attachment;
  203. };
  204. //==============================================================================
  205. class ReaperEmbeddedViewDemo : public AudioProcessor,
  206. private EmbeddedViewListener,
  207. private Timer
  208. {
  209. public:
  210. ReaperEmbeddedViewDemo()
  211. {
  212. addParameter (gain = new AudioParameterFloat ({ "gain", 1 }, "Gain", 0.0f, 1.0f, 0.5f));
  213. startTimerHz (60);
  214. }
  215. void prepareToPlay (double, int) override {}
  216. void reset() override {}
  217. void releaseResources() override {}
  218. void processBlock (AudioBuffer<float>& audio, MidiBuffer&) override { processBlockImpl (audio); }
  219. void processBlock (AudioBuffer<double>& audio, MidiBuffer&) override { processBlockImpl (audio); }
  220. //==============================================================================
  221. AudioProcessorEditor* createEditor() override { return new Editor (*this, *gain, globalBypassFn); }
  222. bool hasEditor() const override { return true; }
  223. //==============================================================================
  224. const String getName() const override { return "ReaperEmbeddedViewDemo"; }
  225. bool acceptsMidi() const override { return false; }
  226. bool producesMidi() const override { return false; }
  227. bool isMidiEffect() const override { return false; }
  228. double getTailLengthSeconds() const override { return 0.0; }
  229. //==============================================================================
  230. int getNumPrograms() override { return 1; }
  231. int getCurrentProgram() override { return 0; }
  232. void setCurrentProgram (int) override {}
  233. const String getProgramName (int) override { return "None"; }
  234. void changeProgramName (int, const String&) override {}
  235. //==============================================================================
  236. void getStateInformation (MemoryBlock& destData) override
  237. {
  238. MemoryOutputStream (destData, true).writeFloat (*gain);
  239. }
  240. void setStateInformation (const void* data, int sizeInBytes) override
  241. {
  242. gain->setValueNotifyingHost (MemoryInputStream (data,
  243. static_cast<size_t> (sizeInBytes),
  244. false).readFloat());
  245. }
  246. VST2ClientExtensions* getVST2ClientExtensions() override { return &vst2Extensions; }
  247. VST3ClientExtensions* getVST3ClientExtensions() override { return &vst3Extensions; }
  248. private:
  249. template <typename Float>
  250. void processBlockImpl (AudioBuffer<Float>& audio)
  251. {
  252. audio.applyGain (*gain);
  253. const auto minMax = audio.findMinMax (0, 0, audio.getNumSamples());
  254. const auto newMax = (float) std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd()));
  255. auto loaded = storedLevel.load();
  256. while (loaded < newMax && ! storedLevel.compare_exchange_weak (loaded, newMax)) {}
  257. }
  258. void timerCallback() override
  259. {
  260. levelToDraw = std::max (levelToDraw * 0.95f, storedLevel.exchange (0.0f));
  261. }
  262. Steinberg::TPtrInt getSizeInfo (reaper::REAPER_FXEMBED_SizeHints* sizeHints)
  263. {
  264. if (sizeHints == nullptr)
  265. return 0;
  266. sizeHints->preferred_aspect = 1 << 16;
  267. sizeHints->minimum_aspect = 1 << 16;
  268. sizeHints->min_height = sizeHints->min_width = 50;
  269. sizeHints->max_height = sizeHints->max_width = 1000;
  270. return 1;
  271. }
  272. Steinberg::TPtrInt doPaint (reaper::REAPER_FXEMBED_IBitmap* bitmap,
  273. reaper::REAPER_FXEMBED_DrawInfo* drawInfo)
  274. {
  275. if (bitmap == nullptr || drawInfo == nullptr || bitmap->getWidth() <= 0 || bitmap->getHeight() <= 0)
  276. return 0;
  277. Image img (juce::Image::PixelFormat::ARGB, bitmap->getWidth(), bitmap->getHeight(), true);
  278. Graphics g (img);
  279. g.fillAll (Colours::black);
  280. const auto bounds = g.getClipBounds();
  281. const auto corner = 3.0f;
  282. g.setColour (Colours::darkgrey);
  283. g.fillRoundedRectangle (bounds.withSizeKeepingCentre (20, bounds.getHeight() - 6).toFloat(),
  284. corner);
  285. const auto minDb = -50.0f;
  286. const auto maxDb = 6.0f;
  287. const auto levelInDb = Decibels::gainToDecibels (levelToDraw, minDb);
  288. const auto fractionOfHeight = jmap (levelInDb, minDb, maxDb, 0.0f, 1.0f);
  289. const auto trackBounds = bounds.withSizeKeepingCentre (16, bounds.getHeight() - 10).toFloat();
  290. g.setColour (Colours::black);
  291. const auto zeroDbIndicatorY = trackBounds.proportionOfHeight (jmap (0.0f,
  292. minDb,
  293. maxDb,
  294. 0.0f,
  295. 1.0f));
  296. g.drawHorizontalLine ((int) (trackBounds.getBottom() - zeroDbIndicatorY),
  297. trackBounds.getX(),
  298. trackBounds.getRight());
  299. g.setGradientFill (ColourGradient (Colours::darkgreen,
  300. { 0.0f, (float) bounds.getHeight() },
  301. Colours::darkred,
  302. { 0.0f, 0.0f },
  303. false));
  304. g.fillRoundedRectangle (trackBounds.withHeight (trackBounds.proportionOfHeight (fractionOfHeight))
  305. .withBottomY (trackBounds.getBottom()),
  306. corner);
  307. Image::BitmapData imgData { img, Image::BitmapData::readOnly };
  308. const auto pixelsWidth = imgData.pixelStride * imgData.width;
  309. auto* px = bitmap->getBits();
  310. const auto rowSpan = bitmap->getRowSpan();
  311. const auto numRows = bitmap->getHeight();
  312. for (int y = 0; y < numRows; ++y)
  313. std::memcpy (px + (y * rowSpan), imgData.getLinePointer (y), (size_t) pixelsWidth);
  314. return 1;
  315. }
  316. Steinberg::TPtrInt handledEmbeddedUIMessage (int msg,
  317. Steinberg::TPtrInt parm2,
  318. Steinberg::TPtrInt parm3) override
  319. {
  320. switch (msg)
  321. {
  322. case REAPER_FXEMBED_WM_IS_SUPPORTED:
  323. return 1;
  324. case REAPER_FXEMBED_WM_PAINT:
  325. return doPaint (reinterpret_cast<reaper::REAPER_FXEMBED_IBitmap*> (parm2),
  326. reinterpret_cast<reaper::REAPER_FXEMBED_DrawInfo*> (parm3));
  327. case REAPER_FXEMBED_WM_GETMINMAXINFO:
  328. return getSizeInfo (reinterpret_cast<reaper::REAPER_FXEMBED_SizeHints*> (parm3));
  329. // Implementing mouse behaviour is left as an exercise for the reaper, I mean reader
  330. case REAPER_FXEMBED_WM_CREATE: break;
  331. case REAPER_FXEMBED_WM_DESTROY: break;
  332. case REAPER_FXEMBED_WM_SETCURSOR: break;
  333. case REAPER_FXEMBED_WM_MOUSEMOVE: break;
  334. case REAPER_FXEMBED_WM_LBUTTONDOWN: break;
  335. case REAPER_FXEMBED_WM_LBUTTONUP: break;
  336. case REAPER_FXEMBED_WM_LBUTTONDBLCLK: break;
  337. case REAPER_FXEMBED_WM_RBUTTONDOWN: break;
  338. case REAPER_FXEMBED_WM_RBUTTONUP: break;
  339. case REAPER_FXEMBED_WM_RBUTTONDBLCLK: break;
  340. case REAPER_FXEMBED_WM_MOUSEWHEEL: break;
  341. }
  342. return 0;
  343. }
  344. void setGlobalBypassFunction (void (*fn) (int)) override { globalBypassFn = fn; }
  345. AudioParameterFloat* gain = nullptr;
  346. void (*globalBypassFn) (int) = nullptr;
  347. std::atomic<float> storedLevel { 0.0f };
  348. float levelToDraw = 0.0f;
  349. VST2Extensions vst2Extensions { *this };
  350. VST3Extensions vst3Extensions { *this };
  351. };