One of my many interests is playing the modular synth. That instrument is not imaginable without the help of LFOs. They are used to control every twistable knob or slider, they control the speed of oscillators, the fading in and out of filters, they can control each other, the possibilities are literally endless.
This class transfers that concept to python.
lfo = LFO(period, *, ...)from lfo import LFO
from time import sleep
orbit = LFO(10)
while orbit.cycle < 3:
print(f'{orbit.sine=} {orbit.cosine=} {orbit.triangle=} {orbit.sawtooth=} {orbit.square=}')
print(f'{orbit.inv_sine=} {orbit.inv_cosine=} {orbit.inv_triangle=} {orbit.inv_sawtooth=} {orbit.inv_square=}')
sleep(0.1)So what is an LFO? LFO stands for "Low Frequency Oscillator". It's an infinitely repeating curve that you can pull out values from. The simplest form is probably a sine wave. Regardless of how often you travel along the circle, you always get consistent and reproducible values out of it.
But LFOs come in many different shapes. Here are the ones implemented right now (see further below for descriptions), but I'm open to suggestions to extend this list:
* lfo.sine, lfo.cosine
* lfo.triangle
* lfo.sawtooth
* lfo.square
* lfo.one
* lfo.zero
* lfo.random
* lfo.inv_<waveform> - All of the above, but inverted
The lfo registers the start time of its instantiation. If no period length - the duration of one single wave - is provided, it defaults to 1 second.
Whenever you now query a value from the lfo, it gives you the proper function result of that wave for this specific point in time. Also, you can query all of these wave forms from the same lfo. The lfo instance basically just defines the heartbeat for all the waves.
Each waveform can be scaled and offset. Note, that the inverted waves use the same scale and offset as the normal ones, otherwise they would run out of sync.
There's one important difference to the lfo you might know from your DAW or synth. Since most programmers will use these to ramp other values by multiplication, this lfo is not centered around the 0 point of the y axis, but all waves except sine and cosine variants are positioned so that they return a value between 0 and 1. There are per-wave parameters to change this.
wave = LFO(10, default_wave=Wave.inv_sawtooth, ...)lfo.period: float = 1.0lfo.frequency: float = 1.0- Internally an alias for1 / lfo.periodlfo.cycles: int = 0lfo.default_wave: lfo.Wave = lfo.Wave.sine
lfo.sine: float,lfo.inv_sine: floatlfo.cosine: float,lfo.inv_cosine: floatlfo.triangle: float,lfo.inv_triangle: floatlfo.sawtooth: float,lfo.inv_sawtooth: floatlfo.square: float,lfo.inv_square: floatlfo.one: float,lfo.inv_one: floatlfo.zero: float,lfo.inv_zero: float
lfo.sine_attenuverter: float = 1.0lfo.sine_offset: float = 0.0lfo.cosine_attenuverter: float = 1.0lfo.cosine_offset: float = 0.0lfo.triangle_attenuverter: float = 1.0lfo.triangle_offset: float = 0.0lfo.sawtooth_attenuverter: float = 1.0lfo.sawtooth_offset: float = 0.0lfo.square_attenuverter: float = 1.0lfo.square_offset: float = 0.0lfo.one_attenuverter: float = 1.0lfo.one_offset: float = 0.0lfo.zero_attenuverter: float = 1.0lfo.zero_offset: float = 0.0lfo.pw: float = 0.5lfo.pw_offset: float = 0.0
lfo.frozen: bool- Frozen state of the lfolfo.t: float- Time within the current cycle of the lfolfo.normalized: float- Likelfo.t, but normalized to 0 - 1lfo.cycle: int- The number of the current cycle of the lfo
lfo.period is the primary setting for the lfo. It's the duration between
wave repeats.
lfo.frequency is the inverse of the period. While the period defines the
duration of one wave cycle, the frequency defines the number of cycles per
second.
The number of periods this lfo will run through. After that, it will return the value of the end of its period until it is reset.
The default is 0, which means the lfo will not terminate.
See note at lfo.random.
Set the default wave form when "casting" the lfo to int, float, bool, or
when calling it with lfo().
Defaults to lfo.Wave.sine.
If you have ever tried making sounds on a computer, you will be very familiar with the available wave types.
All wave forms come with an inverted version named inv_<waveform> which
returns 1 - <waveform>.
`lfo.sine`, `lfo.cosine`, `lfo.inv_sine`, `lfo.inv_cosine`
Your off-the-mill sine and cosine waves.
Note: In the first iteration of this library, I thought it was a good idea to position the sine and cosine waves in the range or 0 - 1. This idea does not work well in reality, since sine and cosine are rarely used to scale things, but to rotate them, so they now deliver the values any programmer will expect.
See <waveform>_attenuverter and <waveform>_offset below on how to change
this.
`lfo.triangle`, `lfo.inv_triangle`
A triangle wave ramps up from 0 to 1 over half of the period. Then it ramps down back to zero for the second half, creating a triangular shape.
`lfo.sawtooth`, `lfo.inv_sawtooth`
A sawtooth wave starts at 1 and ramps down to 0 over the full length of the period.
Think "fading out" (sawtooth) and "fading in" (inv_sawtooth).
`lfo.square`, `lfo.inv_square`
The square wave holds 0 over a given time that defaults to half the period, then it switches to 1. It's basically a timed switch.
See lfo.pw and lfo.pw_offset below for some configuration options.
`lfo.one`, `lfo.inv_one`, `lfo.zero`, `lfo.inv_zero`
While it may sound useless to have an object that puts out a constant value, one and zero can be very useful if you want to deactivate an changing behaviour in an object without adding a special if-clause or changing its interface.
`lfo.random`, `lfo.inv_random`
The random wave contains values that are... well, random. They are not a function of time.
Note: inv_random makes zero sense but it still implemented to provide a
consistent interface.
Note 2: A side effect of being independent of time is, that this wave will not "stop" once the all cycles have finished.
These settings all control the different wave forms. They are configurable both at class instantiation, as well as in runtime.
The consequence of that is that they can be modulated by another lfo...
All waveforms offer these two modifiers. They can passed to the LFO() init
when instantiating, and also during runtime
The weird term attenuverter also comes from the world of modular synths and is a combination of attenuator - a scale factor - and inverter - because a negative scale will invert the wave.
A good use case for these settings are the sine and cosine waves, which return values between -1 and 1. If you want to use them for scaling something instead, you set their attenuverter to 0.5. Now they return values from -0.5 to 0.5. Then you offset them by 0.5 and you have your scaling factor.
lfo = LFO(period, sine_attenuverter=0.5, sine_offset=0.5, ...)
So the attenuverter and offset alwasy return
`wave * attenuverter + offset`
Note: Even lfo.one and lfo.zero are impacted by these.
NOTE: Mighth be renamed to square_pw and square_pw_offset in future
versions.
pw stands for pulse width. In a synth, it's the duration a signal is up or
down. By default, the square wave is up for half of the period, then switches
to down. It's pulse width (up) is 0.5. Setting lfo.pw = 1/3 instead will
make the square wave generate a 1 for the first 3rd of the period, and a 0
for the remaining time.
Note: The value of the pulse width parameters is normalized to 0-1. That way, you won't have to modify it every time you change the period of the LFO.
The lfo.pw_offset now shifts the start position of the pulse. By default,
it starts at the beginning of the period. But if you would want a narrow
pulse in the middle of the wave, you can shift the start.
lfo = LFO(10, pw=2, pw_offset=4)Will result in a square wave that return 0 until the start of the 4th
second. Then it will switch to 1 from second 4 to second 6. Finally, it
will be back to 0 from second 6 until the end of the period at second 10.
So with lfo.pw you control the width of the "on-phase", and with
lfo.pw_offset you control its position.
lfo.t will give you the current time within the current cycle of the curve.
This will be a value between 0 and lfo.period.
lfo.normalized will give you the same, but scaled into the range of 0 to 1.
Both attributes will reset after each period.
The number of the loop that the lfo is currently in. Increments after each period.
An LFO is mostly a fire and forget object. But it still offers a small handful of methods
Resets the start time of the lfo to the current time. With short periods,
this will most likely not be relevant, but an lfo can also run for a very long
time, e.g. to ramp up enemy spawns over a level, and you don't want to have
it deep in its cycle when the next level begins.
Pauses the lfo. The current value is held until the pause is terminated. Note that the lfo's start time shifts so, that the wave it outputs is continuous. It will not jump once it's reactivated.
If you prefer a function interface over the status attribute lfo.frozen
described further above, use lfo.is_frozen(), which returns a bool.
Set all attenuverters to the given amount.
Note: This might require sine and cosine attenuverters to be set additionally.
Set all offsets to the given amount.
Note: This might require sine and cosine attenuverters to be set additionally.
Rewind the lfo by the given amount in seconds.
Skip the lfo forward in time by the given amount in seconds.
This is the function version of passing default_wave= to the constructor or
setting it during runtime.
Set the default wave for lfo(), float(lfo), int(lfo) and bool(lfo) (See Python Magic Methods below).
This accepts an int, but the lfo.Wave enum should be used instead.
lfo.set_default_wave(Wave.inv_triangle)
LFO instances properly convert to bool, int and float and can thus be
directly compared to all of them.
Note: By default, the sine wave is returned. This is configurable by
lfo.set_default_wave(n) described above.
So e.g. bool(lfo) is the same as bool(lfo.sine).
LFO instances also provide __call__, so if you prefer the function
interface, use lfo() to fetch the sine value (or configured default wave)
from the lfo.
In addition, lfo is both an iterator and iterable.
from lfo import LFO, Wave
from time import sleep
l = LFO(10, default_wave=Wave.inv_sawtooth)
next(l)
>>> 0.2457116119475273
l.reset()
for i, v in zip(range(10), l):
print(i, v)
sleep(1)
>>> 0 1.202999999616594e-07
>>> 1 0.10007948659999999
>>> 2 0.20009758980000003
>>> 3 0.3001155295
>>> 4 0.4001310015999999
>>> 5 0.5001526283
>>> 6 0.6002004129
>>> 7 0.7002980965
>>> 8 0.8003155583
>>> 9 0.9004469224999999
>>> 10 0.00046909910000003663_NOTE 1: to preserve the zero-dependencies of lfo, examples/demos will be published in a dedicated package lfo_demos, which is currently in the making but not published yet.
_NOTE 2: This example requires pygame-ce, but the code should be straight
forward enough to be directly translated to other frameworks like pyglet.
NOTE 3: Please don't use the badly maintained pygame project anymore.
#!/bin/env python3
import pygame
import pygame._sdl2 as sdl2
from lfo import LFO
TITLE = 'pygame minimal template'
SCREEN = pygame.Rect(0, 0, 1024, 768)
FPS = 60
DT_MAX = 3 / FPS
clock = pygame.time.Clock()
window = pygame.Window(title=TITLE, fullscreen_desktop=True)
renderer = sdl2.Renderer(window)
renderer.logical_size = SCREEN.size
RADIUS = 256
SATELITE_RADIUS = 32
orbit = LFO(10, sine_offset=0.0, cosine_offset=0.0)
satelite = LFO(5, sine_offset=0.0, cosine_offset=0.0)
speedo = LFO(20, sine_attenuverter=0.3, sine_offset=1.0, cosine_attenuverter=0.3, cosine_offset=1.0)
color = LFO(3)
rect = pygame.Rect(0, 0, 10, 10)
running = True
while running:
dt = min(clock.tick(FPS) / 1000.0, DT_MAX)
for e in pygame.event.get():
if e.type == pygame.QUIT:
running = False
elif e.type == pygame.KEYDOWN:
if e.key == pygame.K_ESCAPE:
running = False
renderer.draw_color = 'darkslategrey'
renderer.clear()
renderer.draw_color = ('red', 'green')[int(color.square)]
renderer.draw_rect(rect.move_to(center=SCREEN.center))
x = orbit.cosine * RADIUS + SCREEN.centerx
y = orbit.sine * RADIUS + SCREEN.centery
renderer.draw_color = ('yellow', 'magenta')[int(color.square)]
renderer.draw_rect(rect.move_to(center=(round(x), round(y))))
x += satelite.cosine * SATELITE_RADIUS
y += satelite.sine * SATELITE_RADIUS
renderer.draw_color = ('cyan', 'orange')[int(color.square)]
renderer.draw_rect(rect.move_to(center=(round(x), round(y))))
satelite.period = speedo.sine
x = orbit.sine * 2 * RADIUS + SCREEN.centerx + satelite.sine * SATELITE_RADIUS
y = orbit.cosine * 2 * RADIUS + SCREEN.centery + satelite.cosine * SATELITE_RADIUS
renderer.draw_color = ('green', 'red')[int(color.square)]
renderer.draw_rect(rect.scale_by(2).move_to(center=(round(x), round(y))))
renderer.present()
window.title = f'{TITLE} - time={pygame.time.get_ticks()/1000:.2f} fps={clock.get_fps():.2f}'Note that this module is in very early draft, and while it is already useful and functional, things might change...
lfo is available on pypi and can be installed by
pip install lfo(You are using venvs, right? RIGHT?!?)
lfo comes with no additional requirements, but it is very good to create input
for easing functions from rpeasings. lfo was primarily created as a tool
for my pygame projects, but it's a generalized control tool that can be used
in many different scenarios and environments.
...are available on the github releases page at
https://github.com/dickerdackel/lfo/releases
Again, use a venv!
lfo is packaged following the python packaging authority at https://www.pypa.io/
You can simply clone the github repo from https://github.com/dickerdackel/lfo
Then change into this directory and install the package - INTO YOUR VENV! -
with pip install .
# clone repo
dickerdackel@minime:~$ git clone https://github.com/dickerdackel/lfo
...
# create venv
dickerdackel@minime:~/lfo$ python3 -m venv --prompt lfo .venv
# activate venv
dickerdackel@minime:~/lfo$ . .venv/bin/activate
# install lfo
(lfo) dickerdackel@minime:~/lfo$ pip install .
(lfo) dickerdackel@minime:~/lfo$Issues can be opened on Github
- Thanks to all the modular synth vendors that showed me the versatility of LFOs
This software is provided under the MIT license.
See LICENSE file for details.