Overtone gets an event system
Overtone now has a global event system. You can subscribe to any event:
(on :chorus-finished #(fade-chorus-echo))
And publish any event:
(event ::chorus-finished)
optionally with added properties:
(event ::chorus-finished :timestamp (+ clock tick))
This is a great example of the power of having a Lisp like Clojure on top of the JVM. Using a fixed thread pool executor from the java.util.concurrent package made this incredibly simple, and since we use a reference variable to store the event handlers all modifications are safe-guarded by the STM.
(ns overtone.core.event
(:import (java.util.concurrent Executors LinkedBlockingQueue))
(:use (overtone.core util)))
(def NUM-THREADS (cpu-count))
(defonce thread-pool (Executors/newFixedThreadPool NUM-THREADS))
(defonce event-handlers* (ref {}))
(defn on
"Runs handler whenever events of type event-type are fired. The handler can
optionally except a single event argument, which is a map containing the
:event-type property and any other properties specified when it was fired.
(on ::booted #(do-stuff))
(on ::midi-note-down (fn [event] (funky-bass (:note event))))"
[event-type handler]
(dosync
(let [handlers (get @event-handlers* event-type [])]
(alter event-handlers* assoc event-type (conj handlers handler)))))
(defn remove-handler
"Remove an event handler previously registered to handle events of
event-type."
[event-type handler]
(dosync
(let [handlers (get @event-handlers* event-type [])]
(alter event-handlers* assoc event-type (filter #(not (= handler %))
handlers)))))
(defn clear-handlers
"Remove all handlers for events of type event-type."
[event-type]
(dosync (alter event-handlers* dissoc event-type)))
(defn- handle-event [event]
(doseq [handler (get @event-handlers* (:event-type event) [])]
(if (zero? (arg-count handler))
(handler)
(handler event))))
(defn event
"Fire an event of type event-type with any number of additional properties.
(event ::my-event)
(event ::filter-sweep-done :instrument :phat-bass)"
[event-type & args]
(.execute thread-pool #(handle-event (apply hash-map :event-type event-type
args))))
The only external code besides this file were a couple functions I added to the utilities for getting the number of available CPU cores and getting the arity of a function.
(defn cpu-count
"Get the number of CPUs on this machine."
[]
(.availableProcessors (Runtime/getRuntime)))
(defn arg-count
"Get the arity of a function."
[f]
(let [m (first (.getDeclaredMethods (class f)))
p (.getParameterTypes m)]
(alength p)))
This is probably not optimal in a couple areas, but it should get us pretty far. I added the arg-count detection because in my very first test of the code I got an error because I was trying to use an event handler that took no arguments.
(on ::foo #(println "This is a foo event handler."))
(event ::foo)
It’s so natural in clojure though, that I had to make it work. Now it will only pass the event argument if it sees that the arity isn’t zero, so these kinds of event handlers will work fine. Performing this reflection on every event handler might be a bit of useless overhead, but until we get to that level of optimization I’m not going to worry about it.
The default right now is to use a thread pool with as many threads as there are available CPU cores. This is just a guess as to what will actually provide efficient handling of events though, so if we start using the event system for high-throughput events we may want to do some experiments.
Mar 15 2010
Atom Feed