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})
3 comments:
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.
Private Label Organic Cosmetics in India
Best Private Label Cosmetics in India
Cosmetic products manufacturers in India
Its library of titles is split into quantity of} categories for Slots, Blackjack, Video Poker, Roulette, Table Games, and Others. With so many, specific selections, 온라인카지노 it’s actually easy to seek out|to search out} the game you want. What’s more, if you know precisely which title you’re after, just type it into the obtainable search bar. Get BacanaPlay in your mobile device and play all of your favorite casino games wherever you go. Several casino websites listed in our evaluations may not be obtainable in your area.
Post a Comment