## Creating a chorus effect by modulating the levels of partials (sines in additive synth)?

DSP, Plug-in and Host development discussion.
mikejm
KVRer
26 posts since 5 Apr, 2017
Razor by Native Instruments is an additive sine wave synth that simulates many effects like chorus by directly modulating the levels of the partials. For example, here you can see the partials being modulated up and down in creating a "chorus" sound:

What equation would be necessary to create such an effect, per partial?

Ie. Given a "rate constant" for the chorus effect, and the frequency or partial number of each sine, with what equation could this be replicated?

Thanks

BertKoor
KVRAF
10740 posts since 8 Mar, 2005 from Utrecht, Holland
This is simply modulation of the frequency of sine oscillators.

You want formulas? See
http://www.kvraudio.com/forum/viewtopic ... 9#p7087109
We are the KVR collective. Resistance is futile. You will be assimilated.
My MusicCalc is back online!!

mikejm
KVRer
26 posts since 5 Apr, 2017
BertKoor wrote:This is simply modulation of the frequency of sine oscillators.

You want formulas? See
http://www.kvraudio.com/forum/viewtopic ... 9#p7087109
I'm not sure if I understand or if that is correct.

According to the visual depiction of the sine wave partials by Razor's display, there is no change in the frequency of any of the partials (x-axis). The only thing it is modulating to create this chorus sound is the amplitude of the partials (y-axis) which is what creates the beating effect of the chorus.

However, the core code behind Razor is incredibly complex and hard to piece out so it is hard for me to figure out how they do this. Modulating frequency (pitch) however does not seem to be part of it at least from what is evident in the animation example I provided.

Any further ideas/thoughts?

mystran
KVRAF
5001 posts since 12 Feb, 2006 from Helsinki, Finland
Right.. so we have this fun little equivalence: 2*cos(a)*cos(b) = cos(a-b) + cos(a+b)!

So if you amplitude modulate with another frequency (or a set of frequencies), you get side-bands shifted from the carrier frequency. If you do this for all your harmonics, but use modulation frequencies proportional to the frequencies of the harmonics, you get something that very much resembles a chorus effect. I don't know if this is how Razor does it, but it seems like the most likely approach. There is also the complex variant that'll let you shift in one direction only, but for a chorus that would be a lot of extra calculations for little to no practical benefit.
If you'd like Signaldust to return, please ask Katinka Tuisku to resign.

signalsmith
KVRer
7 posts since 5 Jul, 2018
TL;DR: For the simplest possible case (symmetric chorus, single LFO), the modulation for each partial would be:

Code: Select all

``````chorus_delay_seconds = chorus_depth*sin(chorus_phase)
output_sin = dry + wet*sin(chorus_delay_seconds*partial_freq)

// Use it to modulate the partial
output = sin(phase)*output_sin
``````
The full derivation

The exact modulation (amplitude/phase for a given frequency) for a delay-based chorus depends on things like delay lengths, number of voices, LFO shape, and so on. There are lots of details like lower frequencies not being affected as much, or complex frequency-specific interactions (like the way the 7th partial in your example remains constant in your example).

To emulate those exactly, I'm not sure there's a better/cleaner way than calculating it than directly - i.e. figure out what your chorus's delay times would be at that instant, and just add up various delayed versions of the partial, so your output is

Code: Select all

``````sin(phase + delay1*freq) + sin(phase + delay2*freq) + ...
``````
If you want, you can split that into separate sin/cos components, using the rule sin(a+b) = sin(a)cos(b) + cos(a)sin(b), which then lets you calculate an amplitude for your display

Code: Select all

``````sin(phase)*(cos(delay1*freq) + cos(delay2*freq) + ...) + cos(phase)*(sin(delay1*freq) + sin(delay2*freq) + ...)

or:
sin(phase)*output_sin + cos(phase)*output_cos
where
output_sin = cos(delay1*freq) + cos(delay2*freq) + ...
output_cos = sin(delay1*freq) + sin(delay2*freq) + ...

magnitude for display:
sqrt(output_sin*output_sin + output_cos*output_cos)
``````
(OK, so the delays are upside-down there and should be negated, but I don't believe that makes a difference to the sound as long as they're all consistent.)

If your chorus's delay lines are weighted, you'd need to add some gain factors to each delayN component, and perhaps an extra "1 + " to output_sin if there's a dry signal.

Frequency changes, and symmetrical delays
mikejm wrote:According to the visual depiction of the sine wave partials by Razor's display, there is no change in the frequency of any of the partials (x-axis).
In general for a chorus, I would expect there to be frequency changes. There is the specific case of a completely symmetrical chorus, where every delay-line has a matching delay-line with opposite delay.

In the equations above, that means delay2 = -delay1, delay4 = -delay3, and so on - which would mean that output_cos comes out as 0 (because sin(delay1*freq) = -sin(delay2*freq), etc.), so you can drop all those calculations. You can also drop half the output_sin calculations (because cos(delay1*freq) = cos(delay2*freq), etc.) so you get a 4x speedup overall.

This boils down to exactly the case that @mystran talked about - I don't know how much you see it in actual choruses because you need negative delay (i.e. latency), and it'll make a subtle difference to the sound, but not necessarily a bad one.

This means that the simplest possible case to calculate is for a symmetrical chorus, with a single LFO (driving two delays in opposite directions), and the equations for this are in the TL;DR section at the top. If you have more delay lines (but still symmetrical), you just get more terms:

Code: Select all

``````chorus_seconds1 = chorus_depth1*sin(chorus_phase1)
chorus_seconds2 = chorus_depth2*sin(chorus_phase2)
...
output_sin = dry + wet1*sin(chorus_seconds1 *partial_freq) + wet2*sin(chorus_seconds2 *partial_freq) + ...

// Use it to modulate the partial
output = sin(phase)*output_sin
``````
Other reasons

This is a reasonable explanation for not having frequency shifts, but it's also possible that frequency-shifts are there, but not shown in the visualisation. This might be for simplicity (calculating it exactly is slightly trickier than amplitude - I suspect just differentiating the phase will get you an unstable result), or it might be because it's too small to be visible.

A slow/tight chorus might detune by only 5 cents, which is 0.3%. The modulated frequency ends up pretty much as the (amplitude-weighted) average of the detunings of the delay lines + dry, so it's totally plausible that the frequency is modulating, but only by 0.1% or something.

Other modulation sources

Anyway - that's if you wanted to exactly simulate a delay-based chorus. An alternative might be going for some other modulation pattern to get something that sounds thick or chorus-like but with its own character.

Instead of an LFO, you could use lowpassed noise to sound breathy and organic, or some other weirder input. You again have a choice between modulating both sin/cos components, or just modulating one and leaving the other as 0 - it will make a very slight difference.

To remain chorus-like, I would expect:
• The frequency of the modulation is generally proportional to the frequency of the partial - so a 800Hz partial would be modulating twice as fast as a 400Hz one. If they are modulating at the same rate instead of proportionally, I'd expect it to sound more phaser-like.
• The amplitude of the modulation reduces for frequencies below some limit. This limit frequency is related to the maximum delay time of our (by this point metaphorical) delay-lines. I think you could approximate this by (1 - 1/(1 + R^2)), where R=partial_freq/limit_freq.
There's no need to stick to these though.

Also, hello KVR. I just joined, and I promise not every post I make is going to be this long.

mystran
KVRAF
5001 posts since 12 Feb, 2006 from Helsinki, Finland
signalsmith wrote: Also, hello KVR. I just joined, and I promise not every post I make is going to be this long.
Welcome... and don't you worry about long posts in the DSP development forum (not that they are rare in KVR in general). We like the dirty details here.
If you'd like Signaldust to return, please ask Katinka Tuisku to resign.