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)))
   device-infos))
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"
  [index]
  (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) 
     7)))
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)
    transmitter))
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"
  [transmitter]
  (let [midi-data (ref ())
        receiver (proxy [Receiver] []
     (close [] nil)
     (send [message timestamp]
    (dosync
     (alter midi-data
     conj (assoc (decode-midi-message message)
     :timestamp timestamp)))))]
    (. transmitter setReceiver receiver)
    (. transmitter open)
    midi-data))
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)))
    midi-agent))
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* 
   (midi-input-collection 
    (:device (first (filter-mididevices Transmitter (get-mididevices))))))
#'com.jalat.cljmidi/*foo*
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})


45 comments:

Daniel Rosenstark said...

Nice article. As of OSX 10.6.4 (with the Java upgrade) mmj etc. are no longer necessary... well, kind of sort of. There are various parts of the MIDI implementation they left out, but for the code you've shown the default SPI will work nicely.

wow leveling zones said...

Today, your article may be feeling pretty good, hope you can make friends. And you become friends, I think I can learn a lot of things. Refueling

cheapcarhireguide said...

I wanted to thank you for this informative and useful read. I was very encouraged to find this site. Thanks a lot.

accidentinjuryhelplines.co.uk said...

I wanted to thank you for this informative and useful read. I was very encouraged to find this site. Thanks a lot.

genemedics.com said...

Excellent posts to read it up and keep going on this way. And keep sharing these types of things thanks.

UAE offshore company formation and setup said...

I am impressing with this information you posted and by the way you got a good looking site. I like your good work.

name said...

I am impressing with this information you posted and by the way you got a good looking site. I like your good work.

genesishealthinstitute said...

I am impressing with this information you posted and by the way you got a good looking site. I like your good work.

http://www.leatherstock.co.uk/ said...

I will bookmark this for future reference and refer it to my friends.

www.realestatepatron.com/dubai-apartments-for-sale.html said...

Thanks for this useful information about blogs that you have provided us.

training4sure said...

I will bookmark this for future and refer it to my friends.

celebjackets said...

Thank you for your time and effort to have had these things together on this site.

http://www.dumps4certs.com/000-780.html said...

You blog is absolutely fantastic as there are lots of great information.

http://www.dumps4exams.com/646-206-exam.html said...

I really like the fresh perspective you did on the issue.

cheapcar4hire said...

Really was not expecting that when i started off studying.

Bourne Legacy Jacket said...

Excellently written article if only all bloggers offered the some level of content as you the internet would be a much better place.

Plagiarism checker said...

Whoa this weblog is great i love studying your posts.

southsmoke.com said...

I am not 100% convinced, but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards IDs.

5starhotelsindubai.net said...

Successful people are simply those with successful habits.

hotelapartmentsindubai.net said...

We went for the conference and it absolutely was fantastic.

custom writings said...

In this informative and useful read wanted to thank you for. I am very encouraged to find this site. Thank you very much.

Homepage said...

asdsfd hguyt erwwe rawer asw very is informative, interesting and very well written. keep up the nice high quality writing

www.legalherbalonline.com/store/ said...

zsdg hdrtnm Thank you so much dear! I’m happy to hear that you’re inspired and on your way- I wish you nothing but the best!!

Protravelnetwork said...

it is such a new boots, because of a lot of Europe and theUnited States star street snap have foot

Legal Incense said...

% convinced, but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards IDs.

Buy Cheap herbal Incense said...

really amazing. Now I have become a regular user of this website. Thanks for giving us so much knowledge

Buy K2 Spice Incense said...

but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards IDs.

Arredamenti da Esterno said...

but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards

K2 Incense said...

but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards IDs

Colorado Springs Brake Repair said...

, but I feel each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list

Dymo labels said...

each individual card has its very own ID, so you can load credits on it for subway passage. So he can white list cards

Anonymous said...

jouer casino 777 is a nice way to make easy money !

tschechien said...

Risk Management is now common in every company. Few also searching about it. I contain got handful valuable info from this point. Acknowledges for this variety of blog. Scarcity you bequeath pursue to enter equal in coming.tschechien

ios 7 download said...

As more students choose where to work based on the firms' diversity rankings, firms face an increasing market pressure in order to attract top recruits.ios 7 download

essay writers said...

Abdominal wonderful to view previous to placing employment as a way to every single on-line framework acquiring business be sure. essay writers Producing Aid be provided successful personas.

mortgage calculator with pmi said...

It’s in reality a great and helpful piece of info. I’m satisfied that you simply shared this useful info with us. Please keep us up to date like this. mortgage calculator with pmi

Jerry said...

That Coding is Awesome. Some essay writing service reviews use that. It is also Negative a multiple of persons apprehend how to remains learning of a dependent moreover ease.

solar savings said...

I am satisfied certain this mail has assisted me save many hours of browsing other alike posts just to find what I was looking for. I bookmarked this blog a while before because of the useful content and I am not ever being disappointed. hold up the good work Just I desire to state: express gratitude you!solar savings

Boarding School said...

you A simple midi wrapper library in Clojure is interesting.Boarding School for more information.

moving to costa rica said...

I shall install Java integration for this purpose.moving to costa rica

Jerry said...

This is really very nice article. Some I’m sure there are things that get be improved here and there but - like with any blogs. My friend website i.e essays shark has told me about it. Now, Thanks for sharing this.

Jerry said...

Very nice and impressive article you have posted. Its very helpful, i have read and bookmark this site and will recommend it to more other peoples.
csi

Jerry said...

This is a good post. This post gives truly quality information. I’m definitely going to look into it. Really very useful tips are provided here. Thank you so much. Keep up the good works.
waterproof iphone case

Anonymous said...

our website is very nice. It will be useful for all of us. You have completed a work. I will come here again to inspect new updates. tiny houses for rent in oregon

Anonymous said...

Risk Management is now common in every company. Few also searching about it. I contain got handful valuable info from this point. Acknowledges for this variety of blog. Scarcity you bequeath pursue to enter equal in coming australia vacation packages 2014