Teletype Sine Wave LFO
Here’s a way to generate a sine LFO on CV 1 that’s based on numerically integrating the 2nd-order differential equation for a spring, namely, y’’(t) = -y(t) with boundary conditions y(0) = 1, y’(0) = 0.
Some other features:
- The PARAM knob can be used to change the LFO frequency from roughly 5Hz down to roughly 0.2Hz.
- The variable
A
defines the amplitude of the LFO in centi-Volts (e.g., A 500
makes the CV output go from 0V to 10V peak-to-peak
- The variable
B
defines an offset for the LFO in centi-Volts (e.g., if you want voltage to range from 1V to 9V, use A 400; B 100
).
The Scripts
# I
PARAM.SCALE 200 5000
A 500; B 0; Y 30; Z 0; $ 1
# 1
T PARAM; J / T 20
Z - Z / Y 14; Y + Y / Z 14
CV 1 VV + / * + Y 30 A 30 B
DEL J: CV.SLEW 1 / J 2; $ 1
For those interested in how this works, there’s a brief explanation below after the video.
Happy to answer any questions.
Video Demo
Explanation
So the core idea of this script is to numerically solve the differential equation y’’(t) = -y(t). Basically, this says that the rate of change of the rate of change y’’(t) at time t is -y(t), i.e., the voltage at time t.
This equations mean that if we take a small time step Δt we can approximate the rate of change y’(t) as:
y’(t + Δt) ≈ y’(t) + Δt y’’(t) = y’(t) - Δt y(t)
and we can also write the value of the voltage y(t) as:
y(t + Δt) ≈ y(t) + Δt y’(t).
We can express these updates in Teletype code like so:
Z - Z / Y K
Y + Y / Z K
Where Z
is y’(t), Y
is y(t) and K
is the reciprocal of the step size, i.e., Δt = 1/K.
That’s really the core of the script. The above integration can generate a sine (well, technically a cosine) with an amplitude of 1 at a fixed frequency. The rest of the script is for allowing the amplitude to be scaled up and for the frequency to be changed.
The frequency scaling is handled by a temporal recursion using DEL
and a variable delay to implement different sized Δt. The rest of the complication is from having to handle lots of small updates when only fixed precision math is available.
Line-by-line
Initialization Script
PARAM.SCALE 200 5000
Set the range of the knob from 200ms to 5000ms.
A 500; B 0; Y 30; Z 0; $ 1
Set the amplitude A
to 5V, the offset B
to 0V then initialise the boundary conditions for the differential equation: Y
= y(0) = 30 and Z
= y’(0) = 0. Then start Script 1.
The use of 30 here instead of one is one of several “magic numbers” that I had to tune by hand to work within the constraints of signed 16-bit integers and a CV output range of 0 to 16,384.
The value 30 is small enough to allow for a reasonably small amplitude LFO of 0.3V but large enough so that we can numerically integrate using only integers.
Script 1
T PARAM; J / T 20
Read the knob value and use it as the period of the LFO in milliseconds. Compute a delay size J
by diving the overall time per cycle by 20. This is another “magic number” that was chosen by calibration.
Z - Z / Y 14; Y + Y / Z 14
This is the main integration loop as described in the pervious section. The 14 is yet another “magic number”. It relates to the earlier choice of 30 for the amplitude of the “base” sine wave. It is chosen to ensure that updates are not too large or small.
CV 1 VV + / * + Y 30 A 30 B
This rescales the “base” sine wave in Y
so that it has amplitude A
and offset B
. First, we compute Y + 30
so we get a value in (0, 60) instead of (-30, 30). Then we scale up by A / 30
(since the “base” wave has amplitude 30). Then we add the offset B
before passing the resulting centi-Volts though the VV
op.
DEL J: CV.SLEW 1 / J 2; $ 1
Finally, we pause for J
milliseconds before updating the CV slew and then recursively calling Script 1. The slew here is chosen to be half the update time so that the “stepping” from the fact we are approximating the differential equation with integer updates is smoothed over.