Initial Explorations of ASL Oscillators
ASL oscillators use an ASL table with loop{} to loop a series a to() functions. Set the ASL table as an output's action
and then run the action. Refer to the
LFO example in the asllib.lua file. We use Lua to create the ASL tables and pass
them to the output which converts it to C. The C code sequences through the different to() calls at the right time,
handles looping (loop{}), and does the DSP of generating samples to send to DAC for the hardware outputs. C code runs
internally at 48kHz and we do not have direct access to the samples created. We can send
updates to dyn variables
inside the ASL table from the Lua level at a stable speed of roughly 200 Hz or 0.005 seconds per update (citation needed)
before crow starts sending “event queue full!”. Filling the event queue is effectively our CPU limit.
A Lua update speed of 0.005 sec per update (200Hz) has been a stable top speed for me, while the
maximum is 0.0015 sec per update (citation needed, I saw it in a Maps episode).
The oscillation happens at the C level where the ASL oscillator can freely loop back and forth to form the waveshape.
Slower CV type tasks like envelopes, LFOs, sequencers, and parameter settings happen at the Lua level by updating dyn variables.
Larger ASL tables with more to() stages, dynamic variables, and math functions are more computationally expensive to run on crow.
Running 4 complex oscillators simultaneously can cause event queue errors, so the drumcrow models used are quite lightweight
compared to what’s possible with ASL. However, crow can handle more complex ASL shapes with fewer outputs being used simultaneously.
If crow is crashing, reduce Lua update speed or use simpler ASL models. Reducing update speed might reduce your maximum LFO frequency
or minimum envelope decay time if you are using Lua to update dyn variables in this way.
Drumcrow works by setting each output to be an ASL oscillator, then runs an
update loop in Lua which applies two envelopes,
one LFO, and a sequencer to the dyn variables. Galapagoose made a simple bass drum
without a Lua update loop which drastically
reduces the computational load. Zebra expanded on Galapagoose’s code to make a closed
formula for decay time for the bass drum.
3-foot-1 made a linear congruential generator (LCG) pseudo-white noise oscillator.
Further investigation into oscillators using
Zebra’s decay time formula could reduce computation cost by replacing envelopes created with a Lua update loop or by using
envelopes created with dynamic variables with mutator functions.
A looped ASL table using arithmetic and mutator functions mul, step, and wrap can update dynamic variables
from the C code level at the speed defined by the to() slew rate. The to() slew can be audio rate like 1/44100 seconds,
which is how 3-foot-1’s LCG oscillator works. The mutator functions of a dynamic variable are
called each time the
dynamic variable is referenced in a single to() stage. For example, take a look at this ASL table,
loop{ to(dyn{x=1}:step(1) + dyn{x}, 0.1, “sine”) }
x=1, x steps by 1 to 2, add dyn{x} which triggers step(1) returning 3, output 5V.
x=5, x steps by 1 to 6, add dyn{x} which triggers step(1) returning 7, output 13V.
(crow will maxes out at 10V, but this illustrates how mutators work)
The FMStep
model in drumcrow uses the mutators step and wrap to create an oscillator that modulates
the to() stage time inside of an ASL table directly, performing frequency modulation without needing
to use a Lua update loop. Looping ASL tables with mutating dynamic variables provide a great way to
make complex waveshapes. This style of synthesis is very similar to bytebeat
but with some key differences,
namely the dynamic variables are mutated each time they’re referenced, the to() slew time can be modulated,
and a selection of different CV shaping functions. One promising direction for creating complex ASL
oscillators is to translate bytebeat to ASL. A bytebeat oscillator (t + 1) will increment t until t hits
a maximum value and wraps around, thus creating a sawtooth waveform. This function can be translated to ASL,
but the sample rate can be adjusted using a dynamic variable, enabling this “bytebeat waveshape” to be
played chromatically as follows,
loop{ to(dyn{t=1}:step(1):wrap(0,10), dyn{cyc}, “now”) }
These same techniques are also available for norns scripting using crow.
Hardware Constraints
Dyn mutator functions, ASL math, and ASL stages evaluate at C speed 48kHz. Lua can update dyn variables as fast as 200Hz for stable operation of 4 oscillators, but probably faster for a less complex set of outputs. Crow CV inputs read at an upper rate of 1.5 kHz or 0.000667 sec per read. Crow CV outputs at an upper rate of 48 kHz or 0.00002084 sec per output. Crow can’t update ASL tables faster than every 32 samples, which is roughly 1.5kHz. Larger ASL tables take time to create, set, and run on outputs, so quickly changing between ASL tables for an output introduces discontinuities in the audio. Anything you want to do with an output in Crow is built using an ASL table. The function time() is limited to 1 ms intervals. The Crow ADC is 16bit and has 4~5(?) LSBs of noise. The function math.rand() uses stm32’s internal analog random number generator feeding an LCG.
Lua Tricks
Updaters are anything written in Lua that update variables in the ASL table. The output shapes of the updaters have a sampling rate as well,
depending on how fast the while loop executes, how fast the clock is retriggered, and so on. Drivers are used to create the updaters that
set the dynamic variables. Just some funny lingo I made up for me to categorize things in my head and figure out what’s possible depending
on the shapes of the systems involved. The “sampling rate” of drivers can be variable. Updaters updating Drivers driving Updaters.
Drivers: metro, clock, while loop, input[1]{mode=’stream’, time=0.0015 minimum}, and delay
Updaters: ENVs, LFOs, Sequins, with any sort of shape, cycle time, stages, retriggering, slew, noise, phase accumulation, stage delay, and so on.
Best Practices
Process oscillator waveforms using ASL when possible (~48kHz).
Create slower CV modulations / updaters with Lua (~200Hz) (ENV, LFO, Sequences).
Using multiple outputs on crow requires less complex ASL tables due to RAM constraints and event queue overflows. Instability can happen when we update ASL tables with Lua too often.
ASL Bytebeat Template:
loop{ to( <bytebeat equation> * amp, cyc, shape) }
- amp = VCA, something like 0 to 1, scales amplitude.
- cyc = sample rate (usually bytebeat sample rate is 8kHz, 16kHz, etc.)
- shape = ‘now’ for authentic digital, but could be any shape for different tones
- dyn{t=0}:step(1):wrap(0,255) would be “8 bits” but crow can only go +10V
dyn{t=0}:step(1/25.5):wrap(0, 10) -- or
dyn{t=0}:step(1/25.5):wrap(-5, 5) -- gets rid of “DC offset”
ASL Table | Math Equation |
---|---|
dyn{x=0} | y = 0 |
dyn{x=0}:step(0.1) | y = 0, 0.1, 0.2, 0.3, … infinity |
dyn{x=0}:step(0.1):wrap(0,1) | y = x (10 steps, 0 to 1) |
dyn{x=0}:step(0.01):wrap(0,1) | y = x (100 steps, 0 to 1) |
dyn{x=0}step(-0.01):wrap(0,1) | y = 1 - x (descend from 1 to 0) |
dyn{x=0}step(-0.01):wrap(0,1) * dyn{x=0} | y = (1 - x) * (1 - x) = 1 - 2x - x^2 |
dyn{x=0}step(0.01):wrap(0,1) * dyn{x=0} | y = x^2 |
dyn{x=0}step(0.01):wrap(0,1) * dyn{x=0} + 1 | y = x^2 + 1 |
1 - dyn{x=0}step(0.01):wrap(0,1) * dyn{x=0} | y = 1 - x^2 |
-0.5 * dyn{x=0}step(0.01):wrap(0,1) * dyn{x=0} | y = -0.5x * x |
dyn{x1=0}:step(0.01):wrap(0,1) + -0.5 * dyn{x2=0}step(0.01):wrap(0,1) * dyn{x2=0} | y = x1 + -0.5x2^2 |
dyn{x=0}:step(0.314):wrap(-3.14,3.14) * 0.101321 * dyn{x=0} * dyn{x=0} * dyn{x=0} | y = sin(x) ish root-product approximation for -3.14 to 3.14 |
References and Usernames
- Drumcrow script, postsolarpunk
- Crow as an oscillator, controlling VCA, and triggering a noise sample, Infinite Digits
- Oscillators using to() and different shapes, Jaseknighter
- Teletype updating Dyn variables, Justmat
- LCG random number generator 1, 3-foot-1
- LCG RNG, more stable, 3-foot-1
- LCG discussion and crow specific tips from Galapagoose
- LCG 16bit algorithms from Zebra
- Bass drum entirely in ASL, no lua update loop, Galapagoose
- Bass drum entirely in ASL, no lua update loop, 3-foot-1
- Closed formula for decay time! Super cool!! Zebra
- Example of ASL functions
- How ASL works at Lua level
- How ASL works at C level
- How Lua & C communicate
- Crow scripting reference
- Bytebeat, A Beginner’s Guide, NightMachines
- Bytebeat, "Some deep analysis of one-line music programs.", Viznut
- Bytebeat, "Algorithmic symphonies from one line of code -- how and why?", Viznut
- Lua Bitwise OPs