Lua~

Lua~ is a Max/MSP external embedding an extension to the Lua programming language for computer music composition (the Vessel library), supporting sample accurate interleaving of synthesis and functional control.

Overview

A Quick Tutorial
  Getting Started
  Getting messages out of lua~
  Coroutines
  Making sound
  Interaction

Examples
  Polyrhythm
  Notelist
  Pulsetrain 'frying pan'

Reference
  Arguments
  Attributes
  Messages
  Max functions
  Coroutine functions
  DSP functions

Download

Publications

Overview

The rich new terrains offered by computer music invite the exploration of new techniques to compose within them. The computational nature of the medium has suggested algorithmic approaches to composition in the form of generative musical structure at the note level and above, and audio signal processing at the level of individual samples. The region between these levels, the domain of microsound, holds special interest due to the potential of sonic events to finely interrelate both signal processing and generative structure. Satisfying this demand poses a challenge for both the (outside-time) representation and (in-time) rendering of computer music compositions. The 'Vessel' extension library for Lua was written for the exploration of such potential, supporting dynamic yet deterministic interleaving of both signal processing and structural control with up to sample accuracy. For representation, it comprises an interpreted music programming language (a variation to the Lua programming language with extensions for event, control and synthesis articulation), while for rendering, it comprises a deterministic, dynamic, lazy scheduling algorithm for both concurrent control logic and signal processing graphs.

This page describes lua~, a Max/MSP external embedding the Vessel library. A lua~ object embedded in a Max patch can load and interpret Lua scripts that make use of these extended capabilities in order to receive, transform and produce MSP signals and Max messages accordingly. Lua~ is particularly well suited to granular synthesis, algorithmic microsound and accurate timing needs, helpfully circumventing some of the limitations of the Max/MSP environment for such work by supporting highly dynamic signal processing graphs in parallel processes according to timing specifications below block rate. Using an interpreted scripting language within a graphical programming environment such as Max offers advantages of control flow, generality of data and structure, high precision and control, complexity of data and functional interdependency and variable scoping.

The choice of the Lua language was based principally on the following factors:

Vessel extends Lua in two principal components, both evaluated under the control of a sample-accurate scheduler:

In contrast to languages such as Csound and SuperCollider 3, the description of sound synthesis and control flow are not separated into distinct realms. Vessel is closer in spirit to SuperCollider 2 and particularly the strongly timed nature of ChucK, and similarly offers support for arbitrary concurrent processing. Concurrency in Vessel is built upon Lua's powerful coroutine capabilites, and generally follows the 'Lua way' of providing simple mechanisms rather than prescribing high level models. A coroutine represents an independent thread of execution for deterministic scheduling (also known as collaborative multi-tasking). In Vessel, such coroutines are extended to be aware of the sample-clock, with a small number of additional functions to interact with the scheduler. Vessel thus permits a sample accurate articulation of the composition that may be dynamically deterministic. State changes that involve interpreted code to generate new signal graphs may occur sub-millisecond rates, ideal for generative microsound.

The majority of scheduling and signal processing code is written in C++ for efficiency. To achieve sample accuracy, the lua~ interpreter necessarily runs in the high propriety audio OS thread, but the cost of interpreted code is minimized by only calling into Lua for the scheduled state change actions. The Lua memory allocator and garbage collector is optimized for real-time, and free-list memory pools are used for audio buffers and coroutines to avoid unbounded memory allocation calls.

See also the jit.gl.lua object by Wesley Smith, providing bindings of Lua to Jitter, OpenGL, ODE and more.

A Quick Tutorial

Getting Started

First, follow these instructions to download and install lua~.

In a new Max/MSP patch, create a lua~ object, and double-click on it to open the text editor (when Max is not in edit mode). Enter code as per the examples below, and close the window to save the script as a .lua file in the same folder as your Max/MSP patch. Alternatively, use an external text editor to edit the lua file, and set attribute @autowatch 1 and @file to be the name of your .lua file - then changes you make to the lua script will update in Max automatically. Some external editors offer syntax highlighting for Lua code, which can be very helpful. Mac users have recommended Smultron, TextMate, Xcode (lua addon), bbedit (lua addon), vim/emacs. Windows users TBC.

Make sure to enable audio - lua~ scripts (even those that have no audio processing) will not function if Max/MSP does not have DSP enabled.

For more information about the Lua language, please check www.lua.org, and the excellent Programming in Lua book.

Getting messages out of lua~

-- this is a comment in Lua
print("hello") 	-- goes to the Max window
outlet("world") 	-- goes to the right outlet of lua~
print("finished after " .. now() .. " seconds")

That was fast. So, making it a little more interesting:

print("hello")
wait(1) 			-- wait here for 1 second (oh, the tension!)
print("world")
print("finished after " .. now() .. " seconds")

What's going on here? Is the program spinning cycles away for that whole second? Not exactly. Wait suspends execution of the program, while informing the Vessel scheduler to reawaken it in one second's time. Because of the way Vessel was designed, this is sample accurate. For example, at 44.1kHz, the call wait(1/44100) will pause execution for 1 sample. Though this is obviously too fast for messaging calls such as outlet() and print(), wait periods of handfuls of samples can be very useful for the algorithmic control of signal processing.

Coroutines

Of course, doing nothing while waiting is boring. In Vessel, we can schedule multiple simultaneous processes of computation using a slightly enhanced form of Lua coroutines. It's quite simple:

go(print, "hello") 		-- create and immediately schedule a coroutine based on myfunc
go(1, print, "world") 	-- create and schedule another, to run 1 second later
print("finished after " .. now() .. " seconds")

What's important to realize is that by the end of the script, no waiting was involved at all. The two new coroutines created (using the print function) were placed in the scheduler, at times 0.0 and 1.0, as soon as the script loaded; that's why it finished after 0 seconds, before either hello or world could print. Basically, coroutines give us a deterministic mode of parallel execution; though our example doesn't really make use of it since the coroutines themselves have zero duration. More typically we would write our own function as the body of the coroutine (rather than using a built-in function such as print), and thus we can incorporate wait() calls inside this function to create a process that is spread over time:

function clock(dur, msg)	-- function definition (will be used as a coroutine body)
	while now() < 4 do 		-- repeat until 4 seconds have passed (now() is relative)
		wait(dur)			-- concurrent coroutines may have different dur and msg variables
		print(msg)
	end
end

go(clock, 1, "tock")		-- 'tock' every second
go(clock, 0.25, "tick")	-- 'tick' four times per second

Furthermore, coroutines can launch other coroutines, and a complex script might trigger many hundreds of coroutines per second, based upon many different functions, each with its own independent timeline, arguments and local variables.

Making sound

Printing messages is nice, but making sounds is more fun. To get sound out of Lua~, we need to connect a unit generator to the outlets. In Vessel, the output signals are represented by the global variable Out, which is a kind of Bus. So, for example:

-- WARNING: turn down your audio before running this script!
local n = Noise() -- create a Noise unit generator
Out:add(n) -- add it to the global output bus
wait(1) -- let time pass as we enjoy the noise
Out:remove(n) -- that was enough noise thanks

Since this combination of calls is very common, a shorthand form exists (the play function):

local n = Noise()			-- create a Noise unit generator
play(Out, 1, n) -- play n into Out for 1 second

We created our unit generator of the Noise type, using its constructor function. Other unit generator types have constructors that can take different arguments, such as frequency for oscillators. Some of these arguments can be other unit generators, to create complex signal graphs (though a constant number can always be used in place of a unit generator). Furthermore, we can use arithmetic operators (+, -, *, / %, ^) on unit generators:

play(Out, 0.4, Sine(440))	-- we can create the unit generator 'in place'
play(Out, 0.4, Sine(330)) -- first arg to Sine is frequency (Hz)
play(Out, 0.4, Sine(330) + Sine(440)) -- we can use arithmetic on unit generators' outputs
play(Out, 0.4, Sine(Sine(10) * 330 + 440)) -- Simple FM synthesis

Look here to see a list of unit generators currently available.

Interaction

So far our scripts run entirely according to the values set in the script when they load; but running scripts can also be controlled externally using Max messages. The principal method is [call], which looks for a global named function in the lua script and runs it. For example, sending the message [call beep] to this script will, as you might expect, beep in response:

function beep()	-- responds to Max message [call beep]
	play(Out, 0.1, Sine(330) * Decay(0.1))
end

Note that the message actually causes a coroutine to be created and scheduled based on the named function, so many beeps may overlap. Similarly, additional values in the Max message are passed to the function, and the function can use any coroutine methods such as now() and wait():

-- responds to Max message [call beep repeats], where repeats is a whole number > 0
function beeps(repeats)	
for i = repeats, 1, -1 do -- run for dur seconds: play(Out, 0.1, Sine(330) * Decay(0.1) * (i/repeats)) -- echo fx end end

In addition to calling named functions, we can also send a string of Lua code to run as a new coroutine function, with the [dostring] message. Furthermore we can access global variables (variables not declared as local) using the [get] and [set] messages.

Examples

Some simple examples are given here for reference, but do check out the more extended examples in the download archive.

Polyrhythm

This simple example demonstrates the layering of concurrent processes. Note that both processes are instantiated from the same function template, but with distinct arguments:

-- simple percussive repeater as coroutine template:
function pattern(stepdur, freq, p)
	while true do
  		-- create a DSP graph:
  		local f = Sine(freq, 0)
  		local ugen = Pan(Sine(freq, 0) * Decay(0.2) * 0.5, p)
 		-- play for one step, then pause for one step:
		play(Out, stepdur, ugen)
		print(f:freq():current())
  		wait(stepdur)
	end
end

-- launch coroutine immediately,
-- at 1/6s step size, 440Hz, pan right:
go(pattern, 1/4, 440, 0.5)

-- launch coroutine after 2 seconds,
-- at 1/4s step size, 330Hz, pan left:
go(2, pattern, 1/6, 330, -0.5)

Notelist

The following code fragment defines a coroutine process to progressively iterate a note list table and interpret its data as a sequence of notes to synthesize using a Sine oscillator:

-- a simple sequence player:
local player = function(notelist)
	for i = 1, #notelist do
		local event = notelist[i] 
		play(Out, event.dur, Sine(event.freq)) 
	end
end

-- a minimal sequence:
local triplet = {
	{ freq = 440, dur = 0.5 }, 
	{ freq = 880, dur = 0.25 }, 
	{ freq = 660, dur = 0.25 }
}

-- play the sequence concurrently:
go(player, triplet)

This is minimally equivalent to the orchestra-score model of Csound et al., yet can be endlessly extended with functional and concurrent programming. For example, the table of event parameter sets could just as easily contain functions or other coroutines in place of numbers. A library of complex and generative pattern streams can be designed using tables, functions and coroutines, according to the composer or programmer’s discretion.

Pulsetrain 'frying pan'

This example demonstrates nested coroutines; a series of pulsetrain coroutines are scheduled, each of which schedules a coroutine for each individual pulse-grain. Functional control logic and math functions determine the properties and temporal progressions of pulse parameters in each train.

-- play a panned single-cycle waveform:
function grain(dur, amp)
	local s = Sine(1/dur, 0) * amp
	local graph = Pan(s, s * (math.random() - 0.5))
	play(Out, dur, graph)
end


-- an algorithmic stream of pulses:
function pulsetrain()
	local duration = 0.005 / math.random(50) -- 0.1 to 5ms
	local pulsewidth = 0.02 / math.random(20) -- 1 to 20ms
	local fade = 0.8 + (math.random() * 0.199) -- 70 to 99%
	-- periodic counter:
	local step = 1	
	local limit = math.random(12)
	-- keep playing grains until very quiet:
	local amp = 1
	while amp > 0.01 do
		go(grain, duration / step, amp) -- schedule pulse
		wait(pulsewidth * step) 	-- pause for pulse width
		amp = amp * fade 			-- decrease amplitude
		step = (step % limit) + 1 	-- interate counter
	end
end  

-- schedule pulsetrains
while true do
	-- launch a new train evern 10-100ms:
	go(pulsetrain)
	wait(0.01 + (math.random() * 0.09))
end 

Reference

For reference about the Lua 5.1 language, please visit www.lua.org, in particular www.lua.org/manual/5.1

Lua~ external

autosave
autowatch
bang
call
dostring
enable
file
fontsize
get
info
open
reset
set
settext
write

Vessel extensions to Lua

Bus
go
In
now
Out
outlet
play
print
wait

Vessel unit generators (beta)

Abs
Biquad
Ceil
Clip
ClipB
Curve
Decay
Delay
Dsf
Env
Floor
Fold
Gt
Imp
Lt
Max
Mean
Min
Noise
Pan
Pink
Reverb
Round
Saw
Sine
Smooth
Square
Tri
Wrap

Instantiation arguments

Lua~ takes two integer arguments to determine the number of signal inlets and outlets respectively (represented in the script as In and Out). Further attribute arguments may be specified (see below).

Lua~ Attributes

As usual, attributes can be specified as instantiation arguments (after the integer args for inlets and outlets), and also be called as methods. For example, the Max message [file] will prompt the user to load a new script file.

@autosave int
Argument specifies whether to save changes automatically on closing the editor window. If disabled, a 'Save changes' prompt will show instead. Default off.

@autowatch int
When set to 1, saving changes to the script file (from a different text editor) will trigger Lua~ to automatically reload the script. Default off.

@enable int
Argument is zero or one; disables or enables processing (default 1).

@file filename
Argument filename is he name of the script file to load, and should be within the same folder as the patcher, or in the Max search path. No default. When sent as a message with no filename argument, the user will be prompted with an open file dialog.

@fontsize int
Sets the font size for the editor window. Default 13.

Lua~ Messages

bang
Will call the global bang() function in the script, if defined.

call funcname [args...]
Will call the global function named funcname (if defined in the script), passing extra arguments to the function. For example, message [call print "hello world"] will call print("hello world") in the script context.

dostring codestring
Will interpret the Lua code codestring in the script context. For example, message [dostring "print(2+2)"] will run the Lua code print(2+2).

get varname
Will retrieve the value of the global Lua variable varname (if defined) and send it out of the right outlet.

info
Prints to the Max window information about Lua~'s status (the Vessel engine), including memory usage, current time etc.

open
Opens the text editor window to edit the Lua script. Double-clicking on the Lua~ external achieves the same effect.

reset
Stops processing of the current script: aborts all active coroutines, removes all unit generators and resets the global state. Loading a new file via the file method/attribute also triggers a reset first.

set varname newval
Changes the value of the global variable varname (if defined) to newval.

settext codestring
Replaces the internal script with the Lua code in codestring, and runs this code immediately.

write [filename]
Saves the current script to the file specified by filename. If filename is omitted, a 'Save as' dialog will prompt instead.

Vessel Reference

Vessel is an extension of the Lua language (5.1) for use within audio signal processes.

Lua functions for Max/MSP

print(args...)
The global print function sends descriptive strings of the variables in args to the Max window.

outlet(args...)
The values of the variables in args will be sent from the right outlet of Lua~.

Vessel coroutines

now()
Returns the time in seconds since the enclosing coroutine (or the main script if global) was started.

go([delay], funcname, [args...])
go([delay], codestring)
Creates and schedules a new coroutine based upon the function defined as funcname, and returns that coroutine. If argument delay is omitted, the coroutine will begin immediately; else it will begin after delay seconds have elapsed. Any extra arguments args are passed to the function funcname when the coroutine begins. Alternatively, passing a string of code in place of a function + arguments interprets the codestring as the function body of the coroutine. For example:

go(print, "hello")       -- call print("hello") immediately
go(0.1, print, "world")  -- call print("world") after 0.1 seconds
go(0.2, "print('bye')")  -- call print("bye") after 0.2 seconds 

wait(delay)
Pauses execution of the enclosing coroutine (or main script) for delay seconds. If omitted, delay defaults to 1.

Vessel DSP

Unfortunately, the current MSP SDK does not support the dynamic creation and connection of signal graphs programmatically within an external. For these reasons, a set of elementary unit generators have been included in the Vessel language, based upon Lance Putnam's C++ DSP library Synz. Please note that this library is very beta, and as such the API is subject to change. Other signal processing libraries may be included at a future date.

Unit generators are Lua objects that encapsulate C/C++ unit generator DSP code. Different types of unit generators are created using constructor functions, which can also take constant numbers for any unit generator parameters. Unit generator instances may provide Lua methods for instantaneous state changes. Many unit generators can expand to multi-channel upon demand, and individual channels can be indexed with the unit[n] notation, where n is an integer starting from 1 (total number of channels is returned by #unit). Units can be composed into graphs via their inputs, by using Busses (see below), or by using math operators (+, -, *, /, %, ^).

Bus()
Busses are special kinds of unit generators nto which other unit generators can write. Busses therefore allow arbitrary signal mixing, efficient effects chains, and graph cycles. Busses add the bus:add(unit) and bus:remove(unit) methods to add or remove unit generators from a Bus.

In
A global read-only Bus representing the signal inlets.

Out
A global Bus representing the signal outlets.

play(bus, duration, ugen)
Plays the unit generator ugen into the Bus bus for duration seconds. Equivalent to:

bus:add(ugen)
wait(duration)
bus:remove(ugen)

Oscillators:

Sine([freq, phase]), Square([freq, phase]), Tri([freq, phase]), Saw([freq, phase])
Bandlimited oscillators. Frequency in Hz (can be a unit generator), phase from 0..1.

oscillator:freq([ugen])
Set the frequency input. If omitted, returns the current frequency input.

oscillator:phase([phase])
Set (and return) the current phase. If phase is omitted, sets phase to zero.

Imp([freq, harmonics, polarity])
Impulse generator. Frequency in Hz (can be a unit generator), harmonics is an integer, and polarity (boolean) indicates bipolar or unipolar.

imp:freq([ugen])
Set the frequency input. If omitted, returns the current frequency input.

imp:phase([phase])
Set (and return) the current phase. If phase is omitted, sets phase to zero.

imp:polarity([bool])
Set (and return) the current polarity. If phase is omitted, returns current value.

Dsf([freq, freqratio, ampratio, harmonics])
Oscillator based upon discrete summation formula. Base frequency in Hz (can be a unit generator), freqatio specifies the frequency ratio between successive harmonics, ampratio the amplitude ratio between successive harmonics, and harmonics the number of harmonics (can be a float).

dsf:freq([ugen])
Set the frequency input. If omitted, returns the current frequency input.

dsf:phase([phase])
Set (and return) the current phase. If phase is omitted, sets phase to zero.

dsf:harmonics([float]), dsf:freqratio([float]), dsf:ampratio([float])
Set (and return) the current value of harmonics, freqratio or ampratio. If omitted, returns current value.

Noise(), Pink()
Creates white and pink noise generators respectively.

Envelopes:

Env(duration, input, [shape])
Shapes the unit generator input with an envelope of duration seconds, and shape chosen from one of:
Env.blackman (default), Env.blackmanHarris, Env.hamming, Env.hann, Env.triangle, Env.welch, Env.nyquist, Env.rectangle

env:input([ugen])
Set the input. If omitted, returns the current input.

env:reset()
Restarts the envelope at zero phase.


Decay([t60])
Creates a decaying envelope that will decay from zero to -60db after t60 seconds.

decay:reset()
Restarts the curve at zero phase.

Curve([duration, curve, start, end])
Creates a control curve beginning at start (default 1) and reaching end (default 0) after duration seconds (default 1), with a curvature determined by curve (default 0 = linear).

curve:reset()
Restarts the curve at zero phase.

Filters:

Smooth(input, [factor])
A simple smoothing filter. Higher values of factor result in more smoothing (default 0.5).

filt:input([ugen])
Set the input. If omitted, returns the current input.

filt:factor([float])
Sets the smoothing factor.

Biquad(input, [freq, resonance, mode])
A classic two-pole two-zero filter. Freqeuncy (Hz) and resonance can both be set as unit generators or constants. Mode can be 'lp', 'hp', 'bp', 'bpc', 'br' or 'ap' (default lowpass).

filt:input([ugen])
Set the input. If omitted, returns the current input.

filt:freq([ugen])
Set the frequency input. If omitted, returns the current frequency input.

filt:res([ugen])
Set the resonance input. If omitted, returns the current resonance input.

filt:model([string])
Sets the filter type.

filt:clear()
Clears the filter's previous inputs.

Delay(input, [maxdelay, delay])
A simple delay line; maxdelay and delay are in seconds. If delay is omitted, it defaults to maxdelay. If maxdelay is omitted, it defaults to one second. A generally useful technique is to create a Bus as the delay input, in order to use the Delay on multiple, possibly momentary sources.

delay:input([ugen])
Set the input. If omitted, returns the current input.

delay:delay([length])
Set the delay length in seconds.

Spatializers:

Pan(input, [pan])
Stereo panning of input. Pan ranges from -1 to 1 for left to right, and may be a unit generator.

pan:pan([ugen])
Set the pan input. If omitted, returns the current pan input.

Reverb({parameters})
Reverberation based upon the Gigaverb model. Reverb is a type of Bus, and this inherits the :add() and :remove() methods, rather than having a single unit generator input. Parameters are sent as a Lua table, mapping parameter name to float value, including any of the following parameters:

roomsize (default 100)
maxroomsize (roomsize cannot be dynamically set greater than maxroomsize; default 100 or roomsize)
length (tail decay in seconds, default 8)
spread (default 15)
damping (default 0.65)
bandwidth (default 0.75)
early (early reflection mix, default 1)
tail (reverb tail mix, default 1)
dry (dry input mix, default 1)

For example:

local rev = Reverb({ roomsize = 25, length = 2, early = 0.7, dry = 0 })
rev:add(Noise() * 0.1)

rev:roomsize(float), rev:length(float), rev:damping(float), rev:bandwidth(float), rev:dry(float), rev:early(float), rev:tail(float)
Set these modifiable reverb parameters.

Math:

Remember that the math operators +, -, *, /, % (modulo or division remainder), ^ (raise to the power) can be applied to unit generators. For example, Sine(Sine(1) * 500 + 1000) returns an FM oscillator sweeping from 500 to 1500 Hz and back every second.

Round(input), Floor(input), Ceil(input), Abs(input), -input
Round, round down, round up, remove sign and flip sign unary operators.

op:input([ugen])
Set the input. If omitted, returns the current input.

Min(input, operand), Max(input, operand), Mean(input, operand), Lt(input, operand), Gt(input, operand)
Note: Lt (less than) and Gt (greater than) return normalized zero or one signal values.

op:input([ugen]), op:operand([ugen])
Set the operand inputs. If omitted, returns the current input.

Clip(input, min, max), ClipB(input, min, max), Wrap(input, min, max), Fold(input, min, max)
Note: ClipB keeps the magnitude between a & b, but preserves the input sign.

op:input([ugen]), op:min([ugen]), op:max([ugen])
Set the operand inputs. If omitted, returns the current input.

Download & Install

Please note that lua~ and Vessel are to be considered beta at this stage. Please send any feedback, crash reports or suggestions to lists at grahamwakefield dot net.

MacOSX (universal binary):

Download and unzip the lua~.zip from here.
Copy the lua~.mxo external to /Applications/MaxMSP 4.6/Cycling '74/externals/lua~/
Copy everything else to /Applications/MaxMSP 4.6/max-help/lua~/

Windows:

(coming soon I hope)

Source files

Publications

G. Wakefield, W. Smith, 2007: "Using Lua for Multimedia Composition", Proceedings of the International Computer Music Conference 2007, International Computer Music Association.

W. Smith, G. Wakefield, 2007: "Realtime Multimedia Composition using Lua", Proceedings of the Digital Art Weeks 2007, ETH Zurich, Switzerland.

G. Wakefield, 2007: "Vessel: A Platform for Computer Music Composition, Interleaving Sample-Accurate Synthesis and Control", MS Thesis, Media Arts & Technology program, University of California Santa Barbara, USA.