Example of calling generated SOUL C++ code

Congrats on the release, looks great!

Apologies if there’s an obvious answer to this, but I’ve looked around and haven’t found one:
Is there an example somewhere of how to call the standalone code that is generated with soul generate -cpp?

I’m just trying to run some of the example patches, and while my code compiles fine, it doesn’t seem to produce or process any audio which makes me think I’m doing something wrong. It’s probably very elementary but I’m new to SOUL and don’t really know what I’m doing!

My wrapper code (for the OWL embedded platform) looks like this:

#include "DiodeClipper.hpp"
#define SOULPATCH Diode

class SoulPatch : public Patch {
private:
  SOULPATCH soulpatch;
  SOULPATCH::RenderContext<float> ctx;
  std::vector<SOULPATCH::Parameter> params;
public:
  SoulPatch():
    ctx({{}, {}, nullptr, (uint32_t)getBlockSize()}) {
    soulpatch.init(getSampleRate(), 0);
    params = soulpatch.createParameterList();
    for(int i=0; i<params.size(); ++i){
      registerParameter(PatchParameterId(PARAMETER_A+i), params[i].properties.name);
    }
  }
  void processAudio(AudioBuffer &buffer){
    for(int i=0; i<params.size(); ++i){
      float min = params[i].properties.minValue;
      float max = params[i].properties.maxValue;
      float value = getParameterValue(PatchParameterId(PARAMETER_A+i));      
      params[i].setValue(value * (max-min) + min);
    }
    for(int i=0; i<ctx.inputChannels.size(); ++i)
      ctx.inputChannels[i] = buffer.getSamples(i);
    for(int i=0; i<ctx.outputChannels.size(); ++i)
      ctx.outputChannels[i] = buffer.getSamples(i);
    soulpatch.render(ctx);
  }
};

In short what I’m doing is:

  • Diode constructor
  • Diode::init(SR, 0)
  • Diode::createParameterList() and Parameter::setValue()
  • Diode::RenderContext constructor (setting numFrames to blocksize)
  • set Diode::RenderContext input and output channels
  • Diode::render()

Am I missing a step?

DiodeClipper.hpp has been generated with

soul generate examples/patches/DiodeClipper/DiodeClipper.soul --cpp > DiodeClipper.hpp 

Any ideas?

Also, is it generally okay to pass the same buffer in the context as input and output data (in-place processing) or do I always need to make a separate output buffer to avoid clobbering the input?

The only thing that jumps out at me is that the soul class doesn’t expect the same pointers to be used for both input and output channels, so that could be causing problems. If your calling code is supplying a single buffer to act as both input and output, you’d need to wrangle it into separate channel copies like we do in the audio plugin wrapper classes.

A tip here is to run the generate --juce option on your patch, and have a look at the glue code that it generates, since it’ll be correctly calling into the same C++ class that you’re trying to use.

BTW I’d always recommend using a local context object each time you call render, not a member variable. As well as making it easier to see what’s going on, it’ll probably also perform better (though you might find that counter-intuitive)

1 Like

Thanks, got that working now, I think the numFrames initialisation was a problem.
And yeah I noticed that RenderContext is passed by value, which is a bit surprising.

Another thing! The generated code includes this:

#include <cassert>

#define SOUL_CPP_ASSERT(x) assert (x)

#ifndef SOUL_CPP_ASSERT
 #define SOUL_CPP_ASSERT(x)
#endif

C assertions are not always wanted when compiling for embedded platforms, they tend to want to call abort. Is there a way to suppress this output?

Changing it to this would be fine:

#ifndef SOUL_CPP_ASSERT
 #define SOUL_CPP_ASSERT(x) assert (x)
#endif

All in all I have to say: nice work! A near-minimum of dependencies in the generated code. And even if you use std::vector and std::array it is still possible to compile without exceptions and full -lstdc++ linkage.

Thanks, glad you got it going!

Good point about the assert definition - it’s clearly a mistake that we’re generating the unguarded version, and I’ll get rid of that right away, ta for reporting!

This is working now with the v1.0.75 release, thanks!

btw I think there’s a typo in one of your examples:

examples/patches/TX/ElectroPiano/ElectroPiano.soul:15
graph ElectoPiano  [[ main ]]

should be

graph ElectroPiano  [[ main ]]

Though there is another graph inside the patch called ElectroPiano so maybe it is intentional, but it means the generated class ends up with an odd name.

And a feature request:
Could you add an option to CLI generate to specify the output class name? This would make it much easier to generate code with a reusable wrapper, i.e. one that is agnostic to what the main graph is called internally.

Thanks, looks like we should tidy that up!

Yeah, good idea about specifying the name, that shouldn’t be a problem to add.

1 Like

Thanks for pointing out that example - there’s usually a voice and a graph class, and the names look to have got mangled at some point. So that’s not a typo, those need to be named differently