Modular synthesis modulation architecture

DSP, Plug-in and Host development discussion.
27 posts since 8 Nov, 2020

Post Mon Oct 25, 2021 6:49 am

Hey everyone,
What do you do for modulating parameters from a source? I’ve been spending a bit of time on properly figuring out how to do this efficiently and in a way that I and others can easily understand to build upon it.

Here’s my current process
-Each parameter in the synth has a pointer variable (this includes voice parameters like pitch. If I was working with 16 voices, I would have 16 pitch pointers (assuming I have only one oscillator).
-The main synth class ticks through all of the sources (LFOs, envelopes, etc) each buffer of samples (once every 1024 times if your buffer length is 1024) and stores the results.
-On the main loop, the parameter pointers are set every n frames.

Thoughts? Is this a bad idea? Thanks.

6574 posts since 12 Feb, 2006 from Helsinki, Finland

Post Tue Oct 26, 2021 2:41 am

Modulation is one of those things where there's probably no "one size fits all" solution, but the general idea is that you have a bunch of sources (represented in code by a pair of scalars for linear interp, or a buffer for audio rate) and a bunch targets (represented similarly).

Then you keep a "mod matrix" as a list (well, array typically) of {src,dst,amount} tuples. In practice, these can be a bit more complex too, for example something like dst+=src1*src2*amount (ie. each row stores {src1,src2,dst,amount} at least can be handy (where one of the sources can be set to a constant source), so an envelope can control how much LFO is added to pitch, or whatever.

Ideally for each modulation update (eg. every 16 or so samples when doing linear interp, or once per buffer when doing audio rate), update all sources, clear all targets to default (eg. you can mix things like current note pitch here directly), then loop the mod-matrix to multiply-accumulate sources to targets. The nice thing about this approach is that neither the modulation sources or the modulation targets need to know anything about the modulation routing, they just see the modulation values (whether interpolated or buffers or whatever) that they read or write.

I'd usually not bother with pointers if the list of sources and destinations are fixed, because it's usually just easier (eg. for debugging) to build enums for each and then store the enum indexes into the mod matrix. These same enums can also be stored into your patch-format as-is, used as index in your user-interface menus and ... well in general it just makes things easier. You could store pointers instead, but that likely only makes sense if you're doing a modular synth where the set of possible sources and targets isn't known a priori.

Now, I said "ideally" above, because there might be some complications in terms of the update order, in case you want to avoid adding latency when one source modulates another source which modulates some actual module. To avoid the latency in this case (other than by special casing) you'll basically need to take the {src,dst} tuples as edges of a directed graph, make it into a DAG by breaking cycles (since you'll have no choice but to introduce a delay for those) and then work down the DAG updating modules and then mixing their outputs to the downstream targets as you go. Standard graph algorithms apply, so it's not like this is truly rocket science, but if you can constrain your synth's design (possibly by special casing, or splitting your modulation into a priori layers) then that's sort of easier. If you're not doing audio-rate, but just processing modulation at some fixed rate (eg. every 16 samples) then I'd probably not worry about the latency too much 'cos the modulation isn't going to be super accurate anyway (where as it's a bigger issue if you're doing audio rate modulation and processing say 1024 samples with bulk buffers, where you probably don't want the whole 1024 samples worth of latency for simple modulator chains).

Either way, my two cents is to keep all this modulation stuff out of your modules at all cost, because you almost certainly don't want to rewrite all your modules every time you decide to change how your modulation is handled (which .. you might find you end up choosing a slightly different approach for every synth you write, where as you could often otherwise reuse the modules as is).
Preferred pronouns would be "it/it" because according to this country, I'm a piece of human trash.

6574 posts since 12 Feb, 2006 from Helsinki, Finland

Post Tue Oct 26, 2021 3:20 am

I also feel like adding that a general purpose mod-matrix like above (eg. a flat list of rows of modulations) also does not place too many restrictions on the actual user interface you want to present. Your UI could have modulation target-slots in source modules (ie. "push model"), it could have source-slots in target modules (ie. "pull model"), it could have patch cables that you can drag around, it could have a flat list of modulation assignments, or even a 2D matrix of amount-knobs. All of these can be flattened to a flat list (ie. array, std::vector) of modulation assignments and processed by the same generic mod-matrix code that loops rows and multiply-accumulates. If you want to change the UI, you just change how the list of modulations is built, the actual modulation code can stay the same.
Preferred pronouns would be "it/it" because according to this country, I'm a piece of human trash.

6574 posts since 12 Feb, 2006 from Helsinki, Finland

Post Tue Oct 26, 2021 3:27 am

Also why a pair of scalars (and not just a single value) for linear interp? Because when you do something like reset a voice, you might want to start some of the modulations (eg. envelopes of that voice) from zero, while interpolating other modulations (eg. a global LFOs) from it's previous value. By having the modulation system accumulate {prev,next} pairs (or equivalent) you can introduce discontinuities in some modulation signals, while having other modulation signals remain smooth and it all works out with no additional effort.
Preferred pronouns would be "it/it" because according to this country, I'm a piece of human trash.

Return to “DSP and Plug-in Development”