+ - 0:00:00
Notes for current slide
Notes for next slide

Replacing Callbacks with Generators

A Case Study in Computer-Assisted Live Music

Matthieu Amiguet    Les Chemins de Traverse

The Augmented Minstrel

A Tale of Music, Snakes and Wizardry

The Augmented Minstrel

Passamezzo antico (renaissance tune)

  • Matthieu Amiguet, Augmented bass flute solo, Live performance 2013
  • Everything is played live - no sound is pre-recorded!
wait_for_midi(192,1);
<<< "Record bass!" >>>;
bass.record(1);
wait_for_midi(192,1);
<<< "Let's add a melody!" >>>;
bass.record(0);
bass.loop(1);
bass.recPos() => bass.loopEnd => chords.duration;
bass.recPos() * 3 => mel.duration;
bass.play(1);
mel.record(1);
bass.recPos() * 3 => now;
<<< "Now Chords!" >>>;
mel.record(0);
mel.gain(1.1);
mel.play(1);
chords.feedback(1);
chords.recRamp(.25::second);
chords.record(1);
chords.play(1);
(bass.recPos() * 3) - .5::second => now;
mel.rampDown(.5::second);
.5::second => now;
wait_for_midi(192,1);
<<< "Record bass!" >>>;
bass.record(1);
wait_for_midi(192,1);
<<< "Let's add a melody!" >>>;
bass.record(0);
bass.loop(1);
bass.recPos() => bass.loopEnd => chords.duration;
bass.recPos() * 3 => mel.duration;
bass.play(1);
mel.record(1);
bass.recPos() * 3 => now;
<<< "Now Chords!" >>>;
mel.record(0);
mel.gain(1.1);
mel.play(1);
chords.feedback(1);
chords.recRamp(.25::second);
chords.record(1);
chords.play(1);
(bass.recPos() * 3) - .5::second => now;
mel.rampDown(.5::second);
.5::second => now;
wait_for_midi(192,1);
<<< "Record bass!" >>>;
bass.record(1);
wait_for_midi(192,1);
<<< "Let's add a melody!" >>>;
bass.record(0);
bass.loop(1);
bass.recPos() => bass.loopEnd => chords.duration;
bass.recPos() * 3 => mel.duration;
bass.play(1);
mel.record(1);
bass.recPos() * 3 => now;
<<< "Now Chords!" >>>;
mel.record(0);
mel.gain(1.1);
mel.play(1);
chords.feedback(1);
chords.recRamp(.25::second);
chords.record(1);
chords.play(1);
(bass.recPos() * 3) - .5::second => now;
mel.rampDown(.5::second);
.5::second => now;

If it quacks like a duck, it might be a python

import pyo
s = pyo.Server(audio='jack', nchnls=1).boot()
s.start()
a = pyo.Input(chnl=0)
fol = pyo.Follower(a, freq=30, mul=4000, add=40)
f = pyo.Biquad(a, freq=fol, q=5, type=2).out()
s.gui()
def callback():
# Implement some great feature here!
# Call the function in two seconds
ca = pyo.CallAfter(callback, 2)
# Execute the callback when b1 is pressed on
# the foot controller
tf = pyo.TrigFunc(b1.trig, callback)
def callback():
# Implement some great feature here!
# Call the function in two seconds
ca = pyo.CallAfter(callback, 2)
# Execute the callback when b1 is pressed on
# the foot controller
tf = pyo.TrigFunc(b1.trig, callback)
def callback():
# Implement some great feature here!
# Call the function in two seconds
ca = pyo.CallAfter(callback, 2)
# Execute the callback when b1 is pressed on
# the foot controller
tf = pyo.TrigFunc(b1.trig, callback)

# DOES NOT WORK!!
def a():
# do whatever a does
tf = pyo.TrigFunc(b1.trig, b)
def b():
# do whatever b does
tf.stop()

tf = pyo.TrigFunc(b1.trig, b).stop()
def a():
# do whatever a does
tf.play()
def b():
tf.stop()
# do whatever b does

tf = pyo.TrigFunc(b1.trig, b).stop()
ca = pyo.CallAfter(2, c).stop()
def a():
# do whatever a does
tf.play()
ca.play()
def b():
tf.stop()
ca.stop()
# do whatever b does
def c():
tf.stop()
ca.stop()
# do whatever c does

tf = pyo.TrigFunc(b1.trig, b).stop()
ca = pyo.CallAfter(2, c).stop()
def a():
# do whatever a does
tf.play()
ca.play()
def b():
tf.stop()
ca.stop()
# do whatever b does
def c():
tf.stop()
ca.stop()
# do whatever c does

# do whatever a does
event = wait_until_first_of( # <------ this does not exist!
b1, # foot controller button 1
2 # timeout of 2 seconds
)
if event == 0:
# do whatever b does
else:
# do whatever c does

Definition of generator

def count(max):
n = 1
while n <= max:
yield n
n += 1
def count(max):
n = 1
while n <= max:
yield n
n += 1
>>> for n in count(3):
... print(n)
1
2
3
def count(max):
n = 1
while n <= max:
yield n
n += 1
>>> for n in count(3):
... print(n)
1
2
3
>>> c = count(3)
>>> next(c)
1
>>> next(c)
2
>>> next(c)
3
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
def adder():
sum = 0
while True:
sum += yield sum
def adder():
sum = 0
while True:
sum += yield sum
>>> s = adder()
>>> s.send(None) # execute to first `yield`
0
>>> s.send(1)
1
>>> s.send(2)
3
>>> s.send(3)
6
# do whatever a does
event = wait_until_first_of( # <------ this does not exist!
b1, # foot controller button 1
2 # timeout of 2 seconds
)
if event == 0:
# do whatever b does
else:
# do whatever c does
# do whatever a does
event = wait_until_first_of( # <------ this does not exist!
b1, # foot controller button 1
2 # timeout of 2 seconds
)
if event == 0:
# do whatever b does
else:
# do whatever c does
class Example(Scenario):
def setup(self):
# setup everything here
def steps(self):
# do whatever a does
event = yield b1, 2
if event == 0:
# do whatever b does
else:
# do whatever c does
# do whatever a does
event = wait_until_first_of( # <------ this does not exist!
b1, # foot controller button 1
2 # timeout of 2 seconds
)
if event == 0:
# do whatever b does
else:
# do whatever c does
class Example(Scenario):
def setup(self):
# setup everything here
def steps(self):
# do whatever a does
event = yield b1, 2
if event == 0:
# do whatever b does
else:
# do whatever c does
class Scenario:
# Max number of TrigFunc's and CallAfter's.
# Override in subclasses if needed.
MAX_TF = 3
MAX_CA = 1
def __init__(self, max_tf=3):
self.tfs = TrigFuncPool(self.step, self.MAX_TF)
self.cas = CallAfterPool(self.step, self.MAX_CA)
self.setup(initial=True)
self._state = self.runner()
# "Bootstrap" the generator
self.step(None)
def setup(self, initial):
''' Override in subclasses. Should be a regular method '''
raise NotImplementedError
def steps(self):
''' Override in subclasses. Should be a generator '''
raise NotImplementedError
class Scenario:
# Max number of TrigFunc's and CallAfter's.
# Override in subclasses if needed.
MAX_TF = 3
MAX_CA = 1
def __init__(self, max_tf=3):
self.tfs = TrigFuncPool(self.step, self.MAX_TF)
self.cas = CallAfterPool(self.step, self.MAX_CA)
self.setup(initial=True)
self._state = self.runner()
# "Bootstrap" the generator
self.step(None)
def setup(self, initial):
''' Override in subclasses. Should be a regular method '''
raise NotImplementedError
def steps(self):
''' Override in subclasses. Should be a generator '''
raise NotImplementedError
class Scenario:
# Max number of TrigFunc's and CallAfter's.
# Override in subclasses if needed.
MAX_TF = 3
MAX_CA = 1
def __init__(self, max_tf=3):
self.tfs = TrigFuncPool(self.step, self.MAX_TF)
self.cas = CallAfterPool(self.step, self.MAX_CA)
self.setup(initial=True)
self._state = self.runner()
# "Bootstrap" the generator
self.step(None)
def setup(self, initial):
''' Override in subclasses. Should be a regular method '''
raise NotImplementedError
def steps(self):
''' Override in subclasses. Should be a generator '''
raise NotImplementedError
class Scenario:
# Max number of TrigFunc's and CallAfter's.
# Override in subclasses if needed.
MAX_TF = 3
MAX_CA = 1
def __init__(self, max_tf=3):
self.tfs = TrigFuncPool(self.step, self.MAX_TF)
self.cas = CallAfterPool(self.step, self.MAX_CA)
self.setup(initial=True)
self._state = self.runner()
# "Bootstrap" the generator
self.step(None)
def setup(self, initial):
''' Override in subclasses. Should be a regular method '''
raise NotImplementedError
def steps(self):
''' Override in subclasses. Should be a generator '''
raise NotImplementedError
def step(self, triggering_event_index=0):
self.tfs.stop_all()
self.cas.stop_all()
wait_for = self._state.send(triggering_event_index)
if wait_for is None: return
if not isinstance(wait_for, tuple):
wait_for = (wait_for,)
for i, event in enumerate(wait_for):
match event:
case Number(): # wait for i seconds
self.cas.start_new(event, i)
case fc.Press(): # wait for SoftStep Button Press
self.tfs.start_new(event.trig, i)
case pyo.PyoObject(): # wait for generic pyo Trigger
self.tfs.start_new(event, i)
case _:
print('Unknown transition type!')
def step(self, triggering_event_index=0):
self.tfs.stop_all()
self.cas.stop_all()
wait_for = self._state.send(triggering_event_index)
if wait_for is None: return
if not isinstance(wait_for, tuple):
wait_for = (wait_for,)
for i, event in enumerate(wait_for):
match event:
case Number(): # wait for i seconds
self.cas.start_new(event, i)
case fc.Press(): # wait for SoftStep Button Press
self.tfs.start_new(event.trig, i)
case pyo.PyoObject(): # wait for generic pyo Trigger
self.tfs.start_new(event, i)
case _:
print('Unknown transition type!')
def step(self, triggering_event_index=0):
self.tfs.stop_all()
self.cas.stop_all()
wait_for = self._state.send(triggering_event_index)
if wait_for is None: return
if not isinstance(wait_for, tuple):
wait_for = (wait_for,)
for i, event in enumerate(wait_for):
match event:
case Number(): # wait for i seconds
self.cas.start_new(event, i)
case fc.Press(): # wait for SoftStep Button Press
self.tfs.start_new(event.trig, i)
case pyo.PyoObject(): # wait for generic pyo Trigger
self.tfs.start_new(event, i)
case _:
print('Unknown transition type!')
def step(self, triggering_event_index=0):
self.tfs.stop_all()
self.cas.stop_all()
wait_for = self._state.send(triggering_event_index)
if wait_for is None: return
if not isinstance(wait_for, tuple):
wait_for = (wait_for,)
for i, event in enumerate(wait_for):
match event:
case Number(): # wait for i seconds
self.cas.start_new(event, i)
case fc.Press(): # wait for SoftStep Button Press
self.tfs.start_new(event.trig, i)
case pyo.PyoObject(): # wait for generic pyo Trigger
self.tfs.start_new(event, i)
case _:
print('Unknown transition type!')
def step(self, triggering_event_index=0):
self.tfs.stop_all()
self.cas.stop_all()
wait_for = self._state.send(triggering_event_index)
if wait_for is None: return
if not isinstance(wait_for, tuple):
wait_for = (wait_for,)
for i, event in enumerate(wait_for):
match event:
case Number(): # wait for i seconds
self.cas.start_new(event, i)
case fc.Press(): # wait for SoftStep Button Press
self.tfs.start_new(event.trig, i)
case pyo.PyoObject(): # wait for generic pyo Trigger
self.tfs.start_new(event, i)
case _:
print('Unknown transition type!')
def runner(self):
''' Wrapper around self.steps to manage resets and scenario ending.
Without it we would have to handle it in the subclass' steps().'''
while 1:
try:
yield from self.steps()
print('Scenario is over.')
yield # keep scenario "alive" to be able to restart it
except ResetScenario:
self.setup(initial=False)
self._state = self.runner()
self.step(None)
def restart(self):
self._state.throw(ResetScenario())

while True:
display('REC ' + loop.name)
fc.led_off(1)()
...
loop.rec()
if (yield 14 * clic_dur, b2) == 1:
loop.stop_rec()
display('CANCELED ' + loop.name)
...
yield b1
continue
loop.stop_rec()
...
display(loop.name + ' OK')
if (next:=(yield b1, b8, b2)) == 2:
...
display('CANCELED ' + loop.name)
yield b1
continue
...
break

while True:
display('REC ' + loop.name)
fc.led_off(1)()
...
loop.rec()
if (yield 14 * clic_dur, b2) == 1:
loop.stop_rec()
display('CANCELED ' + loop.name)
...
yield b1
continue
loop.stop_rec()
...
display(loop.name + ' OK')
if (next:=(yield b1, b8, b2)) == 2:
...
display('CANCELED ' + loop.name)
yield b1
continue
...
break

while True:
display('REC ' + loop.name)
fc.led_off(1)()
...
loop.rec()
if (yield 14 * clic_dur, b2) == 1:
loop.stop_rec()
display('CANCELED ' + loop.name)
...
yield b1
continue
loop.stop_rec()
...
display(loop.name + ' OK')
if (next:=(yield b1, b8, b2)) == 2:
...
display('CANCELED ' + loop.name)
yield b1
continue
...
break
for loop in [bass] + chords + [hymn]:
while True:
display('REC ' + loop.name)
fc.led_off(1)()
...
loop.rec()
if (yield 14 * clic_dur, b2) == 1:
loop.stop_rec()
display('CANCELED ' + loop.name)
...
yield b1
continue
loop.stop_rec()
...
display(loop.name + ' OK')
if (next:=(yield b1, b8, b2)) == 2:
...
display('CANCELED ' + loop.name)
yield b1
continue
...
break
yield b1
for loop in [bass] + chords + [hymn]:
while True:
# REC
if (yield 14 * clic_dur, b2) == 1:
# CANCEL
continue
# STOP_REC
if (next:=(yield b1, b8, b2)) == 2:
# CANCEL
yield b1
continue
break
while True:
if next == 1: # b8 was pressed
# PLAY WITH HYMN
else:
# PLAY WITHOUT HYMN
yield b1, b8
# STOP
next = yield b1, b8
  • Digital Analogies (excerpt), Live performance 2022
  • Pierre-Yves Diacon, Dance // Matthieu Amiguet, Augmented Harpejji

Questions?

 

  • The Dragonfly album is available on all major streaming platforms (if you want to support majors) or on bandcamp (if you want to support artists).

 

Matthieu Amiguet    Les Chemins de Traverse

The Augmented Minstrel

A Tale of Music, Snakes and Wizardry

Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow