Some SOUL questions

Hi Jules,

I am starting to dive into SOUL now (bit late to the party I guess :). and got a few questions after poking around with the example project and the docs.

  1. The demo project expects the soul .dll to be either at the desktop (lol), the user’s home directory (double lol) or the app’s parent directory (better, but not universal). I know we’re far from a production environment but shouldn’t there be already a sensitive default where people can expect the soul binary to be like any other dynamic lib (C:\Windows\System32 and /usr/local/lib for example)? Ideally once this platform has thrived you can just expect it to be on an audio computer like eg. runtime32.dll but if you don’t specify this ideal location soon, everybody and is dog will install it to arbitrary locations and the end user will end up with multiple versions of this dynamic lib file.
  2. I skimmed the Syntax Guide but I didn’t see a mechanism for including other soul files ("#include “MyProcessorCollection.soul”). Is this on the roadmap?
  3. Is there any mechanism to tell a SOUL Processor to execute the inner loop in a fixed block size (or at least fire a callback every 32 samples to eg. update filter coefficients)? I know you’ve stated that you’re trying to follow the paradigm that the algorithm shouldn’t care about block processing, but there are some situations (like above) where you need to have control over this aspect.

Hi Chris

Re: the DLL
a) it’s really up to the host app to decide where it looks for this DLL. In Waveform, we install it into a suitable sub-folder in a nicely-located waveform resources folder containing other bits and pieces, and always load it from there. Obviously the demo project is going to be a bit more flaky, and when you’re tinkering with this stuff, it’s helpful to be able to stick a link on your desktop or somewhere if you’re just testing things out, but clearly that’s not what a production app would do!
b) I really hope at some point in the future to be able to ditch the DLL and replace it with either a static lib or just some C++ code. I really hate DLLs, but it’s just the simplest thing to use at this stage.

We’re planning some kind of import directive (we’ve reserved ‘import’ as a keyword for it) but that’s going to be mainly useful when people are pulling in lots of library code and importing from URLs, etc. In a self-contained soul patch you don’t really need it yet, as you can just tell it to build a list of .soul files, and the linker will allow all of them to reference symbols in the others without needing to explicitly import them.

We’re deliberately avoiding fixed size blocks, but it’s really easy to write code that does periodic updates like that by having a couple of nested loops, e.g. the phaser example does exactly what you’re talking about:

…and also in here:

Hi Jules,

thanks for the fast reply. This approach is only possible within a processor’s run function, however it might be required that the output of a graph’s end point is being send to the output regularly every 32 samples and as far as I can see the graph concept is purely descriptive.

I am asking because I have built a node based DSP system and I am currently evaluating the possiblity of it spitting out SOUL code (it currently generates metatemplated C++).

Is it possible to use a graph within a processor to give it some kind of logic?


// Sorry about the pseudo code mess...
processor GraphWrapper 
{
    output float myOutput;

    graph Internal
    {
         // setup something here, inputs, outputs, etc.

    } internalGraph;

    void updateOn32()
    {
        // do something here, eg. send some internal endpoint to an output
        internalGraph.out << myOutput;
    }

    void run()
    {
        loop
        {
            updateOn32();

            loop(32)
            {
                internalGraph.advance(); // ???
                advance();
            }
        }
    }
}

And yes DLL’s are the worst, but I hope there will be a better solution than compiling LLVM everytime you build a plugin :slight_smile:

Oh, and is there a possibility to define conversion functions for event connections inside a graph? Let’s say I have a envelope follower processor that sends his calculated value to different targets via an event output, but I want it to control a gain module from 0 to -36dB with -12dB as midpoint and an oscillator from 80Hz to 120Hz.

Oh right, if you’re talking about the host that’s running the soul code - i.e. the “performer” - then yes, its external native API will be a block-based interface where you can request any number of samples to be rendered.

You can obviously create a processor that converts any input event to any kind of output event and put that in between the two nodes. And one of our to-do-list items is to add some syntax to make it possible to just add a pure functional expression to the connection declaration itself, so that it’ll effectively create a hidden converter node to do the job without you needing to manually write one.

1 Like

Ah sure, that’s possible. Is there an ETA for when you add function converters to connections? This will highly reduce the boilerplate of my code generator.

Controlling the block size from the caller side isn’t enough for my use case: I have container nodes which process their children top down with different functionality and most of them split up the incoming buffers differently (either into fixed sub-blocks, between MIDI events all the way down to 1 sample blocks). In order to mirror this in SOUL I would need to add some kind of processing logic to the graph. You mentioned somewhere that a graph can be considered as a subclass of a processor, so I am actually asking to make its run() method virtual :slight_smile:

I’m not 100% sure I understand what you’re asking for, but the point about what we do is that our graph gets turned into a single function that renders all the nodes one sample at a time, and then the whole thing is called with a top-level block size, so it can vectorise across node boundaries… It’d be impossible to have any kind of custom run() function for a graph, because it’s just created from the contents of all the node run() functions.

For my use case it’s about controlling how to do stuff outside the most inner loop on a top-level, but while typing I realized that most of this involves when to send modulation signals to targets which can be defined inside the processor (and just pass the events down to the graph).

However there are plenty of applications where the pure declarative nature of the graph might not be enough: how are dynamic signal flows handled - eg. a button that enables the processing of a (sub-)graph?


void setCompressorEnabled(bool shouldBeOn)
{
    compressorEnabled = shouldBeOn;
}

void processBlock(AudioBuffer& buffer, ...)
{
    if(compressorEnabled)
    {
        for(int i= 0; i < buffer.getNumSamples(); i++)
        {
            buffer.setSample(0, i, compressor.process(buffer.getSample(0, i)));
        }
    }
    
    // do the rest
}

How is this handled in SOUL?

There are lots of ways to do that.

e.g. you could write a simple mute node like

processor Mute
{
    input stream float in;
    output stream float out;
    input stream bool isMuted;

    void run()
    {
        loop
        {
            if (! isMuted)
                out << in;

            advance();
        }
    }
}

and then connect your compressor into it, so it can be turned off by switching the flag.

Or you could avoid the extra node by just adding an isMuted input to the compressor itself, so it’d do nothing if muted.

And the optimizer is smart enough to move that condition out of the most inner loop?

This is probably depending on this optimization pass, but it needs to be guaranteed that the isMuted flag will not change inside the loop, which is not possible as long as it’s an end point connectable to the outside world?

Well, it basically turns into an ‘if’ statement and the optimiser will deal with it like any other branch optimisation.

Whether or not the input can change every sample depends on what kind of stream you use. If it’s a sparse stream, or a value stream, or if you use an event instead to flip a flag, then it will stay constant for chunks.

Or if you’re really paranoid you could write

if (isMuted)
{
    loop (64) { renderWithCompressor(); }
}
else
{
    loop (64) { renderWithoutCompressor(); }
}
1 Like

Alright then. I think I have enough for now to get a first prototype for my code generator, but I’m sure there will be more questions while I’m at it :slight_smile:

BTW, can structs have member functions? I’m asking because one of the first things I’ll do is to clone the NormalisableRange class from JUCE but using this C-style approach feels slightly dated:

namespace NormalisableRange
{
struct Data
{
    float64 min, max, interval, skew;
}

float64 convertFrom0To1(const Data& d)
{
     // copy from the JUCE class...
}

float64 convertTo0To1(const Data& d)
{
    // ...
}
}

// better
struct NormalisableRange
{
    float64 min, max, interval, skew;
    float64 convertTo0To1() const 
    {
    }
}

We don’t call them “member” functions but we do have universal function call syntax:

1 Like

Nice, exactly what I was looking for.

Next question: :slight_smile:

Is there any plan for an API to hook up C++ code to SOUL or make the soul runtime extendable by the host that is using it? I think I wrapped my head around the broad architecture and it will work just fine, but there are a few special features (eg. multithreaded convolution or sample streaming) that aren’t part of the SOUL feature set yet and a convenient way of taking the burden from you having to implement every DSP algorithm would be a API that allows us early birds to use existing C++ code. In the long run it might get ported / replaced by standard implementations of the official SOUL release, but I can imagine you have better things to do now than adding these rather specific tools.

Maybe sort of… We think there’s going to be a place for the soul runtime to support graphs that contain some “external” processors, i.e. hooks for native ones, mainly so that you can build a soul graph which incorporates some VSTs. But I guess that would probably also cover what you’re asking for too.

Yes that would fit the description. Any idea about the ETA?

We have no idea about any ETAs! Big to-do-list, just two of us beavering away on it all right now!

Alright I‘ll start with the basics then. I am curious though how you plan to make a generic API that can be fed with streams of any type though :slight_smile: