Low frequency oscillators (LFO)
A low frequency oscillator (or short LFO) is a common way on synthesizers to continuously modulate various signals.
This documentation is an interactive version of glossing’s tutorial https://www.youtube.com/watch?v=11frBA9L638
Signals vs LFOs
In Strudel, there are two ways to modulate:
- signals for pattern-level modulation
lfo(this page) for audio-level modulation
Applying an LFO
Here, the LFO will change the frequency of the saw. Put a comment like this //.lfo() to see and hear how it changes, and remove the comment again.
s("saw").lfo()
.lpf(800)
._spectrum({height: 300, width: 800})By default, the LFO will modulate the control parameter which is right before .lfo():
s("saw")
.lpf(800).lfo()
._spectrum({height: 300, width: 800})Here, the LFO will modulate the low pass filter .lpf.
Moving away from the default
The following sections explain how pass parameters to .lfo. Similar to ._spectrum above, almost all the configuration of lfo lives inside a json object, starting with { and ending with }.
All the parameters (except id) are written as key: value inside and separated by ,.
The reference refers to them as config.key, i.e. for the following one as config.control but you use them like below.
Control
control determines which parameter will be modulated. This allows you to place your lfo at different places,
not necessarily immediately after the controlled parameter.
Here, we place lfo after the s, but it modulates the low pass filter.
s("saw")
.lfo({control:'lpf'})
.lpf(800)
._spectrum({height: 300, width: 800})You can even influence parameters which are always present, even if you haven’t explicitly written them, like gain.
s("saw")
.lfo({control:'gain'})
.lpf(800)
._spectrum({height: 300, width: 800})control has an alias c.
Rate
rate determines how often the lfo oscillates per second:
s("saw")
.lfo({c:'gain', rate:"<2 4>"})
.lpf(800)
._spectrum({height: 300, width: 800})The alias of rate is r.
Sync
Instead of controlling the frequency with rate by setting a frequency in Hz, you can use sync
to snychronize your lfo with your other patterns.
sync expresses this frequency as “times per cycle”
Try removing the sync: "<2 4 8 0.5 >" from the pattern and notice that there is something not in sync.
$: s("bd*4").bank("TR909").postgain(0.5)
$: s("saw")
.lpf(800)
.lfo({sync: "<2 4 8 0.5 >"})
._spectrum({height: 300, width: 800})You can put patterns into the parameters of lfo if you want them to change over time, as seen above.
Relative Depth
.depth is relative depth, a value of 1 (the default) means that
the value goes above and below by half of the value which is being modulated
E.g. for depth 1, it modulates the frequency to oscillate between 32 = (64/2) and 96 (= 64 + 64/2).
s("saw").freq(64)
.lfo({r: 2, depth: "<1 2 3>"})
.lpf(800)
._spectrum({height: 300, width: 800})The freq is not needed here, as this is the default frequency, it’s just for instructive purposes.
The aliases of depth are dr and dep.
Absolute Depth
depthabs controls the absolute modulation depth. For example you can modulate
the low pass filter by exactly 250 Hz up and below:
s("saw")
.lpf(800)
.lfo({r: 2, depthabs: "250"})The alias of depthabs is da.
DC offset
If you don’t want to go up or down by the same amount with your modulation, then you can shift the center of the modulation with dcoffset.
The default value is -0.5, which is the middle point between:
“dcoffset = 0: All modulations increase the control parameter (or keep it constant)”
“dcoffset = -1: All modulations decrease the control parameter (or keep it constant)”
Other values will work as well.
s("saw")
.lpf(600)
.lfo({r: 2, da: "200", dcoffset: "<-0.5 -1 -0.5 0>"})The marvellous alias of dcoffset is dc.
Shape
You can change the shape of the modulation with shape. The default is triangle, but other shapes are available too:
s("saw")
.lpf(800)
.lfo({r: 2, shape: "<triangle sine ramp saw square>"}) You can add ._spectrum() to see the shape of the modulations in the spectrum.
You can also get these shapes by using numbers:
| Shape | number |
|---|---|
| triangle | 0 |
| sine | 1 |
| ramp | 2 |
| saw | 3 |
| square | 4 |
this way, you can use a function like irand to generate numbers:
s("saw")
.lpf(800)
.lfo({r: 2, shape: irand(4)}) The alias of shape is sh.
Skewing some shapes
You can influence some of the shapes (triangle and square) in more detail.
The default skew is 0.5, it does different things for these two.
For triangle, it skews the top of the triangle to the left or right,
where 0 makes it look like saw and 1 makes it look like ramp.
For square, the skew influences the pulse width (see reference for pulse and .pw):
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle square>/5",
skew: "<0 0.25 0.5 0.75 1>"}) The alias of skew is sk.
Curve
You can change the curves of your lfo and can make it more intense. The default value is 1. Writing numbers greater than one can make it more intense, numbers between 0 and 1 will make it less intense. This will raise the lfo to the power of curve, so larger numbers (such as 10) can have very unexpected results.
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle>", curve: "<1.3 1 0.75 1>"}) Referencing your lfos with id
All the lfos are numbered from the first starting with 0, the second having an id of 1 and so on.
You can refer to this in a later call if you want to modify a specific lfo.
The id is outside of the config json (which is different from the other parameters of lfo()
Try out how the sound changes when you replace the 0 with a 1.
s("saw").lfo().lpf(800)
.lfo({s: "<4 8 0.5>"})
.sometimes(x => x.lfo({dr: "4"},0))You can also name your lfos and refer to them by name, using the id parameter
s("saw").lfo({}, "lfo_freq_saw").lpf(800)
.lfo({s: "<4 8 0.5>"}, "lfo_lpf")
.sometimes(x => x.lfo({dr: "4"},"<lfo_lpf lfo_freq_saw>"))FX index
If you are using FX() to reorder your effects, you dont need to write your lfos inside the FX,
but instead can refer to them by their FX index (starting with 0)
s("saw").FX(
distort(3),
gain(0.3), // this has the fx index 1
lpf(400)
).lfo({s: 16, dr:2, c:"gain", fxi: 1})Modulating other LFOs with Sub-control
LFOs can modulate other lfos and will modulate their frequency (given by r or s):
s("saw").lpf(400).gain(0.8)
.lfo({s: 16, dr:2, c:"gain"})
.lfo({s: 0.3, dc:-1, dr: 0.8})To modulate other parameters of the first lfo (like skew, depth and so on), we can specify this with subControlor
its alias sc
s("saw").lpf(400).gain(0.8)
.lfo({s: 4, dr:2, c:"gain"})
.lfo({s: 0.3, sc: "skew"})