So I have spent the past week or so trying to minimally/generically patch JUCE so that an upstream patch can be landed with support for REAPER's VST3 interfaces -- particularly interested in TCP FX embedding.
(Have spoken briefly w/ JUCE devs about this, there is some interest/receptiveness on their side depending on implementation)
To get the base REAPER SDK working (IReaperApplication->getReaperApi()) with a "reaper.ShowConsoleMsg()" was very few changes.
---
However I wanted to ask two quick questions, as trying to get IReaperUIEmbedInterface functional did not work based on what I assumed.
In "reaper_vst3_interfaces.h" and "reaper_plugin_fx_embed.h", the interface given for IReaperUIEmbedInterface is IController. But IController is a VSTGUI-specific interface, I assumed this was a typo and instead it was IEditController from VST3 SDK. Is this right or wrong?
If it's right, part of what I have done is modified JUCE's JuceVST3EditController (see below) to subclass/implement IReaperUIEmbedInterface, and I have overriden the ::embed_message() method, but nothing is ever called. Is there any way of debugging this from REAPER?
Would be grateful for any advise/feedback from folks who have a better clue about what they're doing than me. Getting this working and publishing it for others would be stellar!
JUCE patches made:
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
#ifdef JUCE_VST3_ENABLE_PASS_HOST_CONTEXT_TO_AUDIO_PROCESSOR_ON_INITIALIZE
#include "../../include/vendor/reaper-
DEF_CLASS_IID(IReaperUIEmbedInterface)
#endif
class JuceVST3EditController : public Vst::EditController,
public Vst::IMidiMapping,
public Vst::IUnitInfo,
public Vst::ChannelContext::IInfoListener,
public AudioProcessorListener
#ifdef JUCE_VST3_ENABLE_PASS_HOST_CONTEXT_TO_AUDIO_PROCESSOR_ON_INITIALIZE
, public IReaperUIEmbedInterface
#endif
{
public:
JuceVST3EditController (Vst::IHostApplication* host)
{
if (host != nullptr)
host->queryInterface (FUnknown::iid, (void**) &hostContext);
}
In "reaper_vst3_interfaces.h" and "reaper_plugin_fx_embed.h", the interface given for IReaperUIEmbedInterface is IController. But IController is a VSTGUI-specific interface, I assumed this was a typo and instead it was IEditController from VST3 SDK. Is this right or wrong?
You're correct, this is a typo in the documentation. The plugin's IEditController should implement IReaperUIEmbedInterface.
This is a bug on our side, sorry for the trouble! We'll fix this for the next +dev build.
Thank you Schwa -- appreciate you guys a ton!!
Will keep doing my part to keep convo going with JUCE folks and see if we can get an acceptable generic implementation merged upstream, so that all JUCE projects have optional out-of-the-box functionality for REAPER UI embedding + SDK hosting in both VST2 + VST3
<3
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
Haven't had time to test thoroughly, but the greyed-out "Show/Embed FX in TCP" option is now clickable in my plugin FX slot in 6.30 dev, and after enabling it and trying to expand the track the plugin crashed.
Which is good! It means I fucked up, but that that REAPER let me fuck up.
So it sounds like this is going to be a fun and productive weekend =)
Will continue dev efforts after brief sleep.
Once again, really appreciate you folks
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
I'm going to go off topic from original post here if that's alright
---
I'd like to ask for some basic C++ code-review/help from anyone here experienced and willing to spare it.
I have been working with JUCE devs on allowing users to implement REAPER's VST3 interfaces for TCP/MCP FX embedding and access to it's C++ API.
(So that we can get it merged upstream and all JUCE VST2/VST3 plugins can optionally choose to be REAPER-capable by default)
Got a rough version working but it was hacky (see below), then after talking to one of the JUCE dev's have been swapping it out for his implementation idea.
Problem is, I'm hitting the limits of my knowledge trying to debug why this isn't working.
So I have modified JUCE's IEditController implementation to allow runtime registration of VST3 interfaces and lookups during it's queryInterface() method.
The instance of the REAPER IReaperUIEmbedInterface class is getting put in here.
// I'm defining the same class IID as the IReaperUIEmbedInterface so that this implementation class // gets resolved by this IID during lookups DEF_CLASS_IID(IReaperUIEmbedInterface) DECLARE_CLASS_IID(ReaperVST3EditController, 0x049bf9e7, 0xbc74ead0, 0xc4101e86, 0x7f725981) class ReaperVST3EditController : public Steinberg::FObject, public IReaperUIEmbedInterface { private: // PLUGIN_API = __stdcall virtual Steinberg::uint32 PLUGIN_API addRef () override { return Steinberg::FObject::addRef (); } virtual Steinberg::uint32 PLUGIN_API release () override { return Steinberg::FObject::release (); }
But the ReaperVST3EditController::embed_message() and every other function on the class is never called
Never called at all, or never called after the initial REAPER_FXEMBED_WM_IS_SUPPORTED query? REAPER won't call again if the initial query returns 0.
This is what the code looks like from REAPER's side. if queryInterface() is being called for IReaperUIEmbedInterface and returning properly, embed_message() should be called immediately.
If your code is receiving the interface query from REAPER, as your screenshot/breakpoint suggests, my guess is that it's actually working properly, but you're not seeing the debug message in embed_message() for some reason.
I suppose another possibility is that your interface registration code is returning the IReaperUIEmbedInterface interface for the wrong query. You could dump the actual IID there to make sure.
Really appreciate you responding here and the offer to give a glimpse from REAPER's end.
Let me push current work up to github repo, then will post here =)
I touched C++ first time maybe 6 weeks ago (to add some popular single-header C++ imgui components to ReaImgui & learn REAPER's native API) and that was my "hello world" then it was onto this, this language is kicking my butt. Not an easy one to learn!
Quote:
Originally Posted by schwa
Never called at all, or never called after the initial REAPER_FXEMBED_WM_IS_SUPPORTED query?
The actual invocation of ReaperVST3EditController::embed_message() is never called.
The code you see from the first post, where I had hackily #ifdef'ed the subclassing/extension of IReaperUIEmbedInterface onto JUCE's actual JuceVST3EditController did work!
Right after the v6.28+dev0430 patch (thank you again)
Quote:
Originally Posted by schwa
If your code is receiving the interface query from REAPER, as your screenshot/breakpoint suggests, my guess is that it's actually working properly, but you're not seeing the debug message in embed_message() for some reason.
Hmm -- I think something is maybe wrong with my actual implementation of either the class, or maybe the VST3 COM IID registration stuff? (see below in the logs of targetIID)
Quote:
Originally Posted by schwa
I suppose another possibility is that your interface registration code is returning the IReaperUIEmbedInterface interface for the wrong query. You could dump the actual IID there to make sure.
So I had added this statement inside of queryInterface():
I'm not really sure how C++ works, I can see that IReaperUIEmbedInterface already has a class IID defined.
To provide an instance of it, I wasn't really sure what to do so I just did the below -- and it might be some weirdness with me using the same class IID for my implementation class as the IReaperUIEmbedInterface.
The reason I first noticed this was very broken was when I changed case REAPER_FXEMBED_WM_IS_SUPPORTED: to return 0 and it still gave me the option in context-menu to embed in TCP, then just drew a black square.
// I'm defining the same class IID as the IReaperUIEmbedInterface so that this implementation class // gets resolved by this IID during lookups DEF_CLASS_IID(IReaperUIEmbedInterface) DECLARE_CLASS_IID(ReaperVST3EditController, 0x049bf9e7, 0xbc74ead0, 0xc4101e86, 0x7f725981)
// IReaperUIEmbedInterface has to be private (I think?), because it extends FUnknown and FObject extends FUnknown // so an ambiguous base would occur when trying to cast ReaperVST3EditController -> FUnknown // (The problem: Which FUnknown -- FObject's or IReaperUIEmbed's?) class ReaperVST3EditController : public FObject, private IReaperUIEmbedInterface { private: // PLUGIN_API = __stdcall virtual Steinberg::uint32 PLUGIN_API addRef () override { return Steinberg::FObject::addRef (); } virtual Steinberg::uint32 PLUGIN_API release () override { return Steinberg::FObject::release (); }
So I annotated every single method (ctor, dtor, addRef, release, queryInterface, and embed_message) with logging.
And I faked a call to embed_message() right before I return pointer to the object and it works, the call gets logged.
PHP Code:
auto klazz = this->findInterface(targetIID); if (klazz != nullptr) { DBG("[JuceVST3Component::queryInterface] FOUND MATCH \n targetIID=" << fuidStr << "\n klazzIID=" << tuidToFUIDString(klazz->iid)); klazz->addRef(); auto embedResult = dynamic_cast<ReaperVST3EditController*> (klazz)->embed_message(0, 100, 100); *obj = klazz; return Steinberg::kResultOk; }
PHP Code:
[ReaperVST3EditController::Constructor()] ReaperVST3EditController created [JuceVST3Component::queryInterface] targetIID=DCD7BBE37742448DA874AACC979C759E [JuceVST3Component::queryInterface] targetIID=70A4156F6E6E4026989148BFAA60D8D1 [JuceVST3Component::queryInterface] targetIID=ABCDEF011234ABCD4A75636544656D30 [JuceVST3Component::queryInterface] targetIID=B7F8F85941234872911695814F3721A3 [JuceVST3Component::queryInterface] targetIID=1F2F76D3BFFB4B96B99527A55EBCCEF4 [JuceVST3Component::queryInterface] targetIID=3D4BD6B5913A4FD2A886E768A5EB92C1 [JuceVST3Component::queryInterface] targetIID=31E29A7AE55043AD8B95B9B8DA1FBE1E [JuceVST3Component::queryInterface] targetIID=C3B17BC02C17449480293402FBC4BBF8 [JuceVST3Component::queryInterface] targetIID=0F1947818D984ADABBA0C1EFC011D8D0 [JuceVST3Component::queryInterface] targetIID=049BF9E7BC74EAD0C4101E867F725981 [JuceVST3Component::queryInterface] FOUND MATCH targetIID=049BF9E7BC74EAD0C4101E867F725981 [ReaperVST3EditController::addRef()] CALLED [ReaperVST3EditController::embed_message] msg = 0 parm2 = 100 parm3 = 100 // Removed the plugin [ReaperVST3EditController::release()] CALLED // Destructor is never called I guess? Was supposed to log it's call
Below is a video of debugging it + behavior in REAPER and showing the log messages.
Does calling casts like dynamic_cast<T> and static_cast<T> on pointers that get sent to other functions change runtime behavior?
That's the only difference I can think of -- the pointer object is not casted before being returned whereas "TEST_FOR_AND_RETURN_IF_VALID" does a cast to the class type.
Thanks for posting the binary. REAPER is calling your embed_message(). If you're not receiving the call on your side, it may be because ReaperVST3EditController needs to implement the public interface of IReaperUIEmbedInterface, so the embed_message() REAPER thinks it's calling is the same one that IReaperUIEmbedInterface is implementing.
Also, since you're implementing add_ref and release yourself, you don't need to inherit from FObject. I think it will be helpful for you to make your class a standalone object that only inherits pure-virtual interface classes, with no state anywhere other than itself.
Thanks for posting the binary. REAPER is calling your embed_message(). If you're not receiving the call on your side, it may be because ReaperVST3EditController needs to implement the public interface of IReaperUIEmbedInterface, so the embed_message() REAPER thinks it's calling is the same one that IReaperUIEmbedInterface is implementing.
Also, since you're implementing add_ref and release yourself, you don't need to inherit from FObject. I think it will be helpful for you to make your class a standalone object that only inherits pure-virtual interface classes, with no state anywhere other than itself.
Ahh, thanks a ton Schwa, you're the best. Preserves my sanity a bit to know that it's mostly-working and it's something small in my implementation.
I'll strip the FObject out and try to rewrite the class again. I'm certain I can get this working in the next day or two now that I know it's on the right track!
It's just setting a pointer and then looking for + calling method, so what if?... What if I did something real dirty and just gave it the pointer to an inline struct that had those methods?...
LOL It crashes REAPER but the embed_message() gets logged (somehow AFTER the destructor is called, not sure how that's possible and is probably related to the crashing. But still!!)
PHP Code:
auto klazz = this->findInterface(targetIID);
if (klazz != nullptr) {
DBG("[JuceVST3Component::queryInterface] FOUND MATCH \n targetIID=" << fuidStr << "\n klazzIID=" << tuidToFUIDString(klazz->iid));
klazz->addRef();
auto embedResult = dynamic_cast<ReaperVST3EditController*>(klazz)->embed_message(0, 100, 100);
DBG("[JuceVST3Component::queryInterface] embedResult=" << embedResult);
It's crashing because you're returning an object on the stack. That would work fine (modulo other errors heh) if your ReaperUIEmbedImpl was on the heap. It's not "dirty" at all.
In fact the only reason you need any additional complication is that the ReaperUIEmbedImpl needs to have some connection to the actual plugin controller state. If you stored a pointer to the JUCE edit controller in ReaperUIEmbedImpl, you'd probably be most of the way to where you need to be.
If you stored a pointer to the JUCE edit controller in ReaperUIEmbedImpl, you'd probably be most of the way to where you need to be.
Absolutely, one of their devs (reuk) was the one who came up with this "installInterface" factory pattern kind of idea.
Then he said that the userland implementation of this function, on the JUCE AudioProcessor class, should take a "Location" variable -- which is the name/instance of the JUCE class holding the VST3 interface to install itself in.
PHP Code:
class VST3InterfaceContainer; // Has registerInterface(), findInterface() class JuceVST3EditController : IEditController, VST3InterfaceContainer // And other stuff class JuceVST3Component : IComponent, VST3InterfaceContainer // And other stuff class JuceAudioProcessor : IUnitInfo, VST3InterfaceContainer
// User-facing API function template <class T, class JuceVST3Interface> bool juce::AudioProcessor registerInterface(const TUID iid, T klazz, JuceVST3Interface location) { // Get a pointer to the JuceVST3EditController/JuceVST3Component/JuceAudioProcessor (IUnitInfo) auto locationClassPtrToInstance = JuceVST3Interface->getInstance(); // Instantiate the user class type with a handle to the pointer auto instance = new klazz(locationClassPtrToInstance); // I don't know if you can do this in C++ but you get the idea return JuceVST3Interface->vst3InterfaceMap .insert_or_assign(tuidToFUIDString(iid), dynamic_cast<FUnknown*>(klazz)) .second; }
// Pseudocode example use juce::AudioProcessor audioProcessor; audioProcessor->registerInterface(IReaperUIEmbedInterface::iid, ReaperVST3EditController, JuceVST3EditController)
So once I can sort out this implementation I think it should be ready for reuk to review and (cross our fingers) eventually get merged into JUCE =D
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
For anyone that might be following this, here's a status update if you're curious:
Daily dev work has still been going on, on my end
-
It's been a lot of communication with one of the JUCE devs about architecture. For reasons that are difficult to explain unless you know the innards of JUCE, these are the relative difficulties of implementing both VST3 interfaces/features of REAPER:
-
Access and use of the REAPER C++ SDK from the JUCE plugin userland code:
Trivial, a grand total of ~20-30 lines of code and non-invasive to the architecture.
-
Ability to create and dynamically register classes that implement VST3 interfaces into the JUCE VST3 wrapper classes:
Incredibly thorny to do in a way that is unintrusive, generic, and keeps plugin-type-specific implementation details out of bits of the code
-
The JUCE developer that has been helping me with this says he hopes to take a look at this during the coming workweek or so
-
At minimum, a worst-case solution would be for them to add REAPER-specific code to support this, but obviously this would be a bit of a shame and would miss out on the opportunities/abilities that implementing a generic solution would open up for plugins
Also I'm figuring things out (making things up?) as I go along wrt. C++ so there's that too lol
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
Welp, it's Sunday night, and it took most of the weekend but I've got a generic solution (mostly) working.
There's a problem though: which is that I am using a scope-global variable in the JUCE VST3 wrapper file to share a reference between two classes.
(Would really appreciate any advice on a better way to do this, if the order of construction is definitive and one will always come first)
PHP Code:
// At file global scope: AudioProcessor* __SCOPE_PLUGIN_INSTANCE = nullptr;
// This class is always constructed first, before JuceVST3EditController // so this only works as a dirty hack, due to that class JuceVST3Component { public: JuceVST3Component (Vst::IHostApplication* h) : pluginInstance (createPluginFilterOfType (AudioProcessor::wrapperType_VST3)), host (h) { // ... __SCOPE_PLUGIN_INSTANCE = pluginInstance; } }
// I haven't seen a way to get "AudioProcessor" into constructor args // here, so this is where we are a bit fucked as far as "clean code" class JuceVST3EditController { public: JuceVST3EditController (Vst::IHostApplication* host) { if (__SCOPE_PLUGIN_INSTANCE == nullptr) return;
const auto installLocation = AudioProcessor::VST3InterfaceInstallLocation::EditController; for (auto it : __SCOPE_PLUGIN_INSTANCE->getVST3ExtensionsForInterface(installLocation)) { auto [fuidVoidPtr, vst3Interface] = it; const auto fuidPtr = static_cast<FUID*>(fuidVoidPtr); this->registerInterface(*fuidPtr, vst3Interface); } } }
PHP Code:
// Since base AudioProcessor is generic, these are opaque types using FUID_ = void; using VST3Interface = void;
// Userland implementation in MyPluginAudioProcessor // getVST3ExtensionsForInterface() returns a: // std::vector<std::pair<FUID*, VST3Interface*>> // // The expected response is a list of tuples/pairs of TUID-to-Class-pointers // for replying to queryInterface() calls with. // // This method is called by each of the VST3 interface classes // in the wrapper. // // Each class gives a unique enum-value "location" you can use // to dictate what to install where std::vector<std::pair<FUID_*, VST3Interface*>> MyPluginAudioProcessor::getVST3ExtensionsForInterface(VST3InterfaceInstallLocation location) { switch (location) { case EditController: return std::vector<std::pair<FUID_*, VST3Interface*>>({ { const_cast<FUID*>(&IReaperUIEmbedInterface::iid), static_cast<VST3Interface*>(new ReaperVST3EditController(this)) } }); default: return std::vector<std::pair<FUID_*, VST3Interface*>>(); } }
With this, and the embed_message() paint code from earlier in the thread, what you end up with is:
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
Okay, a fellow named Ben Vining on the JUCE Discord helped me out with making the file global var thing less hacky using "static" + thread-local variables.