lfo 0.0.6
A Low Frequency Oscillator

# LFO - A Low Frequency Oscillator

_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._

## Synopsis

```python
lfo = LFO(period, *, ...)
```

```python

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)
```


## What is an LFO?

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.



## The LFO class

### Instantiation

```python
wave = LFO(10, default_wave=Wave.inv_sawtooth, ...)
```

### Overview

#### Primary Instancing Attributes, Read/Write

* `lfo.period: float = 1.0`
* `lfo.frequency: float = 1.0` - Internally an alias for `1 / lfo.period`
* `lfo.cycles: int = 0`
* `lfo.default_wave: lfo.Wave = lfo.Wave.sine`


#### Wave Outputs, Read-Only

* `lfo.sine: float`, `lfo.inv_sine: float`
* `lfo.cosine: float`, `lfo.inv_cosine: float`
* `lfo.triangle: float`, `lfo.inv_triangle: float`
* `lfo.sawtooth: float`, `lfo.inv_sawtooth: float`
* `lfo.square: float`, `lfo.inv_square: float`
* `lfo.one: float`, `lfo.inv_one: float`
* `lfo.zero: float`, `lfo.inv_zero: float`


#### Waveform Control Parameters and Defaults

* `lfo.sine_attenuverter: float = 1.0`
* `lfo.sine_offset: float = 0.0`
* `lfo.cosine_attenuverter: float = 1.0`
* `lfo.cosine_offset: float = 0.0`
* `lfo.triangle_attenuverter: float = 1.0`
* `lfo.triangle_offset: float = 0.0`
* `lfo.sawtooth_attenuverter: float = 1.0`
* `lfo.sawtooth_offset: float = 0.0`
* `lfo.square_attenuverter: float = 1.0`
* `lfo.square_offset: float = 0.0`
* `lfo.one_attenuverter: float = 1.0`
* `lfo.one_offset: float = 0.0`
* `lfo.zero_attenuverter: float = 1.0`
* `lfo.zero_offset: float = 0.0`
* `lfo.pw: float = 0.5`
* `lfo.pw_offset: float = 0.0`


#### Read-Only Status Attributes

* `lfo.frozen: bool` - Frozen state of the lfo
* `lfo.t: float` - Time within the current cycle of the lfo
* `lfo.normalized: float` - Like `lfo.t`, but normalized to 0 - 1
* `lfo.cycle: int` - The number of the current cycle of the lfo


## Detailed Description and Terminology

### Primary Instancing Attributes

#### `lfo.period`, `lfo.frequency`

`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.

#### `lfo.cycles`

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`.


#### `lfo.default_wave`

Set the default wave form when "casting" the lfo to `int`, `float`, `bool`, or
when calling it with `lfo()`.

Defaults to lfo.Wave.sine.


#### Wave Outputs, Read-Only

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>`.


#### Sine, Cosine (And important configuration parameters!)

    `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.

#### Triangle

    `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.

#### Sawtooth

    `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`).


#### Square

    `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.


#### One, Zero

    `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.


#### Random

    `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.

### Waveform Control Parameters

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...

#### `<waveform>_attenuverter`, `<waveform>.offset`

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.

#### `lfo.pw` and `lfo.pw_offset`

**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.

```python
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.


### Read-Only Status Attributes

#### `lfo.t`, `lfo.normalized`

`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.


#### `lfo.cycle`

The number of the loop that the lfo is currently in.  Increments after each
period.

### Methods

_An LFO is mostly a fire and forget object.  But it still offers a small
handful of methods_

#### `lfo.reset() -> None`

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.

#### `lfo.freeze() -> None`, `lfo.unfreeze() -> None`, `lfo.is_frozen() -> bool`

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.

#### `lfo.set_attenuverters(amount: double) -> None`

Set all attenuverters to the given amount.

**Note:** This might require sine and cosine attenuverters to be set
additionally.

#### `lfo.set_offsets(amount: double) -> None`

Set all offsets to the given amount.

**Note:** This might require sine and cosine attenuverters to be set
additionally.

#### `lfo.rewind(amount: double) -> None`

Rewind the lfo by the given amount in seconds.

#### `lfo.skip(amount: double) -> None`

Skip the lfo forward in time by the given amount in seconds.

#### `lfo.set_default_wave(n) -> None`

_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)


### Python Magic Methods

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.

```python
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
```


## Example

_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.**

```python
#!/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}'
```

## Installation

Note that this module is in very early draft, and while it is already useful
and functional, things might change...


### Installation via pip

lfo is available on pypi and can be installed by

```console
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.


### Pre-built wheels for Windows, Mac and Linux...

...are available on the github releases page at

https://github.com/dickerdackel/lfo/releases


### Install from source

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 .`

```console
# 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$
```

## Support / Contributing

Issues can be opened on [Github](https://github.com/dickerdackel/lfo/issues)

## Credits / Acknowledgements

* Thanks to all the modular synth vendors that showed me the versatility of
  LFOs

## License

This software is provided under the MIT license.

See LICENSE file for details.