Timeline sequencing overview
This is just an overview of the dewdrop_lib timeline sequencing framework. I can't claim this is complete documentation, because I have to do this very quickly. Not much time...
Also, note that the framework is still evolving as I work on my first composition with it. So don't get too attached to this API...
Examples appear toward the bottom, and very likely explain as much as the text!
The basic architecture is that Commands are wrapped into a sequence, that is played by a TLSequenceIterator. Further, iterators can be grouped into sections and those sections may be arranged in a higher-level sequence (but I don't have time to document those layers at the moment).
Before using commands, you must load the prototypes.
TLAbstractCommand.loadCmds;
- Commands
Everything happens on a timeline as part of a command. A command (loosely based on the Command design pattern - http://en.wikipedia.org/wiki/Command_pattern) is an object that encapsulates an action.
On a timeline, every command must have a starting point and an ending point. This may be strictly timed by scheduling the command's termination (the total duration of the command goes into the ~dur variable). Alternately, the command can end when its subject ends (if the subject is, for instance, a Pdef or a chucklib bound process BP), or the command can set up a user signal to stop the command upon input from a performer.
Some commands simply do an action and return immediately. In that case, the 'done' signal is sent immediately to the caller.
Commands are implemented as Proto objects from dewdrop_lib. A library of command Protos may be saved in the chucklib PR repository and accessed by name. The file proto-cmds2.scd saves a number of useful commands.
The main purpose of the timeline framework is to control larger scale form by turning musical processes on and off at specific times. Consequently, the following two commands are the most important:
>> pdefCmd
>> bpCmd
As arguments, they take:
- name (required): a Symbol (or array of Symbols) naming the Pdefs or BPs to control
- dur: how long to allow the processes to run; if nil, the processes will run indefinitely until they stop on their own
- quant: an onset time specification, passed to the process's .play method
- waitForStop: optional, an array of process names that will cause the command to stop when all the named processes. This should be a subset of the 'name' array. If this is not given, all the processes listed under 'name' will be included in waitForStop.
Also useful:
>> funcCmd: Execute a function (saved in the 'func' argument).
>> meterCmd: Set the current clock's beatsPerBar, based on ~beatsPerBar.
You can also create and manipulate individual synths using synthCmd and setCmd.
- server (required): Where to send messages.
- name (required): The synthdef name to use.
- id: A specific node ID -- if omitted, one will be assigned automatically.
- latency: Server messaging latency. If positive, the number is used; if 0, nil latency is used. If not supplied, the server's latency takes over.
- target: Same as normal Synth creation.
- addAction: Same as normal Synth creation.
setCmd needs only the argument list. It will search for currently active synthCmds in the same iterator and send new arguments to all of them. The iterator allows you to fork a new iterator thread, which allows groups of synthCmds to be controlled individually.
User input triggers
Duration is relevant for pdefCmd, bpCmd and synthCmd. Duration may also be omitted. In that case, the process or synth may come to an end on its own. The command will recognize this and stop itself. Or, the command may be ended by user input.
setDoneSignal: A function that creates objects to listen for user input. This could be GUI, MIDI, HID, Arduino or any other input method. This is called when starting the command. It must set the environment variable ~doneSignal to any non-nil value. If that variable is nil, the command assumes there is no user input.
clearDoneSignal: A function that removes the user input listener objects. This is called after receiving the signal.
- TLSequenceIterator
A TLSequenceIterator plays a sequence of Commands. Commands are listed in an Array, along with other modifiers. Valid items for the array are:
- A command, which is any object that responds to 'isTLCommand'. The easiest way to specify the command's Proto is by its PR name, written as a symbol into the array. The Proto object itself is also valid - make sure it's an individual copy. Immediately following the command reference, a Dictionary (Event) containing command arguments may be written.
For example, these are all ways to write a command to play the default synthdef for one beat.
// placing arguments into the command object first
[PR(\synthCmd).copy.putAll((name: \default, freq: 440, dur: 1))]
// giving the command object, with an argument Event
[PR(\synthCmd).copy, (name: \default, freq: 440, dur: 1)]
// giving the command name (automatically looked up in PR), with an argument Event
// this is the easiest, cleanest syntax
[synthCmd: (name: \default, freq: 440, dur: 1)]
Try it:
TLAbstractCommand.loadCmds;
TLSequenceIterator([synthCmd: (name: \default, freq: 440, dur: 1)]).play;
- A number, indicating the time to wait before the next command. Normally this is from the onset of the previous command (so that commands can overlap), but \sync and \cmdSync change that.
- \sync, which waits for all active commands to stop before moving on. \sync is implied at the end of every iterator, unless you set autoSync: false when creating the iterator. E.g., TLSequenceIterator([sequence...], autoSync: false). This is because normally an iterator should not release control until everything that it's doing is finished.
- \cmdSync, which pauses the sequence's progress until the previous command finishes. (Other, older commands are allowed to overlap if their durations are long enough.)
- An array, which is like forking a thread in that a sub-iterator is created based on the array and runs alongside the main thread. The sub-iterator becomes another command whose endpoint figures into \sync and \cmdSync blocking.
To be documented:
non-syncable commands
- Notifications
TLSequenceIterator uses NotificationCenter to receive \done from commands
"Listener" is either "stopped"+hash or "cmdSync"+hash
Parents of TLSequenceIterator may use standard dependencies
notifyCmd
- tlSection
- tlSectionSequencer
- Examples
// important: you must load the prototype commands before running any of these examples
TLAbstractCommand.loadCmds;
// the main use is to control processes
// for now this is either Pdef or BP (in chucklib)
// if you have some other process to control, look at pdefCmd and bpCmd
// and model a new command Proto on these
// for simplicity only pdefCmd is used in this document
// trivial case: one pdef, playing for a specific amount of time
Pdef(\tune, Pbind(
\instrument, \default,
\degree, Pstutter(2, Pseq(#[0, 7, 4, 3, 1, 7, -2, 2], inf)),
\dur, 0.25,
\legato, 0.72
));
// I'll also include a funcCmd so you can see when the command is over
a = TLSequenceIterator([
funcCmd: (func: { z = thisThread.clock.beats }),
pdefCmd: (name: \tune, dur: 4),
\sync, // wait for pdefCmd to finish before moving on
funcCmd: (func: { [z, thisThread.clock.beats, thisThread.clock.beats - z].debug(">>>> ending iterator now") })
]).play;
// changing the command's duration makes it play longer or shorter
// sync automatically picks up the changed timing
a = TLSequenceIterator([
pdefCmd: (name: \tune, dur: 8),
\sync, // wait for pdefCmd to finish before moving on
funcCmd: (func: { ">>>> ending iterator now".postln })
]).play;
// make the pattern finite - it will stop on its own after 2 cycles
Pdef(\tune8bts, Pbind(
\instrument, \default,
\degree, Pstutter(2, Pseq(#[0, 7, 4, 3, 1, 7, -2, 2], 2)),
\dur, 0.25,
\legato, 0.72
));
// the command has no dur parameter, so it will run until the pattern stops
a = TLSequenceIterator([
pdefCmd: (name: \tune8bts),
\sync, // wait for pdefCmd to finish before moving on
funcCmd: (func: { ">>>> ending iterator now".postln })
]).play;
// stop by user signal
a = TLSequenceIterator([
pdefCmd: (name: \tune, // back to infinite Pdef
setDoneSignal: {
~doneSignal = true; // tell the cmd there is now a user signal
defer(e {
~window = GUI.window.new("signal", Rect(10, 300, 100, 50));
GUI.button.new(~window, Rect(5, 5, 90, 30))
.font_(GUI.font.new("Helvetica", 20))
.states_(#[["stop"]])
.action_(e {
~done.value;
});
~window.front;
})
},
clearDoneSignal: {
defer(e { ~window.close })
}
),
\sync, // wait for pdefCmd to finish before moving on
funcCmd: (func: { ">>>> ending iterator now".postln })
]).play;
// use wait time to start the tune again after some time
a = TLSequenceIterator([
pdefCmd: (name: \tune, dur: 4),
4.5, pdefCmd: (name: \tune, dur: 4)
]).play;
// how about a contrapuntal answer?
Pdef(\tune, Pbind(
\instrument, \default,
\degree, Pstutter(2, Pseq(#[0, 7, 4, 3, 1, 7, -2, 2], inf)),
\dur, 0.25,
\legato, 0.72,
\pan, -0.6
));
Pdef(\reply, Pbind(
\degree, Pseq(#[0, 1, 2, 1, 3, 2, 5, 6, 7, 1, 5, 4, 3, 2], 1),
\dur, Pseq(#[1, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 1, 2, 2], 1) * 0.25,
\legato, 1.0,
\pan, 0.6
));
a = TLSequenceIterator([
pdefCmd: (quant: 0, name: \tune, dur: 4),
4,
pdefCmd: (quant: 0, name: \reply, dur: 5),
0.5,
pdefCmd: (quant: 0, name: \tune, dur: 4)
]).play;
a = TLSequenceIterator([
pdefCmd: (quant: 0, name: \tune, dur: 4),
\sync,
pdefCmd: (quant: 0, name: \reply, dur: 5),
0.5,
pdefCmd: (quant: 0, name: \tune, dur: 4)
]).play;
// using synthCmd to play a melody
// this is not the most efficient way to write a tune!
// but TLSequenceIterator is meant for higher level, not note-by-note control
(
a = TLSequenceIterator([
synthCmd: (freq: 60.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 72.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 67.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 65.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 62.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 72.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 57.midicps, dur: 0.36, name: \default),
0.5,
synthCmd: (freq: 64.midicps, dur: 0.36, name: \default),
0.5
]).play;
)
// same, using setCmd for legato
(
a = TLSequenceIterator([
synthCmd: (freq: 60.midicps, dur: 4, name: \default),
0.5,
setCmd: (freq: 72.midicps),
0.5,
setCmd: (freq: 67.midicps),
0.5,
setCmd: (freq: 65.midicps),
0.5,
setCmd: (freq: 62.midicps),
0.5,
setCmd: (freq: 72.midicps),
0.5,
setCmd: (freq: 57.midicps),
0.5,
setCmd: (freq: 64.midicps),
0.5
]).play;
)
// example of forking a sub-iterator to control multiple synthCmds independently
// setCmd applies to all active synthCmds in the current iterator
// here we have three notes
// by forking each one into a sub-iterator, setCmd knows which one to control
a = TLSequenceIterator([
// since there is no wait time between the sub-arrays,
// they run in parallel
[ synthCmd: (name: \default, freq: 60.midicps, dur: 2),
0.75, setCmd: (freq: 72.midicps),
0.5, setCmd: (freq: 60.midicps)
],
[ synthCmd: (name: \default, freq: 63.midicps, dur: 2),
0.75, setCmd: (freq: 76.midicps), // yes, a very naughty E-natural
0.5, setCmd: (freq: 63.midicps)
],
[ synthCmd: (name: \default, freq: 67.midicps, dur: 2),
0.75, setCmd: (freq: 79.midicps),
0.5, setCmd: (freq: 67.midicps)
]
]).play;
a = TLSequenceIterator([
// adding wait time to stagger
[ synthCmd: (name: \default, freq: 60.midicps, dur: 2),
0.75, setCmd: (freq: 72.midicps),
0.5, setCmd: (freq: 60.midicps)
],
0.375,
[ synthCmd: (name: \default, freq: 63.midicps, dur: 2),
0.75, setCmd: (freq: 76.midicps),
0.5, setCmd: (freq: 63.midicps)
],
0.375,
[ synthCmd: (name: \default, freq: 67.midicps, dur: 2),
0.75, setCmd: (freq: 79.midicps),
0.5, setCmd: (freq: 67.midicps)
]
]).play;
// now, more practical use
// play the tune with Pdef, then play it again later
Pdef(\tune, Pbind(
\instrument, \default,
\degree, Pseq(#[0, 7, 4, 3, 1, 7, -2, 2], 1),
\dur, 0.5,
\sustain, 0.36
));
a = TLSequenceIterator([
pdefCmd: (name: \tune),
4.5, pdefCmd: (name: \tune)
]).play;
// melody with contrapuntal answer