Langa Łukasz Langa ambv on GitHub fbme ambv llanga lukaszlangapl todays plan Discover how AsyncIO can be used to process MIDI to drive hardware synthesizers LIVE todays equipment ID: 813168
Download The PPT/PDF document "Asyncio + music Łukasz" is the property of its rightful owner. Permission is granted to download and print the materials on this web site for personal, non-commercial use only, and to display it on your personal computer provided you do not modify the materials and that you retain all copyright notices contained in the materials. By downloading content from our website, you accept the terms of this agreement.
Slide1
Asyncio + music
Łukasz
Langa
Slide2Łukasz Langa
ambv on GitHub
fb.me/ambv@llangalukasz@langa.pl
Slide3today's plan
Discover how
AsyncIO
can be used to
process MIDI
to drive
hardware synthesizers
LIVE
Slide4Slide5today's equipment
Slide6today's equipment
Slide7today's equipment
Slide8Callback 1
Callback 2
…
Callback N
AsyncIO
primer in 5 minutes
Slide9AsyncIO
is an event loop
import
asyncio
if __name__ == "__main__":
loop =
asyncio.get_event_loop()
loop.run_forever
()
Slide10AsyncIO
is literally an event loop
class
BaseEventLoop
:
...
def
run_forever
(self): """Run until stop() is called."""
self._
check_closed
()
self._running
= True
try:
while True:
try:
self._
run_once
()
except _
StopError
:
break
finally:
self._running
= False
Slide11You schedule things for the loop to call
def callback(
i
:
int
) -> None:
print(
i
,
datetime.datetime.now
())
if __name__ == "__main__":
loop =
asyncio.get_event_loop
()
loop.call_later
(2,
loop.stop
)
for
i
in range(1, 4):
loop.call_soon
(callback,
i
)
try:
loop.run_forever() finally:
loop.close()
Slide12Time consuming callbacks slow down the entire loop:
no concurrency?!
def callback(
i
:
int
) -> None:
print(
i
, datetime.datetime.now())
time.sleep
(
i
) # time consuming action
if __name__ == "__main__":
loop =
asyncio.get_event_loop
()
loop.call_later
(2,
loop.stop
)
for
i
in range(1, 4):
loop.call_soon
(callback,
i) try:
loop.run_forever() finally:
loop.close()
Slide13Coroutines to the rescue
async
def
callback
(i:
int
) ->
None
:
print(i,
datetime.datetime.now
())
await
asyncio.sleep
(i)
async
def
main
() ->
None
:
await
callback
(1) await
callback(2)
await callback(3)
if __name__ == "__main__":
asyncio.run(main())
Slide14Run many coroutines concurrently
by gathering them
async
def
callback
(i:
int
) ->
None
:
print
(i,
datetime.datetime.now
())
await
asyncio.sleep
(i)
async
def
main
() ->
None
:
await
asyncio.gather(
callback(1),
callback(2), callback(3),
)if __name__ == "__main__":
asyncio.run(main())
Slide15Coroutines still use the event loop
class Task(
futures.Future
):
def __
init
__(self,
coro
, loop=None):
super().__init__(loop=loop)
...
self._
loop.call_soon
(
self._step
)
Slide16Coroutines still use the event loop
class Task(
futures.Future
):
def _step(self):
...
try:
...
result = next(self._
coro)
except
StopIteration
as
exc
:
self.set_result
(
exc.value
)
except
BaseException
as
exc
:
self.set_exception
(exc) raise else: ...
self._loop.call_soon(
self._step)
Slide17Sliced coroutines on the event loop
coro
(
1
)._step()
coro
(
2
)._step()
coro
(
3
)._step()
coro
(
1
)._step()
coro
(
2
)._step()
...
Slide18MIDI, what's that?
Slide19MIDI, what's that?
COMPUTER
SYNTHESIZER
KEYBOARD CONTROLLER
USB-MIDI
MIDI
OUT
MIDI
IN
MIDI
IN
MIDI
OUT
Slide20MIDI: a synchronous unidirectional
point-to-point network protocol
(
NOTE_ON
on
CHANNEL 10
, C-1,
velocity 127)(NOTE_ON on
CHANNEL 2, E-2, velocity
80)......(NOTE_OFF
on
CHANNEL 10
, C-1,
velocity
0)
(
NOTE_OFF
on
CHANNEL 2
, E-2,
velocity
0)
...
...
(
NOTE_ON
on
CHANNEL 10, F-1, velocity 80)
......
(NOTE_OFF on CHANNEL 10, F-1, velocity
80)...time
MIDI IN
MIDI
OUT
Slide21MIDI clock
(
CLOCK
)
(
CLOCK
)
(CLOCK)(CLOCK)(
CLOCK)
(START)(CLOCK)
(
CLOCK
)
(
CLOCK
)
(
STOP
)
(
CLOCK
)
(
CLOCK
)
(
CLOCK
)(CLOCK)
MIDI
IN
MIDI OUT
24
ppqn
(pulses per quarter note)
time
Slide22MIDI
IN
from Circuit
MIDI
OUT
to Circuit
What we're going to do next
MIDI
OUT
to Mono Station
DRUM MACHINE
ANALOG BASS
CLOCK
Slide23async
def
async_main() -> None: from_circuit,
to_circuit
=
get_ports
("Circuit", clock_source=True) from_mono_station
, to_mono_station = get_ports("Circuit Mono Station")
Slide24async
def
async_main() -> None: from_circuit,
to_circuit
=
get_ports
("Circuit", clock_source=True) from_mono_station
, to_mono_station = get_ports("Circuit Mono Station") queue: asyncio.Queue
[MidiMessage] =
asyncio.Queue(maxsize=256) loop = asyncio.get_event_loop
()
Slide25async
def
async_main() -> None: from_circuit,
to_circuit
=
get_ports
("Circuit", clock_source=True) from_mono_station
, to_mono_station = get_ports("Circuit Mono Station") queue: asyncio.Queue
[MidiMessage] =
asyncio.Queue(maxsize=256) loop = asyncio.get_event_loop
()
def
midi_callback
(
msg
, data=None):
sent_time
=
time.time
()
midi_message
,
event_delta
=
msg
loop.call_soon_threadsafe(
queue.put_nowait, (midi_message
, event_delta, sent_time) )
from_circuit.set_callback(midi_callback)
Slide26async
def
async_main() -> None: from_circuit,
to_circuit
=
get_ports
("Circuit", clock_source=True) from_mono_station
, to_mono_station = get_ports("Circuit Mono Station") queue: asyncio.Queue
[MidiMessage] =
asyncio.Queue(maxsize=256) loop = asyncio.get_event_loop
()
def
midi_callback
(
msg
, data=None):
sent_time
=
time.time
()
midi_message
,
event_delta
=
msg
loop.call_soon_threadsafe(
queue.put_nowait, (midi_message
, event_delta, sent_time) )
from_circuit.set_callback(midi_callback) from_mono_station.close_port
() # we won't be using that one now
Slide27async
def
async_main
() -> None:
from_circuit
, to_circuit = get_ports
("Circuit", clock_source=True) from_mono_station, to_mono_station
= get_ports("Circuit Mono Station")
queue: asyncio.Queue[MidiMessage] = asyncio.Queue
(
maxsize
=256)
loop =
asyncio.get_event_loop
()
def
midi_callback
(
msg
, data=None):
sent_time
=
time.time
()
midi_message, event_delta = msg
loop.call_soon_threadsafe
( queue.put_nowait, (midi_message,
event_delta, sent_time) ) from_circuit.set_callback
(midi_callback) from_mono_station.close_port()
# we won't be using that one now performance = Performance(drums=
to_circuit, bass=to_mono_station) try: await
midi_consumer(queue, performance) except asyncio.CancelledError: from_circuit.cancel_callback
()
Slide28async
def
midi_consumer
(
queue:
asyncio.Queue
[MidiMessage], performance: Performance) -> None:
drums: Optional[asyncio.Task] = None last_msg: MidiPacket
= [0] while True:
msg, delta, sent_time = await queue.get()
if
msg
[0] == CLOCK:
if
last_msg
[0] == CLOCK:
performance.pulse_delta
= delta
elif
msg
[0] == START:
if drums is None:
drums =
asyncio.create_task
(drum_machine(performance))
elif msg
[0] == STOP: if drums is not None: drums.cancel() drums = None
last_msg = msg
Slide29Demo time
MIDI
IN
from Circuit
MIDI
OUT
to Circuit
MIDI
OUT
to Mono Station
DRUM MACHINE
ANALOG BASS
CLOCK