Timeline#
Summary
In this tutorial, we’ll see how to schedule musical events along a global transport timeline.
Note: you can download this tutorial as a
Jupyter notebook
.
Tone.js offers the possibility to schedule with precision sound or musical events along a global timeline. By analogy, this is very similar to how we use the arrangement view in a classic DAW to position audio or MIDI clips and/or draw automation curves.
import ipytone
Transport#
The Tone.js global timeline is exposed in Python with the
Transport
singleton class, which may be accessed via
ipytone.transport
:
t = ipytone.transport
Important
Like the audio context used by ipytone and Tone.js, the
Transport
timeline
is exposed globally in the front-end. Consequently, if two or more notebooks
with independent kernels are open in the same JupyterLab tab, they
will all act on the same timeline.
Playback state, position, bpm, time signature…#
The playback state of the timeline is controlled by the
start()
,
pause()
and
stop()
methods.
t.start().stop("+1")
Transport()
The position
property can be used to move the current “cursor” along the
timeline. It accepts different kinds of values, e.g.,
a string in the form of
"Bars:Beats:Sixteenths"
a string like
"4m"
(four measures from the beginning of the timeline) or “8n” (8-notes)a float (number of seconds from the beginning of the timeline)
See the Tone.js Wiki for more details.
t.position
0.0
It is possible to define a limited segment along the timeline that will be played in loop:
t.loop = True
t.loop_start = "4:0:0"
t.loop_end = "8:0:0"
Transport
has also properties for setting the BPM or
the time signature:
t.bpm
Param(value=120.0, units='bpm')
t.time_signature
4
Basic scheduling#
Basic event scheduling can be done either using
Transport
with callbacks or using context managers
provided by ipytone.
Let’s first create an instrument
synth = ipytone.Synth(volume=-5).to_destination()
Using callbacks#
Callbacks must accept one time
argument (in seconds) and usually implement in
their body all the events that we want be triggered at that time (e.g.,
instrument note, parameter automation, etc.).
def callback(time):
synth.trigger_attack_release("C4", "16n", time=time)
Then we can pass it to one of the schedule()
,
schedule_repeat()
and
schedule_once()
methods of
Transport
, e.g.,
# schedule the call repeatedly at every 4th bar note
event_id = t.schedule_repeat(callback, "4n")
t.start().stop("+2m")
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Those schedule methods return an event id that can be used later to remove it
from the transport timeline with the clear()
,
method, e.g.,
t.clear(event_id)
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
# no scheduled event, no sound
t.start().stop("+2m")
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Important
Ipytone doesn’t handle those callbacks like you might expect, i.e., like functions called at each of the scheduled times. Instead, ipytone uses some tricks internally to reconstruct (more-or-less) equivalent callbacks in the front-end before passing it to Tone.js scheduling functions.
As a consequence, Python callbacks have some limitations compared to Tone.js / JS callbacks. More specifically, the Python code inside a callback will be executed only once. Here is a bad example that won’t behave as we’d like:
import random
def callback(time):
# This will randomly choose one note once for all repeated events!!
note = random.choice(["C4", "G4", "A4"])
synth.trigger_attack_release(note, "16n", time=time)
There’s also very limited support for making operations with the time
argument
(currently, only addition is supported).
Using contexts#
For convenience, the same scheduling operations can be achieved using context managers. For example:
with ipytone.schedule_once("4n") as (time, event_id):
synth.trigger_attack_release("A4", "16n", time=time)
synth.trigger_attack_release("A5", "16n", time=time + "16n")
t.start().stop("+2m")
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Note that unlike the example above, the two note triggers here are scheduled only once so they won’t be re-triggered after stopping and restarting the transport
# no scheduled event, no sound
t.start().stop("+2m")
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Advanced scheduling#
Ipytone also exposes Tone.js event classes for more advanced and handy scheduling.
Event#
Event
is the base class of all events and accepts a callback
that has two arguments: time
and a value
(i.e., a pitch or a note).
def event_clb(time, value):
synth.trigger_attack_release(value, "16n", time=time)
event = ipytone.Event(callback=event_clb, value="A4")
It can then be started / stopped anywhere along the transport timeline and can even be looped:
event.loop = True
event.loop_start = "4:0:0"
event.loop_end = "8:0:0"
event.start()
Event(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Important
Events won’t fire unless the Transport
is started.
t.start()
Transport(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Event
also provides some other properties to control its
playback rate and to randomize it.
# play it 4x faster
event.playback_rate = 4
# the probability to trigger the event
event.probability = 0.8
# apply a small random offset on the trigger time
event.humanize = 0.2
Call cancel
to remove it from the transport timeline:
event.stop()
event.cancel()
Event(loop=True, loop_start='4:0:0', loop_end='8:0:0')
Loop#
Loop
is a simplified event that is looped by default at a
user-defined interval. Like in basic scheduling, it accepts a callback with one
time
argument.
loop = ipytone.Loop(callback=callback, interval="8n")
loop.start()
Loop(interval='8n')
loop.stop()
Loop(interval='8n')
loop.cancel()
Loop(interval='8n')
Part#
Part
is a sequence of events that can be handled just like an
Event
(i.e., it is a music partition). It accepts a callback
with two time
and note
arguments.
Important
While Tone.js accepts any arbitrary value type for the second argument, ipytone
only accepts an instance of Note
. The way ipytone handles
Python callbacks (i.e., not “real” callbacks) imposes this restriction.
def part_clb(time, note):
synth.trigger_attack_release(
note.note, note.duration, time=time, velocity=note.velocity
)
In the Part
constructor, events may be specified either with
instances of Note
(which contain information about the actual
note, time, duration and velocity) or an equivalent dictionary.
part = ipytone.Part(
callback=part_clb,
events=[
ipytone.Note(0, "A4", duration="16n"),
{"time": "8n", "note": "B4", "duration": "8n", "velocity": 0.5},
ipytone.Note("2n", "E4", duration="16n"),
]
)
part.start()
Part()
Although the single events of a Part
cannot be accessed
directly, they can be further updated using the add
,
at
, at
,
remove
and clear
methods.
part.add(ipytone.Note("16n", "A3", velocity=0.2))
Part()
part.at("2n", ipytone.Note("2n", "E5", duration="4n"))
part.clear()
Part()
part.stop()
Part()
Sequence#
Sequence
is an alternative to Part
where the
note events are evenly spaced at a given subdivision.
seq = ipytone.Sequence(
callback=event_clb,
events=["A4", "C4", "B4", "A5"],
subdivision="8n",
)
A sequence is looped by default.
seq.start()
Sequence(loop=True, loop_start=0.0, loop_end=0.0)
Events may be nested (the interval within a nested array corresponds to the parent subdivision divided by the length of the nested array).
seq.events = ["A4", ["C4", "E4", "D4"], "B4", "A5"]
Blank intervals may be defined by None
array elements:
seq.events = ["A4", ["C4", "E4", "D4"], "B4", None]
seq.stop()
Sequence(loop=True, loop_start=0.0, loop_end=0.0)
Pattern#
Pattern
is like an arpeggiator, it cycles trough an sequence
of notes with a given pattern.
pat = ipytone.Pattern(
callback=event_clb,
values=["C3", "E3", "G3"],
pattern="upDown",
)
pat.start()
Pattern(interval='8n', pattern='upDown')
pat.pattern="up"
pat.stop()
Pattern(interval='8n', pattern='up')
Dispose events#
Like audio nodes, events may also be disposed.
event.dispose()
loop.dispose()
part.dispose()
seq.dispose()
pat.dispose()
Pattern(disposed=True, interval='8n', pattern='up')
We’ve reached the end of this tutorial. Let’s cancel all scheduled events, stop the transport and dispose the synth.
t.cancel()
t.stop()
synth.dispose()
Synth(disposed=True)