timeline
The timeline
library is all about sequencing events in time. If you find yourself writing out the same clock routines to get a basic rhythm going, there is a better way! timeline
is built on top of clock
, so all the usual details for controlling tempo and clock source apply here as well.
The library has 3 flavors, each with its own purpose, though they can be combined in interesting ways too.
loop
is for (short) clock-synchronized loops in terms of beat durations.score
creates longer form sequences of events for song structure.real
is free from the clock, calling events over time, and is best for effects and real-world time.
All the events can be launched quantized to the clock, and can all be programmatically repeated a number of times.
sections
loop
We’ll start with a simple clock pulse, sent every beat on output 1:
-- timeline version
timeline.loop{1, function() output[1](pulse()) end }
When we call any of the three core timeline
functions, we always use curly braces. We’re passing a table of elements to the loop
function: first a time duration, then a function to be called. Think of the time element as a duration of beats that the event runs over.
For a more useful example, we’ll extend the loop
table to 2 pairs like so:
tl = timeline -- alias timeline for shorter code
-- trigger outputs 1 & 2 alternately, every beat
tl.loop{ 1, function() output[1](pulse()) end
, 1, function() output[2](pulse()) end
}
For clarity, it’s idiomatic to predefine your events where possible:
kick = function() output[1](pulse()) end
snare = function() output[2](pulse()) end
-- now the timeline is a clear description of rhythm & events
tl.loop{ 1, kick
, 1, snare
}
A Note on Timing
If you’re familiar with clock.sync
you might expect the time element above to be a beat-to-synchronize-to, but timeline
works differently. The timing of a loop
is in terms of the beat-durations. This has quite a different feel, but allows you to write out rhythms more directly.
If this doesn’t mean anything to you, then don’t worry! loop
times will act the way you’d expect!
score
Moving beyond a simple loop, we can start to think about longer form structures. For that we’ll use score
, which calls events at a given timestamp relative to when the score was started:
function intro()
print('the intro!')
end
function verse()
print('the verse!')
end
function chorus()
print('the chorus!')
end
function outro()
print('the outro!')
end
timeline.score{
0, intro
, 32, verse
, 64, chorus
, 96, outro
}
score
still holds a table of pairs, like loop
, but the timing values have a subtly different meaning. Each pair will be called when its timestamp (in beats) is reached. As a result, you’ll typically start with an event at 0
, though this is not strictly necessary.
Above, we call intro()
immediately, then verse()
32 beats after intro, the chorus()
another 32 beats after that, and finally our outro()
another 32 beats later.
Note that we must write the sequence in time-order – this makes it far more readable too.
real
…as in “realtime”. This is very similar to score
– we are still writing timestamps relative to when the real
was started. However, timing is described in seconds instead of beats. Tempo changes have no effect here.
This can be useful for things like strumming notes in a chord:
-- define what each note_x function should do!
timeline.real{ 0, note_1, 0.1, note_2, 0.25, note_3 }
…or triggering events at longer intervals in a live performance as a guide for yourself to indicate the passing of time:
timeline.real{600, function() print "you've been playing for 10 minutes!" end
,1200, function() print "20mins! time to wrap it up!" end}
Function Tables
Timeline is all about events in time, right? Up to here, those events have just been function calls: either using inline style, or as named functions to keep things tidy. We can also use function tables to remove the need for so many tiny functions.
We’ll start by modifying the above timeline.real
example to avoid the inline (aka anonymous) functions:
timeline.real{600, {print, "you've been playing for 10 minutes!"}
,1200, {print, "20mins! time to wrap it up!"}}
A function table goes in the event position, where we’d normally put a function. The first element of this table is the function to call, and any following elements are passed to that function as arguments, separated by commas.
Here’s a modified strumming example:
function note(x)
output[1].volts = x/12
end
timeline.real{ 0, {note,5}, 0.1, {note,3}, 0.25, {note,7} }
technical aside: If you’re familiar with a lisp language, this is just standard function application. We apply
the 2nd and higher arguments to the 1st argument. Code is data, huh!
For a more useful example, we’ll borrow from the next section on sequins
and use a new sequins v2 feature. Here, we trigger notes on Just Friends via ii
:
ii.jf.mode(1) -- synthesis mode
timeline.loop{ 1, {ii.jf.play_note, sequins{0,2,4,6,8,10}/12, 2}}
There’s a lot of information in this line, but the key is that we will call the ii.jf.play_note()
function with the following 2 arguments: first, a sequins of pitch values (converted to volts with /12
), then an amplitude value of 2V. read on for more on sequins
…
sequins
-enabled
To enable a more elegant articulation of patterns, timeline
is fully sequins-enabled. The timing values in your time-event table can be provided as sequins
of values, realizing a different value each time that line is executed.
Here’s an example of using sequins
in a loop
to play some swung hi-hats on eighth-notes. Just implement the hats()
function to call whatever triggers a hi-hat sound in your system:
timeline.loop{ sequins{0.55, 0.45}, hats }
…and this example plays a scale of notes on just-friends (via ii
), once per beat:
ii.jf.mode(1) -- synthesis mode
function note(n, v) ii.jf.play_note(n/12, v) end -- a helper function to keep things clear
timeline.loop{ 1, {note, sequins{0,2,4,6,8,10}, 2}}
These examples all use a single time-event pair, but you’re free to use sequins
in a longer sequence – perhaps adding a simple rhythm alteration for your new math rock band!
Pre-Methods
Similar to sequins
, a timeline
can have methods applied to modify the behavior of the main function (loop
, score
, or real
).
However, timeline
differs from sequins
in that it can have methods applied before and after the main function. In this section, we’ll talk about the pre-methods, which are about changing when a timeline will begin.
You can chain these modifications together with the :
character, passing the timeline
object down through each stage of modification.
As of now, there are only two pre-methods: queue
and launch
.
With pre-methods, it’s important to remember that the order matters. You’ll only use queue
or launch
at the beginning, and everything else will follow the main function.
Playback Control Using Pre-Methods
In our previous examples, you’ll notice that a timeline
runs as soon as it’s created. But perhaps you want to prepare a number of timeline
s ahead of time, and only play them at the right moment?
For that we have the queue
pre-method. This is a pre-method because it affects when the timeline
will begin:
snare = function() output[1](pulse()) end
-- create a queued loop and save it into 'mysnare'
mysnare = timeline.queue():loop{2, snare}
-- ^ note that :loop is method-chained with colon
The above creates our loop
-ing timeline
, but since we use the queue
pre-method, crow knows not to auto-run it.
To run the queue
-d timeline
, we’ll use the play
post-method. This is a post method because it affects how the timeline
runs. We can also save the timeline
into a named variable so we can interact with it in the future – this is important if you want to stop the sequence:
> mysnare:play() -- calls the play() method on your timeline object
After some time you may decide a loop has run its course, at which point you can use the stop
post-method:
> mysnare:stop()
Alternate Approach
For an alternative approach to the queue
pre-method, we can just use standard Lua tables and invoke the timeline
function at the right time in a performance. This could be useful if you prefer to have all your descriptions in one place. The point is to remember that the time-event pairs table is just a regular old Lua table – it only becomes fancy when you load it into the timeline function:
kick = function() output[1](pulse()) end
snare = function() output[2](pulse()) end
my_events = {1, kick, 2, snare} -- just a regular Lua table
Invoke this when you want to play the timeline:
> timeline.loop(my_events) -- pass the table to the looper
Launch Quantization
Every timeline
we’ve used so far has been using the standard launch quantization of 1 beat. This means that a timeline
will always wait until the next beat before executing its first element, or before setting the 0
timestamp for a score/real.
If you want to change the default quantization for all timelines you can set the launch_default
variable in timeline:
timeline.launch_default = 4 -- switch to every 4th beat, eg. 1 bar in 4/4 time
But sometimes you may want a specific timeline to use a custom quantization setting. Perhaps you want your score
to be quantized to 16, or you want your real
to start immediately with no delay. For these cases, you can use the launch
pre-method:
timeline.launch(16):score{...} -- forces the score to wait until the next multiple of 16 beats
timeline.launch(0):real{...} -- no quantization! begins the first element of real immediately
Both launch
& queue
can be used together and the order doesn’t matter:
timeline.launch(16):queue():loop{...} -- note the chain of 2 colons
If the syntax feels weird to you, it can be helpful to see it more sequentially. The trick is that we are passing a timeline object through the chain sequentially. Here it is written as a sequence of mutations:
queued_loop = timeline.launch(16) -- create an empty timeline with custom launch quantization
queued_loop = queued_loop:queue() -- applies the queue() modifier to our loop
queued_loop = queued_loop:loop{...} -- and finally load in the loop data
Stop & Go
As you can see in playback control section above, you save your timeline into a variable, you can always stop it with :stop()
. Additionally, whenever you call :play()
the timeline will be restarted at the beginning and will be launch-quantized again.
Panic!
If you need to stop all the running timelines you can run timeline.cleanup()
to stop all running timelines. Beware that this will also stop any running clock
routines, even if they weren’t started by timeline
!
loop
Post-Methods
Note: as of now, only loop
functions can have post-methods applied.
Post-methods determine how a loop
runs, or when it will stop.
By default, loop
will repeat the time-event table endlessly. You can stop the timeline at any moment with the :stop()
method, but you can also make the looping programmatic with the following post-methods:
:unless
:unless(predicate)
takes a “predicate” – a true or false value – or a function that produces one. Whenever the predicate evaluates as true
, the timeline will stop!
If you want a loop
that only runs once, you can apply :unless(true)
to the end of the loop. On the other hand, if the predicate is a function, it will allow you to stop the loop based on the result of the function call. This could be used for probabilistic looping:
timeline.loop{1, kick, 2, snare} -- add white-space before the method chain for readability
:unless(function() math.random() > 0.5 end) -- 50% chance of stopping the loop
Or perhaps you want to loop until a high-signal is detected at the input:
timeline.loop{1, kick, 2, snare}
:unless(function() input[1].volts > 2.0 end) -- stop if input[1] is high
Of course you could call my_timeline:stop()
from the input event, but this approach requires both events to happen at once, and means the timeline will always complete a full cycle.
:times
This post-method is used instead of unless
when you want the timeline to be run a specific number of times. Perhaps your loop
is 4 beats long and you want it to play for 32 beats – just append :times(8)
and it will stop automatically.
timeline.loop{4, random_note}:times(8) -- play 8 random notes, one note every 4 beats
Note: adding a times
post-method will always play the timeline at least once. Passing values to times
that are less than 1 are effectively the same as 1.
score
and real
: "reset"
Keyword
Though the score
and real
event types don’t have any post-methods, they do have one special feature: the "reset"
keyword.
If you have a score
or real
that you would like to repeat endlessly, you can set the last event in your table to be the string "reset"
. This will immediately jump to the beginning of the timeline and start again.
timeline.score{
0, intro
, 32, verse
, 64, 'reset' -- will jump to beat 0, aka intro (single or double quotes ok)
}
Note that you can return the "reset"
string from a function! This means your last event can be a function that returns “reset”, or more interestingly might return “reset”. This updated score has a 50% chance of repeating every time it completes:
timeline.score{
0, intro
, 32, verse
, 64, function() return (math.random() > 0.5) and 'reset' end
}
While these examples imply that the “reset” message has to happen at the end of the timeline
, this is not strictly true! Any event can return the “reset” message and jump to the start of the timeline
. Exploiting this could lead to some very interesting repeating sequences that play different amounts of the whole score.
Wrapping Up
There are a lot of features here to absorb, but timeline
is all about making rhythmic ideas more concise. You’ll likely need many of timeline
’s features only occasionally, so try and build up your ideas slowly. And remember that sprinkling a sequins
or two into your timeline
is a fantastic way to add variation into your patterns!
example: automatic music
This script is featured in the banner video above.
Outputs 1 and 2 are assigned LFOs which are quantized to scales, to create arpeggios. Clocked pulses are sent to output 3, which sync to the pulse_pacing
variable. Using timeline.score
we change pulse_pacing
, set output scaling, and change LFO time / maximum / shape.
In the banner video:
- output 1 is sent to Mangrove PITCH
- output 2 is mult’d to Just Friends TIME + Three Sisters FREQ
- output 3 triggers Just Friends envelopes, which reveal Mangrove SQUARE and FORMANT through a Dual Pass Low Pass Gate
scales = {
intro = { 10, 12, 19, 0, 3, -5, 17, 15 },
verse = { 10, 2, 3, 5, 7 },
chorus = { 10, 14, 3, 5, 7, 2, 12, 10, 0, -5, 17 },
outro = { 10, 14, 15, 3, 7 },
}
function init()
pulse_pacing = 1
pulse_clock = clock.run(function()
while true do
clock.sync(pulse_pacing)
output[3](pulse())
end
end)
end
function intro()
print("the intro!")
pulse_pacing = 1
output[1].scale(scales.intro)
output[2].scale(scales.intro)
output[1](lfo(5, 2, "sine"))
output[2](lfo(3, 3, "logarithmic"))
end
function verse()
print("the verse!")
pulse_pacing = 1 / 4
output[1].scale(scales.verse)
output[2].scale(scales.verse)
output[1](lfo(13, 3, "sine"))
output[2](lfo(5, 2, "rebound"))
end
function chorus()
print("the chorus!")
pulse_pacing = 1 / 5
output[1].scale(scales.chorus)
output[2].scale(scales.chorus)
output[1](lfo(8, 3, "exponential"))
output[2](lfo(7, 1, "logarithmic"))
end
function outro()
print("the outro!")
pulse_pacing = 1 / 12
output[1].scale(scales.outro)
output[2].scale(scales.outro)
output[1](lfo(7, 3, "rebound"))
output[2](lfo(10, 2, "exponential"))
end
function the_end()
print("song done")
output[1](ar(3, 2, 1))
output[2].volts = 0
clock.cancel(pulse_clock)
end
timeline.score({
0,
intro,
64,
verse,
128,
chorus,
155,
outro,
200,
the_end,
})