Tutorial: A simple LFO
Here’s an example of the first LFO I made, with which I wasn’t too happy, but this learning curve may be relevant.
I demonstrated this on Superbooth 2022 on a Korg NTS-1. A lot of visitors didn’t (want to) believe the modulated sound was made by that little device. That was a bit of motivation for me to keep searching for better modulation for simple synths in dire need
So here below I’ll explain the LFO I built back then for demo purposes. Actually in SB there were two Electra LFO’s running simultaneously, but I left the second one out to keep focus (imagine: combined with the NTS-1’s own tremolo and pitch lfo’s, and additonal aftertouch and modwheel capabilities I already explained, a lot was going on at the same time ).
Anyways, this was my first attempt to make an LFO, using 5 controls. It will use the timer function of the Electra to provide modulation :
- LFO On/Off : used to start and stop the timer
function timerEnable (valueObject, value)
if value == 1 then -- start the timer
timer.enable ()
lfoTarget1= parameterMap.get(deviceId, PT_VIRTUAL, 134) -- read the destination
lfoDefault1 = parameterMap.get(deviceId, PT_CC7,lfoTarget1) -- read the value of the destination before modulating it
print (lfoTarget1.." def= "..lfoDefault1 )
else -- stop the timer
timer.disable()
parameterMap.set(deviceId, PT_CC7, lfoTarget1,lfoDefault1) -- put back the original value after stopping the modulation
parameterMap.send(deviceId, PT_CC7, lfoTarget1)
lfoPrevTarget1 =128 -- make the lfo forget its previous target
end
end
- Rate: set the lfo speed in steps
function lfoRate1(valueObject, value)
lfoStep1 = value
end
- Type: choose between triangle, square, ramp up and ramp down
function lfoShape1(valueObject, value)
lfoType1 = value
end
- Depth: set the depth of the modulation
function lfoDpth1(valueObject, value)
lfoDepth1 = value
end
- Destination : in this example all destinations are meant to be controlled as CC values 0-127
function lfoDest1(valueObject, value)
if lfoPrevTarget1 ~= 128 then -- be sure to set and send any previous target back to its original value
parameterMap.set(deviceId, PT_CC7, lfoTarget1,lfoDefault1)
parameterMap.send(deviceId, PT_CC7, lfoTarget1)
end
lfoPrevTarget1 = lfoTarget1 -- set the 'new' previous target
lfoTarget1= parameterMap.get(deviceId, PT_VIRTUAL, 134)
lfoDefault1 = parameterMap.get(deviceId, PT_CC7,lfoTarget1) -- memorize the original value of the new target
print ("value= "..value.." lfoPrevTarget1 : "..lfoPrevTarget1 .." lfoTarget1 : "..lfoTarget1.." with lfoDefault1 : "..lfoDefault1 )
end
Now, all of that won’t do much good. We also need to initialize some parameters and then tell the E1 what to do when the timer reaches a tick.
- The initialisation of the parameters
deviceId = 1 -- check your preset, as your deviceId might be chosen differently
device = devices.get(deviceId)
devPort = device:getPort()
channel = device:getChannel()
lfoVal1= 0 -- current value of the lfo (will move between 0 and 127)
lfoPos1 = 1 -- current position of the lfo in its cycle (cycle starts at 1 until 1270)
lfoStep1 = 0
lfoType1 = 0
lfoPrevTarget1=128
lfoDepth1=127
lfoTarget1=26 -- setting a default could be resolved nicer, but I used a shortcut here
parameterMap.set (deviceId, PT_VIRTUAL, 134, lfoTarget1)
lfoDefault1 = parameterMap.get(deviceId, PT_CC7,lfoTarget1)
timer.setPeriod (20) -- timer will tick each 20 milliseconds
Finally the heartbeat:
Every time 20 milliseconds elapse, the E1 will set and send a MIDI message and move to the next lfo value and lfo position, as long as the timer remains enabled.
function timer.onTick ()
parameterMap.set (1, PT_CC7, lfoTarget1, math.min(math.max(lfoDefault1+lfoVal1,0),127)) -- changes the control that is being modulated within the 0-127 boundaries starting at its orginal default value
parameterMap.send (1, PT_CC7, lfoTarget1) -- modulates the control
lfoPos1= math.fmod (lfoPos1+ lfoStep1+1, 1270)+1 -- calculates the new position of the lfo between 0-1270
if lfoType1==0 then -- sets lfo value according to a triangle
if lfoPos1 >=636 then lfoVal1=math.floor(lfoDepth1*((1270-lfoPos1)/635-1/2))
else lfoVal1=math.floor(lfoDepth1*((lfoPos1)/635-1/2))
end
elseif lfoType1==1 then -- sets lfo value according to a square
if lfoPos1 >=635 then lfoVal1 = lfoDepth1 else lfoVal1 = 0
end
elseif lfoType1==2 then
lfoVal1=math.floor(lfoDepth1*(lfoPos1-1)/1270) -- sets lfo value according to a ramp up
else lfoVal1=math.floor(lfoDepth1*(1270-lfoPos1+1)/1270) -- ramp down
end
end
Lessons learned with the simple LFO
- allthough this LFO works, I could not get it stable for a longer period of time, but this could be due to other circumstances. Would love to hear some else’s experience with this LFO.
- this LFO keeps the E1 and certainly the MIDI very busy as it sends out a MIDI message every 20 ms, regardless if the value had changed or not.
- the range of the rate was not very satisfying. A linear rate is not very musical for an LFO. In the lower settings the jumps are too big, in the higher settings the changes are almost irrelevant.
- I loved to see the controls move up and down, and it is good for demo purposes, but there is a drawback: this simple logic is not coping well if you start changing the modulated midi CC value itself while the lfo is running.
Anyways, it would be good to get some feedback. If you can make this one work, you 'll be able to understand better the more elaborate ones I’m currently making.
That’s it for this week
Have a good weekend, keep on making music.