Handling large quantities of external sample data sources

Hello, I’m writing SOUL code generator / graph parser for a project and I need to be able to select different external source for different instances of the same processor. Does anyone know if this is even possible at this stage?

The issue I found is that you can only specify external variables per namespace / processor type. In other words, all instances of this processor share the same external variables.

E.g. all Node_WavePlayers are going to play “thunder” sounds, and if I need to change it to something else, it’s going to be changed for all instances of the processor:

image

The workaround I found in MinimumViablePiano is to add all of the possible external variables that all of the Node_WavePlayers within this SOUL patch need and then select which one is supposed to play on each instance individually (haven’t tried it yet, not sure how it would work).

image

…In this case, if a WavePlayer needs to select a random sound from a group at runtime, the externals list gets increasingly more entangled.

Other workaround that I’ve thought of is to wrap each external file into a simple processor reader and append a tag to processor type for each instance, e.g. Node_Wave_Fire, Node_Wave_Thunder, etc. But that seemed like an unreasonable duplication of code for the compiler. And the bigger problem with this approach is it would make it impossible for multiple WavePlayers to read the same external sources from different positions.

If it would be possible to hold external variables in the top graph and pass them in to the instances of WavePlayer, maybe that would solve this. Although I’m not sure if that wouldn’t produce more issues.

TLDR: Is the MinimumViablePiano’s approach my best bet at this point?

P.S. ExternalDataProvider also doesn’t solve the issue, as it provides data for the whole patch, not for individual instances of a processor within the patch. Technically I could compile multiple instances of the same patch each with its own manifest file, which would contain only required external variables and then route them together externally in the host, but that doesn’t make it simpler, or more elegant and requires even more recompiling of the same thing for just a variable change.

P.P.S. It’s possible I am completely overthinking this :smile:

A different scenario touching this issue - if you have a deeply sampled piano, with a lot of velocity layers and a lot of round robins for each of them, you could have hundreds and even thousands of samples needed to be delivered in a timely manner. I wonder how would logistics look like for such case. Having a gigantic list of external soul::audio_samples sounds ridiculous. You’d probably need an instance of a processor for each key, but then you’d run into the same issue I’m having in not being able to set different external variables for different instances of the same processor (in this case PianoKey).

Or maybe such project is outside of the scope of SOUL?

Update

I figured that I can use namespace to hold “global” variables accessible by all processors in the patch.
For each individual external sample data source or an array of data sources I have in the node graph I’m adding external variable to the GraphInputs namespace.

image

But here’s the question that really bother me: when I output an element from that “global” array:

image

image

…is it making a copy of the data, or is it passing out the pointer to the same shared read-only external data source?

Copying data each frame is probably not ideal, especially if those are quite large files :smile:

Solution

After some digging I've found a solution - audio data streaming. It's not pretty, but it works.

The idea is to request audio data from the host when it is needed using output event and to send data into SOUL player using input event. You need “swap” buffers so that the host has time to provide SOUL player with new data while the player is consuming previous data.

You can’t have your DataIn event be of type float array, or vector of arrays, it won’t work.

image

…it needs to be encapsulated in a custom structure:

image

image

Also you can’t use soul::audio_samples::Stereo, because the size of the frames member vector of array is not constant, the compiler will throw errors at you:

image
image

To pass sample data into SOUL player via input event you need to wrap it into choc::value::ValueView object, which you can do with choc::value::create2DArrayView() if you don’t want to include soul_core.h, or with any of the conversion functions in soul_AudioDataGeneration.h.

Limitations

  • For some reason I could not pass buffer larger than a certain amount, 6000 samples or more. It was either glitching out, or not playing at all.
  • You need to figure out an optimal way to initialize first buffer before starting playback (or if you switch read position often). It’s going to be different depending on your needs. But the important point is getting next buffer is going to be delayed by at least one audio processing block of your host. It needs to receive outgoing event and then send input event on the next block.

I hope this information can prevent someone from wasting time figuring this stuff out :slightly_smiling_face:
If you have any comments or suggestions, please feel free to post them here.

1 Like