CLAP: Convenience classes and "AGain"-like examples

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

In the last two weeks, I have finally gotten around to learn the CLAP-API - or at least the most important parts of it. To get started, I was trying to find example code similar to the well known "AGain" example from the VST2-SDK. But the examples that I could find were all either too low level (written against the core C-API, featuring a lot of boilerplate code) or missing essential features (like handling state recall) or too complicated (involving GUI stuff with various frameworks) or written in the (for me) wrong language (Rust) or a combination of the above. A simple "AGain"-like example based on this convenience wrapper class:

https://github.com/free-audio/clap-help ... /plugin.hh

that does all the usual stuff like handling parameters and state recall but is not burdened with the additional layer of complexity of handling a GUI framework did not yet seem to exist (Or does it? If so - where?). I also wanted to understand how the C++ wrapper class around the core C-API actually works - how is it all glued together? To figure this out, I found the wrapper class a bit big. It implements loads of features that I wasn't interested in (at least for the time being - as a beginner) and buried a lot of the functionality in the error handling. So I sat down and stripped away all the stuff that I do not (yet) need and simplified the error handling to use simple assert-like statements. Then I created subclasses of my simplified wrapper class and in these subclasses I added stuff that I do need but was not yet present in the wrapper - such as parameter and state handling.

The result of all of that is a C++ class from which actual plugins can conveniently be derived and whose code can look quite similar to Steinberg's "AGain" example as demonstrated in this pair of .h/.cpp files:

https://github.com/RobinSchmidt/RobsCla ... oPlugins.h
https://github.com/RobinSchmidt/RobsCla ... lugins.cpp

They actually contain two plugins - a gain plugin with "Gain" and "Pan" parameters and a waveshaper plugin with "Shape", "Drive", "DC" and "Gain" parameters. To build the actual plugin library, one also needs the file:

https://github.com/RobinSchmidt/RobsCla ... yPoint.cpp

for the entry point of the dll/so/... There is a Visual Studio solution with a project that builds this .clap plugin library:

https://github.com/RobinSchmidt/RobsCla ... pStuff.sln

So far, I have compiled this only on Windows with Visual Studio. At some point, I want to make this code portable to Linux and MacOS, too - but it's not there yet. The resulting .dll (actually .clap - but it's really just a renamed .dll) passes all applicable tests in the clap-validator.exe application ("35 tests run, 27 passed, 0 failed, 8 skipped, 0 warnings") and seems to work as expected in Bitwig. This project demonstrates also how to put more than one plugin into a single library and the "Shape" parameter in the waveshaper demonstrates how one could handle choice/enum parameters. All of the state-recall handling and much of the parameter handling has been factored out into the reusable baseclass which in this case is ClapPluginStereo32Bit. It is actually itself a subclass of the more basic and more general baseclass ClapPluginWithParams. These classes are implemented here:

https://github.com/RobinSchmidt/RobsCla ... nClasses.h

I do not yet recommend to derive directly from ClapPluginWithParams, though. Some more work needs to be done on it before (see documentation). But if you have the (rather common) case of a stereo I/O plugin that works with 32 bit float data, it should be very convenient to derive from the class ClapPluginStereo32Bit and override a couple of virtual member functions. You should override at least two: processBlockStereo() for the actual audio processing and parameterChanged() to handle parameter changes. You may optionally also override some more functions to get customized text formatting for your parameters (see the examples).

Disclaimer:
I am not saying that this code any good or production-ready or publication-ready or anything like that. It's just something that I came up with and that I want to use personally as a basis for some GUI-less plugins. It's mostly meant to be educational to show how things *could* be done - not necessarily how they *should* be done in any sort of "best practices" sense. There is definitely some more work that needs to be done. So, use it at your own risk.

Any comments, feedback, bug-reports, suggestions for improvements, etc. are welcome
Last edited by Music Engineer on Wed Apr 17, 2024 8:13 am, edited 2 times in total.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Thanks for this. I'm going to keep an eye on it

Post

Oh - I just discovered some embarrassing bug...working on it...

Edit: OK - the bug seems to be fixed. There is also a new plugin called ToneGenerator that demonstrates handling of midi events. It just plays a sine with a frequency of the most recent note that is being held - or silence when no note is held. The library has now 3 plugins: StereoGainDemo, WaveShaperDemo, ToneGeneratorDemo. I really like the fact that I can put multiple plugins into a single dll. That makes it very space efficient. All the common code can be shared between the plugins.

clap-validator reports: 49 tests run, 40 passed, 0 failed, 9 skipped, 0 warnings

and it also passes all of my own unit tests from here:

https://github.com/RobinSchmidt/RobsCla ... nTests.cpp

...what a day - working from 9 in the morning to 1 in the night with very few breaks. I guess, it's called "the flow". To find this bug, I had to implement a lot of mocking stuff to set up unit tests. That stuff will certainly be useful later on. Maybe it can someday even help to write an actual host. :scared:
Last edited by Music Engineer on Tue Apr 16, 2024 12:04 am, edited 6 times in total.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Have you looked at my CRTP exercise that wraps the CLAP API into C++ methods without virtual dispatch (it does static dispatch on a member template directly from the API entry points, skipping one level of indirection) and automatically generates the relevant structures for extensions by virtue of id-check template instantiation.. and even automatically deals with multi-plugin factories just by virtue of having declared the plugins?

https://github.com/signaldust/clap-glue ... lap-glue.h

Post

mystran wrote: Mon Apr 15, 2024 5:59 pm Have you looked at my CRTP exercise that wraps the CLAP API into C++ methods without virtual dispatch (it does static dispatch on a member template directly from the API entry points, skipping one level of indirection) and automatically generates the relevant structures for extensions by virtue of id-check template instantiation.. and even automatically deals with multi-plugin factories just by virtue of having declared the plugins?

https://github.com/signaldust/clap-glue ... lap-glue.h
Oh - very cool. No, that thing wasn't really on my radar yet - although I'm actually watching the repo on GitHub. Zero-overhead is of course also a very desirable feature. My current focus is to minimize the boilerplate in the plugin code while keeping the overhead in check but not necessarily zero. How much boilerplate will be needed with your approach? Do you have some "AGain"-style example somewhere that demonstrates the usage?
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Music Engineer wrote: Mon Apr 15, 2024 11:49 pmHow much boilerplate will be needed with your approach? Do you have some "AGain"-style example somewhere that demonstrates the usage?
The CRTP magic itself is very thin wrapper around the CLAP API.. like basically all it does is generate the dispatch tables automatically and turns everything into regular method calls. The first parameter (clap_plugin *) becomes the this pointer, but otherwise it's essentially the same.

I think that alone makes the API quite bearable, but it was really my intention to then abstract some of the stuff for my toolkit with a separate wrapper. The thing though is that where the CRTP glue is completely agnostic to any toolkits or frameworks (literally all it's doing is turning the API into C++ methods), once we start to abstract the API we need to start making more specific decisions about how to handle things internally .. like even the mechanisms for passing events between GUI and DSP requires decisions about what sort of event queue you want to use, the GUI factory methods probably want to open a toolkit window and so on.

So.. I split the CRTP magic into just the CRTP magic and kept it thin. It doesn't make any decisions about the plugin, it just translates the API to a slightly less "boilerplaty" form.

Post

Ah - OK - so your CRTP wrappers do not really reduce boilerplate that has to written on the client code side (aside from a more concise syntax maybe) but just translates it. It's a sort of "orthogonal" purpose. It would be nice to see some example plugin code for how the actual plugin code could look like when using this wrapper in its pure form (I'm not that familiar with the CRTP pattern, to be honest.).
mystran wrote: Tue Apr 16, 2024 8:23 amThe thing though is that where the CRTP glue is completely agnostic to any toolkits or frameworks (literally all it's doing is turning the API into C++ methods), once we start to abstract the API we need to start making more specific decisions about how to handle things internally
Yes - absolutely. With every such decision one makes, one reduces the flexibility and at the same time one reduces the burden for the plugin implementor. The decision making process itself is actually a big part of that burden*. Also, when more specific decisions for handling certain things are made, one has the opportunity to reduce the amount of boilerplate* on the client code side by factoring out the common functionality that is needed to implement the particular decision. I think, I should set up a text document to explain the decisions I made and why. What I liked about Steinberg's "AGain" example was its simplicity. All the decisions were already made and one just had to subclass AudioEffectX and override a couple of methods and got a working (GUI-less) plugin with all the expected regular baseline plugin functionality (audio I/O, parameters and state recall) and it all "just works". The entry barrier for writing your first own plugin that is actually viable was very low. I want something similar for CLAP. Plugins without any parameters and without state-recall (like in the simplemost clap examples) are not really viable for publishing. They don't do enough. On the other hand, plugins with GUI frameworks are already too specific/opinionated and complicated to serve as a staring point (in my humble opinion). They already do too much that distracts the beginner from the core plugin functionality. "AGain"-like examples are the sweet spot for a starting point balancing simplicity and viability. The important features (audio, midi, params, state) work out of the box and there is no additional baggage (GUI, threads) to worry about.


* One has to anticipate the consequences of all the possible routes to follow and weigh the pros and cons of each. That can take quite a lot of time that somehow can feel quite unproductive because no code is being written. But just starting to write code to implement the seemingly most straightforward solution only to discover later that one didn't anticipate one of the consequences and then having to redo it is not really the way to go.

* Boilerplate is really a burdensome thing - not necessarily when initially writing it but later when one has already dozens or hundreds of plugins and then some things need to be changed in *all* of them because one decides to do some thing differently. Less boilerplate == easier maintenance. Maybe AI can help with that someday. But I wouldn't be sure if I would trust the result when it batch-processes a big pile of text which no one really wants to read. But I wouldn't be sure if I would trust myself or any other human with that either. :-| Dunno. :shrug: But it's better if we can avoid having to deal with such a big pile of text in the first place. The DRY principle (Don't repeat yourself) is important.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

I wonder, if I may have fallen into some potential pitfall. The loop over the samples in the block in my gain example looks like this:

https://github.com/RobinSchmidt/RobsCla ... ns.cpp#L78

Code: Select all

for(uint32_t n = 0; n < numFrames; ++n)
{
  outL[n] = ampL * inL[n];
  outR[n] = ampR * inR[n];
}
Pretty straightforward stuff - but could that be a bug when I announce to the host that my plugin can do in place processing (which I do)? For in place processing, this code would work only iff outL == inL and outR == inR which is what I would assume to happen in this case. However, it is not spelled out in CLAP-API, that the host must behave this way (Or is it? I didn't see anything like that at least). Couldn't some weird host perhaps pass some in-place processing buffers where the channels are swapped, i.e. outL == inR and outR == inL? If that were to happen, then the first line

outL[n] = ampL * inL[n];

would overwrite inR[n], implying that in the second line

outR[n] = ampR * inR[n];

where I read from inR[n] again, it would not have the same value anymore and my plugin would produce the wrong result. I do assume here, that when the host opts for in-place processing, that we would have the situation outL == inL and outR == inR. But I wonder if that is a safe assumption. I suppose, this might be the reason for the 3 step "fetch -> process -> store" code in the example code here?


https://github.com/free-audio/clap/blob ... ate.c#L262

Code: Select all

/* process every samples until the next event */
for (; i < next_ev_frame; ++i) {
   // fetch input samples
   const float in_l = process->audio_inputs[0].data32[0][i];
   const float in_r = process->audio_inputs[0].data32[1][i];

   /* TODO: process samples, here we simply swap left and right channels */
   const float out_l = in_r;
   const float out_r = in_l;

   // store output samples
   process->audio_outputs[0].data32[0][i] = out_l;
   process->audio_outputs[0].data32[1][i] = out_r;
}
Dos it mean, that I always should write such "fetch -> process -> store" boilerplate in my DSP code to be robust against in place process buffers with permuted channels?
Last edited by Music Engineer on Thu Apr 18, 2024 4:37 pm, edited 1 time in total.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Music Engineer wrote: Thu Apr 18, 2024 3:54 pm Dos it mean, that I always should write such "fetch -> process -> store" boilerplate in my DSP code to be robust against shuffled/permuted in place buffers?
Nope. You can write a wrapper that sanity checks that the buffer layout is "good enough" and transparently performs a copy to temporary if there's something cheesy going on.

Btw.. one can also do "in-place" processing with separate input/output buffers: the first thing you do in process() is copy the input to the output buffer.. and then you can modify the output buffer as if it was serving as both buffers at once.

Then if you want to be really clever you can write a general shuffling algorithm that tries to minimize the number of copies when the inputs and outputs overlap arbitrarily. Without in-place swap tricks, this takes one temporary buffer worst-case to break cycles... but .. I think it might be a bit overkill.

Post

mystran wrote: Thu Apr 18, 2024 4:35 pm
Nope. You can write a wrapper that sanity checks that the buffer layout is "good enough" and transparently performs a copy to temporary if there's something cheesy going on.
Hmm...yeah...I could do that. But that would mean that I would always have to pre-allocate such temp-buffers in the activation, even when they are not needed, and then in every call to process, do some additional checks. That's a non-negligible cost so I'm not so sure, if that's really preferable over just telling the plugin implementor (i.e. myself) to not make any assumptions about the buffer layout. I mean, the "fetch -> process -> store" strategy doesn't always need to be so verbose. In the case of the gain, I could also get away with something like that (if I'm not mistaken):

Code: Select all

for(uint32_t n = 0; n < numFrames; ++n)
{
  float tmpL = ampL * inL[n];
  outR[n]    = ampR * inR[n];
  outL[n]    = tmpL;
}
It's a bit uglier than the original version but still bearable....hmmm.... :neutral: ...hhmmmm ... dunno :?
Btw.. one can also do "in-place" processing with separate input/output buffers: the first thing you do in process() is copy the input to the output buffer.. and then you can modify the output buffer as if it was serving as both buffers at once.
Yes - I've actually done such things in another context.
Then if you want to be really clever you can write a general shuffling algorithm that tries to minimize the number of copies when the inputs and outputs overlap arbitrarily. Without in-place swap tricks, this takes one temporary buffer worst-case to break cycles... but .. I think it might be a bit overkill.
Indeed. I aggree.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post Reply

Return to “DSP and Plugin Development”