Sunday, June 7, 2009

A simple midi wrapper library in Clojure

Java integration is good in Clojure, but to get a more native feel a small wapper library may be used. This is a such a wrapper I used to read midi messages from a midi device. This is not a complete wrapper, there is no functionality for sending midi messages, reading midi files, sysex messages etc. (The source can be found at github) (Note to OS X users, you will need to install a SPI to be able to access external midi devices from java. Mandolane and mmj are two alternatives.) First I'll create a namespace for the wrapper, and import some classes from javax.sound.midi:
(ns com.jalat.cljmidi
  (:import (javax.sound.midi MidiSystem MidiUnavailableException
        MidiDevice MidiDevice$Info
        Receiver Transmitter Synthesizer Sequencer
        MidiMessage ShortMessage SysexMessage MetaMessage)))
First a function that collects info about available mididevices into a list of hashmaps. The device object itself stored with the :device key since that would be used later to get hold of the actual midi device object.
(defn get-mididevices
  "Returns a list of hashes with info about the midi devices available."
  (map (fn [device]
  {:name (.getName device)
   :vendor (.getVendor device)
   :version (.getVersion device)
   :description (.getDescription device)
   :device (. MidiSystem getMidiDevice device)})
       (. MidiSystem getMidiDeviceInfo)))
We would usually only be interested in in devices of one type so a filter function that filters on device type is probably useful.
(defn filter-mididevices [class device-infos]
  "returns a list of midi devices of the Class class from a map of midi devices"
  (filter (fn [device-info]
     (instance? class (:device device-info)))
In my case I'm using a software midi device with two outputs (Transmitters):
com.jalat.cljmidi> (map :name (filter-mididevices Transmitter (get-mididevices)))
("from v.m.k. 1.6 osx 1 " "from v.m.k. 1.6 osx 2 ")
It's fairly easy to use Java ENUMS from clojure, but I find keywods more clojury, so I set up a couple of hashmaps to translate from ENUMS to keywords:
(def midi-shortmessage-status {ShortMessage/ACTIVE_SENSING :active-sensing
                               ShortMessage/CONTINUE :continue
                               ShortMessage/END_OF_EXCLUSIVE :end-of-exclusive
                               ShortMessage/MIDI_TIME_CODE :midi-time-code
                               ShortMessage/SONG_POSITION_POINTER :song-position-pointer
                               ShortMessage/SONG_SELECT :song_select
                               ShortMessage/START :start
                               ShortMessage/STOP :stop
                               ShortMessage/SYSTEM_RESET :system-reset
                               ShortMessage/TIMING_CLOCK :timing-clock
                               ShortMessage/TUNE_REQUEST :tune-request})

(def midi-sysexmessage-status {SysexMessage/SYSTEM_EXCLUSIVE :system-exclusive
                               SysexMessage/SPECIAL_SYSTEM_EXCLUSIVE :special-system-exclusive})

(def midi-shortmessage-command {ShortMessage/CHANNEL_PRESSURE :channel-pressure
                                ShortMessage/CONTROL_CHANGE :control-change
                                ShortMessage/NOTE_OFF :note-off
                                ShortMessage/NOTE_ON :note-on
                                ShortMessage/PITCH_BEND :pitch-bend
                                ShortMessage/POLY_PRESSURE :poly-pressure
                                ShortMessage/PROGRAM_CHANGE :program-change})
Some helper definitions/functions.
(def key-names [:C :C# :D :D#  :E :F :F# :G :G# :A :A# :B])

(defn keyname 
  "Given a midi note, returns the name of the note/key"
  (nth (cycle key-names) index))

(defn- calculate-14-bit-value
  "Calculates the the 14 bit value given two integers 
representing the high and low parts of a 14 bit value."
  [lower higher]
  (bit-or (bit-and lower 0x7f)
   (bit-shift-left (bit-and higher 0x7f) 
Midi commands are midi events like note-on/note-off/pitch-bend etc. This function takes the data from a command message and returns a hashmap of the values. I've split up the cond with whitespace between the different branches to make it easier to read. There is one clojure idiom here that may look strange if you're new to clojure: the (#{:foo :bar} :gaz) test. #{} is the reader macro for creating a hashset and using it as a function it will return true if the argument is member of the set. so (#{:foo :bar} :foo) will return true, while (#{:foo :bar} :gaz) will return false.
(defn- decode-midi-command 
  "Takes the data of a midi-command and returns a hashmap of the message"
  [command channel data1 data2]
  (cond (#{:note-on :note-off} command)
 {:command command :channel channel :key (keyname data1)
  :octave (int (/ data1 12)) :velocity data2}

 (#{:channel-pressure :poly-pressure} command)
 {:command command :channel channel :key (keyname data1) 
  :octave (int (/ data1 12)) :pressure data2}

 (= :control-change command)
 {:command command :channel channel :change data1 :value data2}

 (= :program-change command)
 {:command command :chanel channel :change data1}

 (= :pitch-bend command)
 {:command command :channel channel
  :change (calculate-14-bit-value data1 data2)}))
With the helper functions out of the way it's time to get the java midi events translated to clojure maps. There are three types of midi messages in javax.sound.midi: ShortMessage, SysexMessage and MetaMessage. MetaMessages is used when reading midi files, so I'm just going to ignore it for now and just add functions for decoding ShortMessages and SysexMessages and worry about MetaMessages later. Clojure has a neat way of allowing me to do this cleanly. I'm going to make a multimethod that dispatches on the class of the message. If I send it a MetaMessage it will raise an exeption, but since I'm just going to use this to read events from midi devices that is not an issue for me.
(defmulti decode-midi-message class)

(defmethod decode-midi-message javax.sound.midi.ShortMessage [message]
  (let [status (midi-shortmessage-status (. message getStatus))
        command (midi-shortmessage-command (. message getCommand))
        channel (inc (. message getChannel))
        data1 (. message getData1)
        data2 (. message getData2)]
    (cond command (decode-midi-command command channel data1 data2)
   status  {:status status}
   :else   {:unknown-status (. message getStatus)
     :unknown-command (. message getCommand)
     :byte1 data1 :byte2 data2})))

(defmethod decode-midi-message SysexMessage [message]
  (let [bytes (. message getData)]
    {:status (midi-sysexmessage-status (. message getStatus))
     :data bytes}))
The most foreign concept of the midi api for me is that to be able to accept midi messages I need to create a object that implements the Receiver interface. Much more natural for me would be to set up a callback for each event that arrives. The callback function would accept a hashmap representing the event. In addition to the data from decode-midi-message we add a timestamp.
(defn midi-input-callback
  "Sets up a callback to f with a map representing a midi message"
  [transmitter f]
  (let [receiver (proxy [Receiver] []
     (close [] nil)
     (send [message timestamp]
    (f (assoc (decode-midi-message message)
                                   :timestamp timestamp))))]
    (. transmitter setReceiver receiver)
    (. transmitter open)
We might also not worry about a callback, maybe we just want to collect all incoming messages in a sequence? This function set up a closure with a ref to a sequence, and then creates a callback that simply add all incoming events to that sequence.
(defn midi-input-collection
  "Takes a transmitter as the argument and sets up a receiver that puts
all incoming messages into a ref to a sequence. Returns the ref"
  (let [midi-data (ref ())
        receiver (proxy [Receiver] []
     (close [] nil)
     (send [message timestamp]
     (alter midi-data
     conj (assoc (decode-midi-message message)
     :timestamp timestamp)))))]
    (. transmitter setReceiver receiver)
    (. transmitter open)
Finally, agents could combine the two features. You could set up a callback that get sent to the agent for each message that arrives, and store the sequence of events in the agent itself. Other functions could be sent to the agent by other parts of the program, for example to empty or truncate the sequence.
(defn setup-midi-agent
  "Sets up and agent and sends 'handler-function' to it whenever a message
arrives from the transmitter"
  [transmitter handler-function]
  (let [midi-agent (agent {:transmitter transmitter
                           :beat-stamp 0
                           :beat-gap 0
                           :timing-clock-queue []
                           :last-message-timestamp 0})]
    (midi-input-callback transmitter
    (fn [message-map]
      (send midi-agent handler-function message-map)))
Now we've got a simple way of collecting midi events that doesn't really show too much of the java underpinnings:
com.jalat.cljmidi> (def *foo* 
    (:device (first (filter-mididevices Transmitter (get-mididevices))))))
com.jalat.cljmidi> @*foo*
({:timestamp 14031000, :command :note-on, :channel 1, :key :C, :octave 0, :velocity 0}
 {:timestamp 13883000, :command :note-on, :channel 1, :key :C, :octave 0, :velocity 80}
 {:timestamp 13882000, :command :note-on, :channel 1, :key :E, :octave 4, :velocity 0}
 {:timestamp 12680000, :command :note-on, :channel 1, :key :E, :octave 4, :velocity 0}
 {:timestamp 12523000, :command :note-on, :channel 1, :key :E, :octave 4, :velocity 80}
 {:timestamp 12523000, :command :note-on, :channel 1, :key :D#, :octave 3, :velocity 0}
 {:timestamp 11280000, :command :note-on, :channel 1, :key :D#, :octave 3, :velocity 0}
 {:timestamp 11148000, :command :note-on, :channel 1, :key :D#, :octave 3, :velocity 80}
 {:timestamp 11146000, :command :note-on, :channel 1, :key :D, :octave 2, :velocity 0}
 {:timestamp 10431000, :command :note-on, :channel 1, :key :D, :octave 2, :velocity 0}
 {:timestamp 10235000, :command :note-on, :channel 1, :key :D, :octave 2, :velocity 80}
 {:timestamp 10235000, :command :note-on, :channel 1, :key :A, :octave 9, :velocity 0})