Block-Based Modulators?

Hi all,

[Am quite new to all of this] Have been trying block based processing for some things such as oscillators, delays and filters for the supposed CPU benefits. For example:

processBlockofSine(frequency, buffer) etc etc
However, I’m confused as to how you do things which require one thing modulating another without having awkward passing of buffers through other objects.

For example, two operator FM.

Do I have to render a block of the modulator to some buffer, then pass this rendered buffer to the carrier object for when it renders it’s block of audio? If so, is there a more elegant way than to have to pass it through a function like:
Carrier.getModulatorBuffer(ModOscBuffer)

Carrier.processBlock()

Or, is it instead better to have this two op FM as a single class and do it all inside that and abstract that away?

Block based is the normal way of going about things so you’re on the right track.

Either way you’ve mentioned is viable. You can have an oscillator that generates a wave (sine) and then have flexible routing to another oscillator that outputs/fills a modulated buffer. Or, if you don’t need the routing flexibility you can just blackbox it into one class that combines waves into a single output buffer.

No right or wrong way, it’s up to you.

i think it’s good to render a modulator into a buffer before passing it on to the destinations, because each destination might work with the buffer in a different way

What is the advantage of using “a single class”?
And what do you mean by buffer passing?

Just wanna add that from my observations, it’s a good idea to decide on a case by case basis whether to render buffers or to do more ops per sample.
Often, the bottlenecks are memory access, cache, call overhead, and branch prediction misses.
With out of order execution and all the kinds of magic micro parallelism in modern CPUs, I found that doing much more math in somewhat longish functions can provide way more bang for the buck than repeatedly iterating over buffers with multiple simple operations. It’s also worth mentioning that modern compilers are very good optimising everything that they can make solid assumptions about (read about SSA).
Ultimately, it’s important to come up with realistic benchmarks which measure performance in a plausible usage scenario (so, no micro-benchmarking small building blocks, gotta benchmark in context to put stress on memory). Also note that we once more have to cope with very different architectures with x86_64 and Apples Arm flavor, the latter having massive advantages when it comes to these memory things…

Carrier.processBlock(ModOscBuffer)

How is that awkward?

That said, if you want to do FM with feedback (which is the most fun way to use FM), it’s better to process sample by sample, rather than blocks, and then output a block later down the chain.