Hey everyone,
TL;DR When handling MIDI/OSC events, don't always use on-event or on-sync-event, favour on-latest-event.
I've been playing with events and the event system recently and thought it might be useful to share some of my findings and also explain what's new.
Firstly, I thought it useful to give a brief overview of Overtone's event system and some typical use-cases. In the simple case there are two main fns:
* event
* on-event
## event
event is a fn you call to tell the system that an event has occurred. An event is denoted by a unique key. Currently we are using vectors as keys, so events such as midi events have keys such as:
[:midi :control-change]
[:midi-device "KORG INC." "SLIDER/KNOB" "nanoKONTROL2 SLIDER/KNOB" :control-change 16]
events also take a message describing the event which is a standard Clojure map containing whatever you like.
The events then trigger matching handler fns which typically get executed asynchronously and therefore the thread creating the event should not be blocked. More on this later...
## on-event
on-event lets you register a handler fn to a specific event key. When subsequent events with the matching key are received, the event message map is applied to the handler fn. This takes place asynchronously on a thread pool with a potentially different thread used to call all the handler fns of a specific incoming event.
This means that the actual ordering of handler fn execution is not guaranteed to be the same as the incoming events. This trips up many people.
## Synchronised events
Sometimes, it isn't OK to have the actual execution of handler functions happen asynchronously on a thread pool. For example, you may wish to preserve execution order across handler fns and incoming events. You may also wish to wait until all the matching handler fns have completed execution before continuing. We therefore have sync flavours of event and on-event:
# sync-event
This is similar to event except it *forces* all handlers to be executed on the event calling thread. This means that the event calling thread is blocked whilst the handlers are called. This may also not be acceptable - see below.
# on-sync-event
This is similar to on-event only it forces this specific handler to execute on the same event calling thread regardless of whether the event was triggered with event or sync-event. This means that the event calling thread is always blocked whilst this handler is triggered.
## Lossy events
For much of the Overtone internals, the above varieties of on-event and on-sync event have been more than adequate to build a robust and reliable system with deterministic behaviour where necessary. However, as we're now starting to use the event system in more end-user like contexts, these options don't always give us what we want. For example, one common use-case is to ctl a synth with a value from a MIDI controller:
;; Don't do this!
(on-event
[:midi :control-change]
(fn [msg]
(ctl my-inst :freq (* 10 (:data2 msg))))
::my-midi-handler)
However, this won't always give you what you want. As on-event will register this handler to be called on a thread pool, it's execution order will not be kept in sync with the incoming MIDI events. This means that if the incoming MIDI data is [100 110 120], the ctl messages sent to SC may be in the order [100 120 110]. This means that your MIDI controller will be at value 120, but the synth will be playing 110 - which probably isn't desirable.
The other option is to use on-sync-event:
;; Don't typically do this!
(on-sync-event
[:midi :control-change]
(fn [msg]
(ctl my-inst :freq (* 10 (:data2 msg))))
::my-midi-handler)
This will have the effect of preserving the order of the incoming MIDI messages and the SC control message. However, depending on what else you might be doing in the handler, this runs the risk of stalling the incoming MIDI thread which means that all other system MIDI messages could be delayed or worse. This can introduce unacceptable lag into the system. Remember, on-sync-event should typically only be used for super-fast non-blocking handler fns.
## Introducing on-latest-event
I've therefore added a new type of handler: on-latest-event. This has the asynchronous behaviour of on-event combined with the order preservation behaviour of on-sync-event with an added lossy twist. It will drop incoming events if it can't handle them in time. This means that there's no chance for a backlog of unhandled events to build up and introduce lag into the system. However, it will always handle the last event received.
;; Do this!
(on-latest-event
[:midi :control-change]
(fn [msg]
(ctl my-inst :freq (* 10 (:data2 msg))))
::my-midi-handler)
If we use this handler type, our ctl messages will be sent through to SC in the correct order with no lag. However, not all MIDI messages will necessarily be converted to ctl messages - some may be dropped in order for the system to keep up and remove lag. This is typically what we want when we're controlling synths with MIDI controllers.
Hopefully this helps people use the event system more effectively. Please do reply if you have any related questions about any of this stuff...
Sam
---
http://sam.aaron.name