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

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

ASL Oscillator Models

function var_saw(shape)
return loop { to( dyn{amp=2}, dyn{cyc=1/440} * dyn{pw=1/2}, shape), to(0-dyn{amp=2}, dyn{cyc=1/440} * (1-dyn{pw=1/2}), shape) }
end
function bytebeat(shape) return loop { to(dyn{x=1}:step(dyn{pw=1}):wrap(-20,20) * dyn{amp=2}, dyn{cyc=1}, shape) } end
function noise(shape) return loop { to(dyn{x=1}:mul(dyn{pw2=1}):step(dyn{pw=1}):wrap(-10,10) * dyn{amp=2}, dyn{cyc=1}/2, shape) } end
function FMstep(shape) return loop { to( dyn{amp=2}, dyn{x=1}:step(dyn{pw2=1}):wrap(1,2) * dyn{cyc=1} * dyn{pw=1}, shape), to(0-dyn{amp=2}, dyn{x=1} * dyn{cyc=1} * (1-dyn{pw=1}), shape) } end
function ASLsine(shape) return loop { to((dyn{x=0}:step(dyn{pw=0.314}):wrap(-3.14,3.14) + 0.101321 * dyn{x=0} * dyn{x=0} * dyn{x=0}) * dyn{amp=2}, dyn{cyc=1}, shape) } end
function ASLharmonic(shape) return loop { to((dyn{x=0}:step(dyn{pw=1}):mul(-1):wrap(-3.14,3.14) + 0.101321 * dyn{x=0} * dyn{x=0} * dyn{x=0}) * dyn{amp=2}, dyn{cyc=1}, shape) } end
function bytebeat2(shape) return loop { to(dyn{x=0}:step(dyn{pw2=1}):wrap(-20,20) * dyn{x=0} * dyn{amp=2}, dyn{cyc=1}, shape) } end
function bytebeat3(shape) return loop{to( dyn{x=0}:step(dyn{pw=1}):wrap(-20,20) * dyn{x=0} % dyn{pw2=1} * dyn{amp=2}, dyn{cyc=1}, shape) } end
function bytebeat4(shape) return loop{to( dyn{x=0}:step(dyn{pw=1}):wrap(-20,dyn{bit=10}) * dyn{x=0} % dyn{pw2=1} * dyn{amp=2}, dyn{cyc=1}, shape) } end
function bytebeat5(shape) return loop{to( (dyn{t=0}:step(dyn{pw=0.1}):wrap(0, 10) % 5 + dyn{t=0}:step(dyn{pw2=0.1}):wrap(0, 10) % dyn{bit=1}) * dyn{amp=2}, dyn{cyc=1}, shape) } end