Hi, I’m writing this on behalf of a student. His graduation project seeks to implement a modular approach to control of a granular synthesis unit (targeting VST as the executable format).
His initial thought was to implement each module as an AudioProcessor, and wrap up the whole bundle in an AudioProcessorGraph (to allow AudioProcessorGraph to handle order of execution). For this purpose the AudioProcessors would need to have independent bus configurations, distinct from the bus configuration requested by the host. This is where he’s getting stuck.
He asked Gemini for advice, and it basically said, 1/ don’t try to mess around with the bus configuration, since this is meant primarily for validating against the host’s bus structure; 2/ either use a lock-free queue, or write into and read directly from audio buffers his code should explicitly manage.
I suppose for LFOs that it’s more common practice to build the LFO directly into the main AudioProcessor, but the student’s intent here is to allow the plugin user to add and interconnect modulation signals dynamically.
We’ve both been searching tutorials, but haven’t found anything for this type of question.
I’m good in SuperCollider but I’m not a C++ dev, so, on my own I haven’t able to make a concrete recommendation to him. (It’s a music technology major but few of the students pursue any sort of programming.)
Any recommendations for handling this type of requirement?
TIA,
James
To counter the LLM here, I don’t see the problem with having whatever kind of bus layout in your internal graph as long the enclosing plugin processor is playing nicely with the host.
The host sets your bus layout before processing, then gives you an audio buffer appropriate to the layout in processBlock. You then write samples into the correct output channels in the buffer. Whatever you need to do internally to calculate what goes there is up to you.
Like AdamVenn explained, it should be quite doable. I am not the biggest fan of the idea of using AudioProcessorGraph for the internal routing in a plugin, though. The AudioProcessorGraph has some downsides :
- Each processing node needs to be an AudioProcessor which is not a lightweight object. The CPU and memory overhead could get substantial when a non-trivial number of them are used, which could be an unnecessary cost when the processing nodes would be something more simple like LFOs.
- The AudioProcessorGraph does not support feedback connections which would limit the creative possibilities in a modular style environment.
- The AudioProcessorGraph already once had slowness problems dealing with more than a trivial number of nodes. The Juce team did work on a fix for that but I don’t know if anyone has actually benchmarked the current behavior with the number of nodes and connections involved. I’d suggest doing some extreme and perhaps randomized benchmarks/tests to see if it can actually handle lots of nodes and connections now.
- The AudioProcessorGraph does not do multithreaded rendering. Perhaps not a huge issue when the processing nodes are of lower complexity like LFOs, though.
That said, I do understand it’s not an attractive proposition to do a full custom signal processing graph especially if one is not already a seasoned programmer. The AudioProcessorGraph may end up working acceptably enough.
1 Like
Sure – I take anything from an LLM with a grain of salt.
I’m not completely clear on the exact problem my student was having with the internal bus approach. I can try to get some more information and come back with that.
Fair concerns about performance. I am equipped to help the student with topological sorting for node order, but… yeah… it’s a big job, and there might not be enough time before v0.1.
hjh
OK, some more specifics.
His GranularProcessor declares (note “Auxiliary_Input”):
GranulatorProcessor::GranulatorProcessor()
: ProcessorBase
(
BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withInput("Auxiliary_Input", juce::AudioChannelSet::discreteChannels (4), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)
)
{
}
… and validates…
bool GranulatorProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.inputBuses.size() < 2 || layouts.outputBuses.size() < 1)
return false;
return layouts.getMainInputChannelSet() == juce::AudioChannelSet::stereo()
&& layouts.getChannelSet (true, 1) == juce::AudioChannelSet::discreteChannels (4)
&& layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo();
}
But then he says that in processBlock(), “getBusCount(true) returns 1, getTotalNumInputChannels() returns 2,” seemingly ignoring the 4-channel aux input.
(One thing, btw, that isn’t quite clear to me is, what exactly is layouts in isBusesLayoutSupported()? Always from the host? if it’s from the host, I would expect the given test to fail, because the host would not be requesting the extra 4 buses. But it’s continuing on to processBlock() where the bus layout seems to be different. Perhaps the “sanity checks” mentioned in the API reference are passing in a manufactured 4-channel bus, but it’s disabled, and this code isn’t checking the disabled status? And then `getBusCount()` ignores disabled buses?)
Also, the LFO declares mono (both “Input” and “Output” are juce::AudioChannelSet::mono()) but then the buffer for this processor is stereo. ??
Thanks again!
hjh
I’m not an expert with multi-bus layouts but the juce::AudioChannelSet::discreteChannels(4) looks a little suspicious to me. It might be better to simply check the auxilary buffer has 4 channels reguardless of whether they’re discrete or not.
Maybe they could try allowing any channel configurations for the time-being, i.e. remove the BusesProperties() argument to the base constructor, and have isBusesLayoutSupported() always return true. Then they could set up the correct configuration in their DAW and use getBusesLayout() to see how that desired layout is being reported by JUCE?
Since you’re dealing with AudioProcessors within AudioProcessors, do make sure you’re clear when you are working with the host-facing ‘outer’ processor and when you are working with the inner processors which form your internal graph.
Since AudioProcessor is used for plugins, file processing and audio graphs, a lot of these functions are context-dependent.
Assuming the processor above is your outer processor, I agree with Jimmi that there’s almost certainly no need to restrict the speaker layout you accept in isBusesLayoutSupported, you can just check the number of channels it has. A four channel bus could be ‘quad’, etc. and it would be refused.
The host gives a layout to the plugin to test if it is supported. This will be called many times as the host figures out the right settings for the track in the DAW and the plugin. I recently added a function in a command line host to give you a list of all the layouts your plugin supports. Look for Plugalyzer on Github if that sounds useful. You can also put DBG statements in isBusesLayoutSupported to show the interactions between the plugin and the host.