# Frequency Modulation – Phase Modulation – Delay Modulation (FM / PM / DM)

Thinking again about these basic synthesis strategies, I wanted to see how to write them in equivalent ways in SC. Phase modulation is roughly the same as frequency modulation: it produces sidebands in the same way, and this is also the case for delay modulation. Delay modulation is practical because it enables FM-like modulation without knowing the source’s frequencies.

Definitions:

`carr: carrier frequency`
`mod: modulator frequency`
`dev: deviation, maximum change of carrier`

`index = dev / mod`

Let `phi(t)` be the phase, with a time-dependent frequency, the phase of the sine is the derivation of the frequency – and the phase is the integrated frequency change.

FM written as function `f(t)`:

`f(t) = sin(2pi carr t + 2pi dev INT(0, t, sin(2pi mod x) dx))`

`phi(t) = 2pi dev INT(0, t, sin(2pi mod x) dx)`

Resolving the integral leads to

`phi(t) = (1 - sin(2pi mod t + pi/2)) * index`

Therefore, FM and PM with sine oscillators can equivalently be written like this. Due to a restriction of SinOsc’s phase argument (it has to be between -8pi and +8pi), we apply a modulo operation to phi.

``````(
// evaluate several times
{
var carr = Rand(100, 1000).poll(0, \carr);
var mod = Rand(100, 1000).poll(0, \mod);
var index = Rand(0.5, 5).poll(0, \index);

var dev = mod * index;

var fm = SinOsc.ar(carr + SinOsc.ar(mod, 0, dev));
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index);
var pm = SinOsc.ar(carr, phi.mod(2pi));

[fm, pm] * 0.1
}.plot
)
``````

With delay modulation, `t` is replaced by `t - delay(t), delay(t) >= 0`. To equal phase modulation, we must have

`sin(2pi carr (t - delay(t))) = sin(2pi carr t + phi(t))`

With `0 < phi(t) < 2pi`, this holds for

`delay(t) = (1 - phi(t)/2pi) / carr > 0`

Implementing this with DelayL or DelayC works straightly – however, a snippet at the start is missing. This can be related to the fact that in this example we start with a raising modulator frequency (for `delay(t) > t` the state is unknown).

``````(
// evaluate several times
{
var carr = Rand(200, 1000).poll(0, \carr);
var mod = Rand(50, 300).poll(0, \mod);
var index = Rand(0.5, 5).poll(0, \index);

var dev = mod * index;

var fm = SinOsc.ar(carr + SinOsc.ar(mod, 0, dev));
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index).mod(2pi);
var pm = SinOsc.ar(carr, phi);
// DelayC might be more accurate than DelayL
var dm = DelayC.ar(SinOsc.ar(carr), 1, (phi / -2pi + 1) / carr);

[fm, pm, dm] * 0.1
}.plot
)
``````

The init section issue (if it is such) improves by starting with a decreasing carrier (a modulator phase shift of 180 degrees). In most practical use cases, the introduced complication probably doesn’t pay as we would start with an envelope anyway.

There is a slight numerical difference between the FM, PM, and DM, but this is not audible in most cases.

``````(
// comparison test
x = { |type = 0|
var carr = Rand(200, 1000).poll(0, \carr);
var mod = Rand(30, 500).poll(0, \mod);
var index = Rand(1.5, 3).poll(0, \index);

var dev = mod * index;

var fm = SinOsc.ar(carr + SinOsc.ar(mod, 0, dev));
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index).mod(2pi);
var pm = SinOsc.ar(carr, phi);
var dm = DelayC.ar(SinOsc.ar(carr), 1, (phi / -2pi + 1) / carr);

type.poll(1, \type);
Select.ar(type, [fm, pm, dm]) * 0.1 ! 2;
}.play
)

s.scope

s.freqscope

// change type of modulation, no audible difference

x.set(\type, 1)   // PM

x.set(\type, 2)   // DM

x.set(\type, 0)   // FM

x.release
``````

With unknown input for delay modulation, some rough estimations can help. Here the index works like in the SinOsc case for a 200 Hz carrier, maximum delay 5 ms.

``````
(
// control sideband strength with mouse
x = {
var mod = 200;
var index = MouseX.kr(0, 3).poll(5, \index);

var dev = mod * index;

var src = LFTri.ar(LFDNoise3.ar(0.2!2).range(50, [700, 700]));
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index).mod(2pi);
var dm = DelayC.ar(src, 0.2, ((phi / -2pi + 1) / 200).poll(5, \delay));

Splay.ar(dm, 0.7) * 0.1;
}.play
)

s.freqscope

x.release
``````

However, the basic meaning of the index in the sine case is lost. Sidebands are not of equal strength in all registers:

``````(
// control sideband strength with mouse
// observe different strengths of low and high sin esp. with low index values

x = {
var mod = 200;
var fund = 500;
var index = MouseX.kr(0, 2).poll(5, \index);

var dev = mod * index;

var src = SinOsc.ar([1, 3] * fund);
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index).mod(2pi);

var dm = DelayC.ar(src, 1, ((phi / -2pi + 1) / fund).poll(5, \delay));
// this would compensate
// var dm = DelayC.ar(src, 1, ((phi / -2pi + 1) / fund / [1, 3]).poll(5, \delay));

dm ! 2 * 0.05;
}.play
)

s.freqscope

x.release
``````

Here a comparison with buffer playback modulation of a sine wave. With BufRd, you have the option to do FM (by modulating the phasor rate) and PM (by modulating the phasor signal). The SC implementation is more practical with Sweep than with Phasor. With sine sources, this is just a proof of concept. Certainly, plain sine wave modulation with BufRd and PlayBuf is not a good idea as other issues arise (numerical stability). However, BufRd and PlayBuf are practical to apply FM-like effects to buffered sources.

``````(
u = Signal.sineFill(512, );
b = Buffer.loadCollection(s, u, 1);
)

b.plot

(
// evaluate several times
{
var carr = Rand(500, 1000).poll(0, \carr);
var mod = Rand(100, 400).poll(0, \mod);
var index = Rand(0.5, 3).poll(0, \index);

var dev = mod * index;

var fm_1 = SinOsc.ar(carr + SinOsc.ar(mod, 0, dev));
var phi = ((1 - SinOsc.ar(mod, pi/2)) * index).mod(2pi);
var pm_1 = SinOsc.ar(carr, phi);

var bufFrames = BufFrames.ir(b);
var fm_2 = BufRd.ar(
1,
b,
Sweep.ar(
0,
SinOsc.ar(mod, 0, dev, carr) * bufFrames * BufRateScale.ir(b),
// carr + SinOsc.ar(mod, 0, dev) * bufFrames * BufRateScale.ir(b),
).mod(bufFrames)
);
var pm_2 = BufRd.ar(
1,
b,
(Sweep.ar(
0,
carr * bufFrames * BufRateScale.ir(b),
) + (phi * bufFrames * BufRateScale.ir(b) / 2pi)).mod(bufFrames)
);

var fm_3 = PlayBuf.ar(
1,
b,
carr + SinOsc.ar(mod, 0, dev) * bufFrames / SampleRate.ir,
loop: 1
);
var dm_1 = DelayC.ar(SinOsc.ar(carr), 1, (phi / -2pi + 1) / carr);
var dm_2 = DelayC.ar(
PlayBuf.ar(1, b, carr * bufFrames / SampleRate.ir, loop: 1),
1,
(phi / -2pi + 1) / carr
);

[fm_1, pm_1, fm_2, pm_2, fm_3, dm_1, dm_2] * 0.1
}.plot
)
``````
7 Likes