Passing complex data to (and from) a compiled C++ SOUL program

i’m wondering how i can pass in complex data to a SOUL program that’s been compiled to C++. i looked through the API and helper classes, and even tried to read the generated C++ files but wasn’t able to figure it out. seems like the API/helper classes only support audio, MIDI and parameter data to be passed through…?

by complex data i mean structs and data marked as external, e.g. for a patch like this:

processor Example [[main]]
{
  input event MyStruct myStructIn;
  output event MyOtherStruct myOtherStructOut;

  external soul::AudioSamples::Stereo audioSample;

  ...
}

so is it possible to pass in a C++ struct into myStructIn, listen to events from myOtherStructOut, and change the data in audioSample?

externals are read only, so no, you can’t generate a sample this way, and certainly not something visible externally.

We do this sort of thing in our tests (passing structs in, and getting structs out) using the generated C++, but we use a different API, a Performer instance, but this isn’t exposed to you through the soulpatch interface (it’s what that is implemented with). However, you can reach into the generated code to do this, but it’s ugly.

I’ll see if I can put an example together to show how that would work.

1 Like

thanks, that would be awesome!

I’ve generated c++ for the following rather artificial example:

namespace Events
{
    struct Input
    {
        float f1;
        float f2;
    }

    struct Output
    {
        float sum;
        float product;
    }
}

processor SumProduct [[ main ]]
{
    input stream float audioIn;
    output stream float audioOut;

    input event Events::Input eventIn;
    output event Events::Output eventOut;

    event eventIn (Events::Input i)
    {
        sum     = i.f1 + i.f2;
        product = i.f1 * i.f2;
    }

    float sum;
    float product;

    void run()
    {
        loop
        {
            eventOut << Events::Output (sum, product);

            loop (10)
            {
                audioOut << audioIn * product;
                advance();
            }
        }
    }
}

And for my test, i’m calling it like this:

TEST (CppGenerator, SumProduct)
{
    uint32_t sampleRate = 44100;
    uint32_t blockSize = 100;

    auto inputChannels  = soul::AllocatedChannelSet<soul::DiscreteChannelSet<float>> (1, blockSize);
    auto outputChannels = soul::AllocatedChannelSet<soul::DiscreteChannelSet<float>> (1, blockSize);

    for (uint32_t i = 0; i < blockSize; i++)
        inputChannels.channelSet.getSample (0, i) = float (i);

    SumProduct sumProduct;

    sumProduct.init (sampleRate);

    // Submit an event
    sumProduct._external_event_eventIn (sumProduct.state, { 0.1f, 0.2f } );

    // Render 100 samples
    auto rc = SumProduct::RenderContext { inputChannels.channelSet.channels, outputChannels.channelSet.channels, nullptr, blockSize, 1, 1, 0 };

    sumProduct.render (rc);

    // Check output data
    for (uint32_t i = 0; i < blockSize; i++)
        EXPECT_NEAR (outputChannels.channelSet.getSample (0, i), 0.02f * float(i), 0.0001f);

    // Retrieve output events
    SumProduct::FixedSizeArray<SumProduct::Events__Output, 1024> events;
    SumProduct::FixedSizeArray<int32_t, 1024>                    eventTimes;

    auto eventCount = sumProduct._getEventsFromOutput_eventOut(sumProduct.state, events, eventTimes);

    EXPECT_EQ (10, eventCount);

    for (int i = 0; i < eventCount; i++)
    {
        EXPECT_NEAR (0.3f,  events[i].m_sum,     0.0001f);
        EXPECT_NEAR (0.02f, events[i].m_product, 0.0001f);
        EXPECT_EQ (i * 10, eventTimes[i]);
    }
}

As I said, it’s not pretty, but you should be able to understand what we’re up to. Events in are processed immediately, so if you want to put an event in half way through a block, it’s your problem to split the block into pieces and call the eventIn handler at the appropriate sample number.

For output events, you retrieve them to a FixedSizedArray, and the return tells you how many were generated during that block. Now this can overflow the buffer, so if > 1024 were generated in the block, we’d only get the first 1024, but the return code would indicate the number that were generated (so we can tell event overflow). This test code doesn’t deal with that situation.

Hopefully this is clear. Sorry the interface is a bit crappy, but this is as I said an area we’re working to improve.

1 Like

thanks, got a proof of concept to burst out some sine waves based on events passed in as structs, yay! the interface is indeed a bit on the cryptic side but didn’t expect anything too shiny from a first beta release anyway :slight_smile: documentation is key!