Event Lists and Sequencer
In our previous post, I introduced litePlay.js
and its basic
principles of operation. We emphasised the idea of lite coding, as
opposed to the more arcane and complex practices of live coding.
Although based on simple concepts, it is my intention that the
system should allow enough scope for musical expression within
various forms of music making.
Within that post, we spoke about the concept of events, which are
the bricks with which we can construct a composition. Such events
are defined by five attributes, what
, howLoud
, when
, howLong
,
and instr
. The what
largely depends on the instr
used for
the event. For example, an instrument modelled after a piano may
ask for pitches, while another modelled after percussion requires
the definition of a sound to play. This in fact is fairly open and
can be expanded to instruments that are not modelled after
conventional ones such as these.
The other attributes are more straightforward as far as their
definition is concerned. How loud an event is defines its intensity
in a scale from 0 to 1. We also should determine the timings
of the events and for how they last; both of these are given in
beats. These timings can be translated to wall clock times in
seconds via the global beats per minute (BPM) setting (getBPM()
gives the current BPM, which is set to 60 at the start).
Event Lists
We can now introduce the notion of lists of events. The idea is
not completely new, as we have seen that the .play()
interface
can handle multiple events, and these are effectively lists of events.
More generally, litePlay.js
has an event list format, which is
nothing more than an array of events, and an eventList
object
that encapsulates this,
list = lp.eventList.create()
We can then add events to the list, as in
list.add([Bb5, 0.2, 0, 1, lp.piano], [Eb5, 0.4, 0.2, 2, lp.piano])
and of course, there’s a .play()
interface as we should expect
list.play()
In this case, the .play()
can take as a first argument time offset
so the eventList can be scheduled to play in the future.
We can clear the list (.clear()
), remove elements
(.remove(index)
), or insert (.insert(ndx, ...events)
). The
eventList create method can also be passed a series of events as
parameters.
Abbreviated Events
As we noted before, events can be abbreviated by passing only a few attributes. The form of this is
[what [,howLoud [,when [,howLong [,instr]]]]]
each instrument has a howLoud
and howLong
default,
which can be set by the user. The default instrument can also
be set by the instrument(instr)
function. The when
in
the case of eventList objects defaults to the end time of the previous
event in the list.
Step Sequencer
Event lists can be used to define complete compositions or parts of
them. They can be used modularly to start patterns, melodies, chord
or sound sequences, at any time on-the-fly. Since the lists themselves
are only JS arrays, it is possible to use algorithmic processes to
fill these, and create eventList
objects to play them. Separate
event lists however can only be soft-synchronised, as individual
play commands do not reference an external clock. The timing of
events inside each list is precisely defined, in reference to the list
playback start time, but there is no explict syncing of lists with
other lists.
For that, we introduce the concept of a step sequencer, which is
designed to provide a clock to which any number of sequences
are aligned. With the sequencer
object, we can have
-
sequences: these are defined by event lists (JS arrays) with a similar format as used in
eventLists
. -
callbacks: functions that can be registered to be executed in sync with the sequencer clock. We can trigger the playback of event lists with these.
The sequencer runs by moving from one step to another at the global BPM. This makes its clock beat-based.
Sequences
Any number of sequences can be defined to run on it. For each sequence, we can subdivide this clock by any (meaningful) number of integer divisions (2, 3, 4, 5 etc). Sequences are given a list of events (as a JS array) and play these at each beat or subdivided beat.
To set up a sequence, we use the .add()
interface to the sequencer
sequencer.add(instrument, whatList, [howLoud [,beatDiv]])
where we have to provide an instrument, an list of what to play, an optional overall volume control, and a beat subdivision.
For example, we can have these whatLists
,
const shuf = [[cymbal, 1, 0, 1 / 3], O, [cymbal, 0.9], [cymbal, 0.9]];
const kck = [[kick, 0.1], O, [kick, 0.2]];c
const snr = [snare,O];
where an O
means play nothing. The when
parameter is always
relative to the current beat (or subdivision), so it can be used to
offset the start time (always in beat units). If we want to have more
than one event on a given beat, we can pass an event list instead
of a single event for that step. A default instrument for a given
sequence can be overwridden by passing a different instrument as the
last event parameter,
melody = [
[[Eb5, 3, 0, 2, organ], [Bb5, 3, 0, 2, organ], [G6, 1, 0, 2]],
O,
[[F5, 3, 0, 2, organ], [Ab5, 3, 0, 2, organ], [Db6, 1, 0, 3.5]],
O,
O,
[Bb5, 1, 0.5, 0.5],
[[Eb5, 3, 0, 1, organ], [G5, 3, 0, 1, organ], [C6]],
O,
Bb5,
Ab5,
];
riff = [Eb3, [G3, 1, 0, 0.5], Bb3, [Db3, 2, 0.75, 0.2]];
Once we have set up our whatList
we can add it to the sequencer,
const sequencer = lp.sequencer;
const drums = lp.drums;
const cymbals = sequencer.play(drums, shuf, amp, 1/3);
const kicks = sequencer.play(drums, kck, amp, 1/3);
const snares = sequencer.play(drums, snr, amp);
const bassline = sequencer.add(bass, riff, 0.1);
const topline = sequencer.add(synth, melody, 0.1);
When litePlay.js
starts, the sequencer is stopped. So to hear these
sequences, we need to
sequencer.play()
The sequencer has a complete set of playback and sequence controls:
.play()
, .stop()
, .togglePause
. Each sequence can be
muted with .toggleMute(sequence)
, soloed
.toggleSolo(sequence)
, and removed with .remove(sequence)
.
Since each sequence uses a reference to an existing whatList
, these
can be modified on the fly to change the patterns played by the step
sequencer. JS offers many array-manipulation methods to easily
pop, push, splice, iterate over, slice, etc. This simplifies the
manipulation of sequences, making the process very malleable.
The sequencer, as all the timing of events, etc in litePlay.js
is
controlled by the global BPM, which can be adjusted at any time using
the setBPM()
function. This allows us to manipulate the tempo
of sequences more or less at will.
Callbacks
Callbacks are functions that can be passed to the sequencer to be
run at the next clock cycle (beat). This guarantees that events
triggered by the sequence are hard-synced to the sequencer
clock. The callback signature is func(t)
, where t
is the
sequencer clock time. This should be used to align the playback
of an eventList
so that the event list is played exactly in sync
with the sequences.
For example,
const piano = lp.piano;
const eList = lp.eventList.create(
piano.event(Eb7, 0.1, 0, 1),
piano.event(Bb6, 0.1, 0.25, 1),
piano.event(G6, 0.1, 0.75, 1),
piano.event(Eb6, 0.1, 1, 1));
sequencer.addCallback((t) => { eList.play(t) });
In this code, we are using the .event()
method from an
instrument, which creates an event with a set of parameters
(what, howLoud, when, howLong
). The eList
then
is used in an arrow function defining the callback, and
we can add to the sequencer. The callback is run
only once but it may be rescheduled recursively.
Conclusions
In this blogpost, we discussed the scheduling and sequencing
components of litePlay.js
, which can be seen in action in
this sketch.
In keeping with the overall
principle of lite coding, we have very simple interfaces, which
nevertheless are quite powerful for constructing event patterns
and whole compositions on-the-fly. As the platform is designed
to be open, it is possible to employ it for various kinds of music
making. In the following posts we will explore a little more the
instrument side of litePlay.js
, and expand our means of
control beyond the manipulation of events.