LuaAV Timing & Scheduling Tutorial

LuaAV has a powerful timing system, using on an internal scheduler that preserves deterministic ordering and logical timestamps to nanosecond accuracy. The deterministic ordering and accuracy is preserved in many messages to the audio system, such as adding/removing synths.

The main scheduler follows the cpu clock as closely as possible, usually within and accuracy of around 10 milliseconds; however when slow functions are called (such as loading files and creating complex resources such as windows), the scheduler may experience a temporary drop-out, from which it will attempt to recover as soon as possible.

The Lua language itself does not have means to control time, however this has been added in LuaAV via the LuaAV.time module. Several of the functions in this module are so useful that they are pre-loaded as globals in every script:

  1. now()
  2. wait()
  3. go()
  4. event()

Printing out now() in a new script will return the logical time (in seconds) since the script was loaded. Until we start scheduling with time, all script actions occur immediately, so now() will return 0.

The wait() function allows us to pause the execution of the script for a number of seconds, after which it will continue. The following script will print out the current logical time every 0.1 seconds (100 ms):

local period = 0.1
while true do
	print(now())
	wait(period)
end

This is ok, but there's nothing we can do in between each wait, so this kind of control over time is still quite minor.

Fortunately, Lua provides a way to create parallelism within a script, using a coroutines. One way of thinking about a coroutines is that it is like a parallel function or script state; another way to think about it is as a function that can be paused in mid execution, while Lua goes off to execute some other code, and to later returned to (resume) at the point at which it paused (yielded).

LuaAV adds more power to coroutines by connecting them with the scheduler. A convenience function go() will take a function and arguments, create a coroutine based on that function and arguments, and schedule this coroutine within the scheduler. Now we can create many parallel copies of the same function that can be scheduled alongside each other, each with potentially distinct timing, but without losing deterministic accuracy:

function clockprinter(name, period) 
	while true do
		print(now(), name) 
		wait(period)
	end
end

go(clockprinter, "T O C K!", 4) 
go(clockprinter, "...tick...", 1)

The go() function can also take an optional first argument (delay in seconds), which allows us to schedule it to occur at some point in the future:

 go(2, clockprinter, "...tick...", 1) -- will start 2 seconds later

Note that the even if the delay is 0, or is not given, the coroutine will not run immediately; go() simply adds the coroutine to the internal scheduler. (Lua is single-threaded by design, which means that only one actual function is executing at any time.) The scheduled function will not begin running until the context in which it was launched using go() yields with a wait() command, or reaches the end of the script.

Scheduling with events

Sometimes we want to schedule activity to occur not at a given time, but when a given situation occurs. To support this, the go() and wait() functions can also take a string argument in place of a duration. The string represents a unique event.

The event() function can then be used to resume ALL coroutines that were scheduled against or waiting upon a particular event. A classic use-case of this is to make sure that OpenGL rendering commands only execute during a window's draw() method. The following example shows how a function drawstuff() is scheduled to execute only when the draw event occurs, and once it does, to wait until subsequent draw events to continue rendering:

local gl = require("opengl")
local GL = gl

win = Window("test")


function drawstuff()
	while true do
		gl.Begin(gl.LINES)
			gl.Vertex(-math.cos(now()), math.sin(now()), 0)
			gl.Vertex(0, 0, 0)
		gl.End()
		wait("draw")
	end
end


go("draw", drawstuff)


function win:draw()
	-- resume any functions waiting on the draw event
	event("draw")
	
end

The event() function can also take additional arguments; these arguments are returned by any corresponding wait(). Using this feature quite powerful event-based programming systems are possible.

Script-controlled schedulers

Users can create their own schedulers, which are independent of CPU clock, using the LuaAV.time.scheduler() function. A scheduler is an object that provides its own scheduler.now(), scheduler.wait() and scheduler.go() functions. The logical time of this user-controlled scheduler is not tied to the CPU clock, but instead is advanced within the script using the scheduler.advance() or scheduler.update() functions. For more information, see the LuaAV.time module in the reference documentation.

Timing and the OpenGL Window

Callbacks to draw the OpenGL window might not be scheduled with this tight accuracy, since they are dependent on timing factors outside their control (such as graphics drivers and GPU load). However, they will occur as closely as possible to the rate specified by the win.fps attribute.