class: bottom background-image: url(pics/couverture.jpg) .transp-back[ # Replacing Callbacks with Generators ## A Case Study in Computer-Assisted Live Music .br[ [![Matthieu Amiguet](pics/ma.png)](http://www.matthieuamiguet.ch/) [![Les Chemins de Traverse](pics/logo-small.png)](http://www.lescheminsdetraverse.net/) ] ] --- background-image: url(pics/parchment.jpg) class: center, middle # The Augmented Minstrel ## A Tale of Music, Snakes and Wizardry --- background-image: url(pics/PyconDE_page001.svg), url(pics/parchment.jpg) --- background-image: url(pics/PyconDE_page002.svg), url(pics/parchment.jpg) class: bottom, center # The Augmented Minstrel --- background-image: url(pics/parchment.jpg) # Passamezzo antico (renaissance tune)
- Matthieu Amiguet, Augmented bass flute solo, Live performance 2013 - Everything is played live - no sound is pre-recorded! --- background-image: url(pics/PyconDE_page003.svg), url(pics/parchment.jpg) --- background-image: url(pics/chuck.jpg) background-size: contain --- ``` 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; ``` --- background-image: url(pics/PyconDE_page004.svg), url(pics/parchment.jpg) --- background-image: url(pics/pyo.jpg) background-size: contain --- background-image: url(pics/PyconDE_page015.svg) # If it quacks like a duck, it might be a python ```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() ```
--- background-image: url(pics/PyconDE_page005.svg) - More details on why we chose python+pyo for live music: - Europython 2019 Talk - https://www.matthieuamiguet.ch/blog/europython19-talk-online - ![](pics/europython19-qr.png)
--- class: middle ```python 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) ``` --- class: middle ```python 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) ``` --- class: middle ```python 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) ``` --- class: middle ![](graphs/a_b1_b.dot.svg) ```python # DOES NOT WORK!! def a(): # do whatever a does tf = pyo.TrigFunc(b1.trig, b) def b(): # do whatever b does tf.stop() ``` --- class: middle ![](graphs/a_b1_b.dot.svg) ```python *tf = pyo.TrigFunc(b1.trig, b).stop() def a(): # do whatever a does tf.play() def b(): tf.stop() # do whatever b does ``` --- class: middle ![](graphs/wait2s.dot.svg) ```python 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 ``` --- background-image: url(pics/PyconDE_page011.svg), url(pics/parchment.jpg) --- class: middle ![](graphs/wait2s.dot.svg) ```python 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 ``` --- class: middle ![](graphs/wait2s.dot.svg) ```python # 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 ``` --- background-image: url(pics/PyconDE_page006.svg), url(pics/parchment.jpg) --- background-image: url(pics/parchment.jpg) class: middle, center ![](pics/oracle.png) --- background-image: url(pics/PyconDE_page007.svg), url(pics/parchment.jpg) --- background-image: url(pics/PyconDE_page008.svg), url(pics/parchment.jpg) --- background-image: url(pics/PyconDE_page009.svg), url(pics/parchment.jpg) --- ![Definition of generator](pics/generators.jpg) --- ```python def count(max): n = 1 while n <= max: yield n n += 1 ``` -- ```python-repl >>> for n in count(3): ... print(n) 1 2 3 ``` -- ```python-repl >>> c = count(3) >>> next(c) 1 >>> next(c) 2 >>> next(c) 3 >>> next(c) Traceback (most recent call last): File "
", line 1, in
StopIteration ``` --- ```python def adder(): sum = 0 while True: sum += yield sum ``` -- ```python-repl >>> s = adder() >>> s.send(None) # execute to first `yield` 0 >>> s.send(1) 1 >>> s.send(2) 3 >>> s.send(3) 6 ``` --- ```python # 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 ``` -- ```python 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 ``` --- ```python # 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 ``` ```python 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 ``` --- ```python 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 ``` --- ```python 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 ``` --- ```python 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 ``` --- ```python 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 ``` --- ```python 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!') ``` --- ```python 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!') ``` --- ```python 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!') ``` --- ```python 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!') ``` --- ```python 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!') ``` --- ```python 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()) ``` --- background-image: url(pics/PyconDE_page011.svg), url(pics/parchment.jpg) --- class: middle, center background-image: url(pics/PyconDE_page012.svg), url(pics/parchment.jpg) --- .tr[
] ```python 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 ``` --- .tr[
] ```python 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 ``` --- .tr[
] ```python 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 ``` --- background-image: url(pics/PyconDE_page016.svg), url(pics/parchment.jpg) --- ```python *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 ``` --- background-image: url(pics/PyconDE_page017.svg), url(pics/parchment.jpg) --- class: middle ```python 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 ``` --- background-image: url(pics/PyconDE_page010.svg), url(pics/parchment.jpg) --- class: middle, center
- Digital Analogies (excerpt), Live performance 2022 - Pierre-Yves Diacon, Dance // Matthieu Amiguet, Augmented Harpejji --- background-image: url(pics/PyconDE_page014.svg), url(pics/parchment.jpg) --- # Questions?
- Find code, references, and details on https://www.matthieuamiguet.ch/blog/pyconde24 ### - The *Dragonfly* album is available on all major streaming platforms (if you want to support majors) or on [bandcamp](https://lescheminsdetraverse.bandcamp.com/album/dragonfly) (if you want to support artists). ### - All music by [Les Chemins de Traverse](https://www.lescheminsdetraverse.net/) - Drawings by [Matthieu Amiguet](https://www.matthieuamiguet.ch/) - parchment background by [Devontt on Flickr](https://www.flickr.com/photos/devontt/167285657/in/photostream/) (CC-BY) .br[ [![Matthieu Amiguet](pics/ma.png)](http://www.matthieuamiguet.ch/) [![Les Chemins de Traverse](pics/logo-small.png)](http://www.lescheminsdetraverse.net/) ]