├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── README.md ├── project.clj └── src └── overtone ├── midi.clj └── midi └── file.clj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cake 2 | *.swp 3 | *.swo 4 | *.class 5 | *.scsyndef 6 | session 7 | checkouts 8 | autodoc 9 | lib 10 | native 11 | \#*# 12 | \.\#* 13 | *.log 14 | *.tmproj 15 | .DS_Store 16 | pom.xml 17 | *.jar 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Overtone midi-clj Change Log 2 | 3 | ## Version 0.5.0 (7th Jan 2012) 4 | 5 | * Added support for reading MIDI files via fns in overtone.midi.file 6 | * Support stringed hex messages (such as "F0 7E 7F 06 01 F7") for sysex messages 7 | * Prevent midi-handle-events from crashing on non-ShortMessage messages. 8 | * Now throws an IllegalArgumentException in midi-in and midi-out if the device couldn't be resolved. 9 | * MIDI sysex messages may now be received by supplying an extra function argument to midi-handle-events 10 | 11 | 12 | ## Version 0.4.0 (20th May 2012) 13 | 14 | * changing properties of midi event messages to be full names 15 | - e.g. :vel => :velocity 16 | 17 | * putting midi timestamp as a property in the event map, rather than 18 | as a second argument to the midi-handle-events handler function. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | midi-clj 2 | ============== 3 | 4 | #### A streamlined midi API for Clojure 5 | 6 | midi-clj is being developed for Project Overtone, and it is meant to simplify 7 | the usage of midi devices and the midi system from within Clojure. 8 | 9 | (use 'midi) 10 | 11 | ; Select midi input and output devices 12 | ; These functions bring up a GUI chooser window to select a midi port 13 | (def keyboard (midi-in)) 14 | (def phat-synth (midi-out)) 15 | 16 | ; Once you know the correct device names for your devices you can save the 17 | ; step of opening up the GUI chooser by putting a unique part of the name 18 | ; as an argument to midi-in or midi-out. The first device with a name that 19 | ; matches with lookup is returned. 20 | (def ax (midi-in "axiom")) 21 | 22 | ; Connect ins and outs easily 23 | (midi-route keyboard phat-synth) 24 | 25 | ; Trigger a note (note 40, velocity 100) 26 | (midi-note-on phat-synth 40 100) 27 | (Thread/sleep 500) 28 | (midi-note-off phat-synth 0) 29 | 30 | ; Or the short-hand version to start and stop a note 31 | (midi-note phat-synth 40 100 500) 32 | 33 | ; And the same thing with a sequence of notes 34 | (midi-play phat-synth [40 47 40] [80 50 110] [250 500 250]) 35 | 36 | In Ubuntu Linux I use the snd-virmidi kernel module to provide software midi 37 | ports. USB midi devices should be pretty much plug and play. 38 | 39 | 40 | ### Project Info: 41 | 42 | Include in your project.clj like so: 43 | 44 | [overtone/midi-clj "0.1"] 45 | 46 | #### Source Repository 47 | Downloads and the source repository can be found on GitHub: 48 | 49 | http://github.com/overtone/midi-clj 50 | 51 | Eventually there will be more documentation for this library, but in the 52 | meantime you can see it in use within the context of Project Overtone, located 53 | here: 54 | 55 | http://github.com/overtone/overtone 56 | 57 | #### Mailing List 58 | 59 | For any questions, comments or patches, use the Overtone google group here: 60 | 61 | http://groups.google.com/group/overtone 62 | 63 | ### Authors 64 | 65 | * Jeff Rose 66 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject overtone/midi-clj "0.5.0" 2 | :description "A high level midi library to read files, play notes, and 3 | interact with external midi devices." 4 | :dependencies [[org.clojure/clojure "1.3.0"] 5 | [overtone/at-at "1.0.0"]]) 6 | -------------------------------------------------------------------------------- /src/overtone/midi.clj: -------------------------------------------------------------------------------- 1 | (ns overtone.midi 2 | #^{:author "Jeff Rose" 3 | :doc "A higher-level API on top of the Java MIDI apis. It makes 4 | it easier to configure midi input/output devices, route 5 | between devices, read/write control messages to devices, 6 | play notes, etc."} 7 | (:import 8 | (java.util.regex Pattern) 9 | (javax.sound.midi Sequencer Synthesizer 10 | MidiSystem MidiDevice Receiver Transmitter MidiEvent 11 | MidiMessage ShortMessage SysexMessage 12 | InvalidMidiDataException MidiUnavailableException 13 | MidiDevice$Info) 14 | (javax.swing JFrame JScrollPane JList 15 | DefaultListModel ListSelectionModel) 16 | (java.awt.event MouseAdapter) 17 | (java.util.concurrent FutureTask ScheduledThreadPoolExecutor TimeUnit)) 18 | (:use clojure.set) 19 | (:require [overtone.at-at :as at-at])) 20 | 21 | ; Java MIDI returns -1 when a port can support any number of transmitters or 22 | ; receivers, we use max int. 23 | (def MAX-IO-PORTS Integer/MAX_VALUE) 24 | 25 | (def midi-player-pool (at-at/mk-pool)) 26 | 27 | (defn midi-devices [] 28 | "Get all of the currently available midi devices." 29 | (for [^MidiDevice$Info info (MidiSystem/getMidiDeviceInfo)] 30 | (let [device (MidiSystem/getMidiDevice info) 31 | n-tx (.getMaxTransmitters device) 32 | n-rx (.getMaxReceivers device)] 33 | (with-meta 34 | {:name (.getName info) 35 | :description (.getDescription info) 36 | :vendor (.getVendor info) 37 | :version (.getVersion info) 38 | :sources (if (neg? n-tx) MAX-IO-PORTS n-tx) 39 | :sinks (if (neg? n-rx) MAX-IO-PORTS n-rx) 40 | :info info 41 | :device device} 42 | {:type :midi-device})))) 43 | 44 | (defn midi-device? 45 | "Check whether obj is a midi device." 46 | [obj] 47 | (= :midi-device (type obj))) 48 | 49 | (defn midi-ports 50 | "Get the available midi I/O ports (hardware sound-card and virtual 51 | ports). NOTE: devices use -1 to signify unlimited sources or sinks." 52 | [] 53 | (filter #(and (not (instance? Sequencer (:device %1))) 54 | (not (instance? Synthesizer (:device %1)))) 55 | (midi-devices))) 56 | 57 | (defn midi-sources [] 58 | "Get the midi input sources." 59 | (filter #(not (zero? (:sources %1))) (midi-ports))) 60 | 61 | (defn midi-sinks 62 | "Get the midi output sinks." 63 | [] 64 | (filter #(not (zero? (:sinks %1))) (midi-ports))) 65 | 66 | (defn midi-find-device 67 | "Takes a set of devices returned from either (midi-sources) 68 | or (midi-sinks), and a search string. Returns the first device 69 | where either the name or description matches using the search string 70 | as a regexp." 71 | [devs dev-name] 72 | (first (filter 73 | #(let [pat (Pattern/compile dev-name Pattern/CASE_INSENSITIVE)] 74 | (or (re-find pat (:name %1)) 75 | (re-find pat (:description %1)))) 76 | devs))) 77 | 78 | (defn- list-model 79 | "Create a swing list model based on a collection." 80 | [items] 81 | (let [model (DefaultListModel.)] 82 | (doseq [item items] 83 | (.addElement model item)) 84 | model)) 85 | 86 | (defn- midi-port-chooser 87 | "Brings up a GUI list of the provided midi ports and then calls 88 | handler with the port that was double clicked." 89 | [title ports] 90 | (let [frame (JFrame. title) 91 | model (list-model (for [port ports] 92 | (str (:name port) " - " (:description port)))) 93 | options (JList. model) 94 | pane (JScrollPane. options) 95 | future-val (FutureTask. #(nth ports (.getSelectedIndex options))) 96 | listener (proxy [MouseAdapter] [] 97 | (mouseClicked 98 | [event] 99 | (if (= (.getClickCount event) 2) 100 | (.setVisible frame false) 101 | (.run future-val))))] 102 | (doto options 103 | (.addMouseListener listener) 104 | (.setSelectionMode ListSelectionModel/SINGLE_SELECTION)) 105 | (doto frame 106 | (.add pane) 107 | (.pack) 108 | (.setSize 400 600) 109 | (.setVisible true)) 110 | future-val)) 111 | 112 | (defn- with-receiver 113 | "Add a midi receiver to the sink device info. This is a connection 114 | from which the MIDI device will receive MIDI data" 115 | [sink-info] 116 | (let [^MidiDevice dev (:device sink-info)] 117 | (if (not (.isOpen dev)) 118 | (.open dev)) 119 | (assoc sink-info :receiver (.getReceiver dev)))) 120 | 121 | (defn- with-transmitter 122 | "Add a midi transmitter to the source info. This is a connection from 123 | which the MIDI device will transmit MIDI data." 124 | [source-info] 125 | (let [^MidiDevice dev (:device source-info)] 126 | (if (not (.isOpen dev)) 127 | (.open dev)) 128 | (assoc source-info :transmitter (.getTransmitter dev)))) 129 | 130 | (defn midi-in 131 | "Open a midi input device for reading. If no argument is given then 132 | a selection list pops up to let you browse and select the midi 133 | device." 134 | ([] (with-transmitter 135 | (.get (midi-port-chooser "Midi Input Selector" (midi-sources))))) 136 | ([in] 137 | (let [source (cond 138 | (string? in) (midi-find-device (midi-sources) in) 139 | (midi-device? in) in)] 140 | (if source 141 | (with-transmitter source) 142 | (throw (IllegalArgumentException. 143 | (str "Did not find a matching midi input device for: " in))))))) 144 | 145 | (defn midi-out 146 | "Open a midi output device for writing. If no argument is given 147 | then a selection list pops up to let you browse and select the midi 148 | device." 149 | ([] (with-receiver 150 | (.get (midi-port-chooser "Midi Output Selector" (midi-sinks))))) 151 | 152 | ([out] (let [sink (cond 153 | (string? out) (midi-find-device (midi-sinks) out) 154 | (midi-device? out) out)] 155 | (if sink 156 | (with-receiver sink) 157 | (throw (IllegalArgumentException. 158 | (str "Did not find a matching midi output device for: " out ))))))) 159 | 160 | (defn midi-route 161 | "Route midi messages from a source to a sink. Expects transmitter 162 | and receiver objects returned from midi-in and midi-out." 163 | [source sink] 164 | (let [^Transmitter tran (:transmitter source)] 165 | (.setReceiver tran (:receiver sink)))) 166 | 167 | (def midi-shortmessage-status 168 | {ShortMessage/ACTIVE_SENSING :active-sensing 169 | ShortMessage/CONTINUE :continue 170 | ShortMessage/END_OF_EXCLUSIVE :end-of-exclusive 171 | ShortMessage/MIDI_TIME_CODE :midi-time-code 172 | ShortMessage/SONG_POSITION_POINTER :song-position-pointer 173 | ShortMessage/SONG_SELECT :song-select 174 | ShortMessage/START :start 175 | ShortMessage/STOP :stop 176 | ShortMessage/SYSTEM_RESET :system-reset 177 | ShortMessage/TIMING_CLOCK :timing-clock 178 | ShortMessage/TUNE_REQUEST :tune-request}) 179 | 180 | (def midi-sysexmessage-status 181 | {SysexMessage/SYSTEM_EXCLUSIVE :system-exclusive 182 | SysexMessage/SPECIAL_SYSTEM_EXCLUSIVE :special-system-exclusive}) 183 | 184 | (def midi-shortmessage-command 185 | {ShortMessage/CHANNEL_PRESSURE :channel-pressure 186 | ShortMessage/CONTROL_CHANGE :control-change 187 | ShortMessage/NOTE_OFF :note-off 188 | ShortMessage/NOTE_ON :note-on 189 | ShortMessage/PITCH_BEND :pitch-bend 190 | ShortMessage/POLY_PRESSURE :poly-pressure 191 | ShortMessage/PROGRAM_CHANGE :program-change}) 192 | 193 | (def midi-shortmessage-keys 194 | (merge midi-shortmessage-status midi-shortmessage-command)) 195 | 196 | ;; 197 | ;; Note-off event: 198 | ;; MIDI may send both Note-On and Velocity 0 or Note-Off. 199 | ;; 200 | ;; http://www.jsresources.org/faq_midi.html#no_note_off 201 | (defn midi-msg 202 | "Make a clojure map out of a midi ShortMessage object." 203 | [^ShortMessage obj & [ts]] 204 | (let [ch (.getChannel obj) 205 | cmd (.getCommand obj) 206 | d1 (.getData1 obj) 207 | d2 (.getData2 obj) 208 | status (.getStatus obj)] 209 | {:channel ch 210 | :command (if (and (= ShortMessage/NOTE_ON cmd) 211 | (== 0 (.getData2 obj) 0)) 212 | :note-off 213 | (midi-shortmessage-keys cmd)) 214 | :msg obj 215 | :note d1 216 | :velocity d2 217 | :data1 d1 218 | :data2 d2 219 | :status (midi-shortmessage-keys status) 220 | :timestamp ts})) 221 | 222 | (defn midi-handle-events 223 | "Specify handlers that will independently receive all MIDI events and 224 | sysex messages from the input device. Both handlers should be a 225 | function of one argument, which will be a map of the message 226 | information" 227 | ([input short-msg-fn] (midi-handle-events input short-msg-fn (fn [sysex-msg] nil))) 228 | ([input short-msg-fn sysex-msg-fn] 229 | (let [receiver (proxy [Receiver] [] 230 | (close [] nil) 231 | (send [msg timestamp] (cond (instance? ShortMessage msg ) 232 | (short-msg-fn 233 | (assoc (midi-msg msg timestamp) 234 | :device input)) 235 | 236 | (instance? SysexMessage msg) 237 | (sysex-msg-fn 238 | {:timestamp timestamp 239 | :data (.getData msg) 240 | :status (.getStatus msg) 241 | :length (.getLength msg) 242 | :device input}))))] 243 | (.setReceiver (:transmitter input) receiver) 244 | receiver))) 245 | 246 | (defn midi-send-msg 247 | [^Receiver sink msg val] 248 | (.send sink msg val)) 249 | 250 | (defn midi-note-on 251 | "Send a midi on msg to the sink." 252 | ([sink note-num vel] 253 | (midi-note-on sink note-num vel 0)) 254 | ([sink note-num vel channel] 255 | (let [on-msg (ShortMessage.)] 256 | (.setMessage on-msg ShortMessage/NOTE_ON channel note-num vel) 257 | (midi-send-msg (:receiver sink) on-msg -1)))) 258 | 259 | (defn midi-note-off 260 | "Send a midi off msg to the sink." 261 | ([sink note-num] 262 | (midi-note-off sink note-num 0)) 263 | ([sink note-num channel] 264 | (let [off-msg (ShortMessage.)] 265 | (.setMessage off-msg ShortMessage/NOTE_OFF channel note-num 0) 266 | (midi-send-msg (:receiver sink) off-msg -1)))) 267 | 268 | (defn midi-control 269 | "Send a control msg to the sink" 270 | ([sink ctl-num val] 271 | (midi-control sink ctl-num val 0)) 272 | ([sink ctl-num val channel] 273 | (let [ctl-msg (ShortMessage.)] 274 | (.setMessage ctl-msg ShortMessage/CONTROL_CHANGE channel ctl-num val) 275 | (midi-send-msg (:receiver sink) ctl-msg -1)))) 276 | 277 | (def hex-char-values (hash-map 278 | \0 0 \1 1 \2 2 \3 3 \4 4 \5 5 \6 6 \7 7 \8 8 \9 9 279 | \a 10 \b 11 \c 12 \d 13 \e 14 \f 15 280 | \A 10 \B 11 \C 12 \D 13 \E 14 \F 15 281 | \space \space \, \space \newline \space 282 | \tab \space \formfeed \space \return \space)) 283 | 284 | (defn- not-space? 285 | [v] (and 286 | (not= \space v) 287 | (not= \newline v) 288 | (not= \return v) 289 | (not= \tab v))) 290 | 291 | (defn- byte-str-to-seq 292 | "Turn a case-insensitive string of hex bytes into a seq of integers. 293 | Bytes can optionally be delimited by commas or whitespace" 294 | [midi-str] 295 | (map #(Integer. (+ (* 16 (first %)) (second %))) 296 | (partition-all 2 (map hex-char-values (filter not-space? (seq midi-str)))))) 297 | 298 | (defn- byte-seq-to-array 299 | "Turn a seq of bytes into a native byte-array of 2s-complement values." 300 | [bseq] 301 | (let [ary (byte-array (count bseq))] 302 | (doseq [i (range (count bseq))] 303 | (aset-byte ary i (unchecked-byte (nth bseq i)))) 304 | ary)) 305 | 306 | (defmulti midi-mk-byte-array 307 | (fn [byte-seq] (type (first byte-seq)))) 308 | 309 | (defmethod midi-mk-byte-array 310 | java.lang.Character 311 | [byte-seq] 312 | (byte-seq-to-array (byte-str-to-seq byte-seq))) 313 | 314 | (defmethod midi-mk-byte-array java.lang.Long 315 | [byte-seq] 316 | (byte-seq-to-array (map #(Integer. %) byte-seq))) 317 | 318 | (defmethod midi-mk-byte-array java.lang.Integer 319 | [byte-seq] 320 | (byte-seq-to-array (seq byte-seq))) 321 | 322 | (defmethod midi-mk-byte-array :default 323 | [byte-seq] 324 | (byte-seq-to-array (seq byte-seq))) 325 | 326 | (defn- midi-mk-sysex-msg [bytes] 327 | (let [bytes (if (= (type bytes) (type (byte-array 0))) 328 | bytes 329 | (midi-mk-byte-array (seq bytes))) 330 | sys-msg (SysexMessage.)] 331 | (.setMessage sys-msg bytes (count bytes)) 332 | sys-msg)) 333 | 334 | (defn midi-sysex 335 | "Send a midi System Exclusive msg made up of the bytes in byte-seq 336 | byte-array, sequence of integers, longs or a byte-string to the sink. 337 | If a byte string is specified, must only contain bytes encoded as hex 338 | values. Commas, spaces, and other whitespace is ignored" 339 | [sink byte-seq] 340 | (let [sysex (midi-mk-sysex-msg byte-seq)] 341 | (midi-send-msg (:receiver sink) sysex -1))) 342 | 343 | (defn midi-note 344 | "Send a midi on/off msg pair to the sink." 345 | ([sink note-num vel dur] 346 | (midi-note sink note-num vel dur 0)) 347 | ([sink note-num vel dur channel] 348 | (midi-note-on sink note-num vel channel) 349 | (at-at/after dur #(midi-note-off sink note-num channel) midi-player-pool))) 350 | 351 | (defn midi-play 352 | "Play a seq of notes with the corresponding velocities and 353 | durations." 354 | ([out notes velocities durations] 355 | (midi-play out notes velocities durations 0)) 356 | ([out notes velocities durations channel] 357 | (loop [notes notes 358 | velocities velocities 359 | durations durations 360 | cur-time 0] 361 | (if notes 362 | (let [n (first notes) 363 | v (first velocities) 364 | d (first durations)] 365 | (at-at/after cur-time #(midi-note out n v d channel) midi-player-pool) 366 | (recur (next notes) (next velocities) (next durations) (+ cur-time d))))))) 367 | -------------------------------------------------------------------------------- /src/overtone/midi/file.clj: -------------------------------------------------------------------------------- 1 | (ns overtone.midi.file 2 | (:import java.io.File 3 | java.net.URL 4 | [javax.sound.midi MidiSystem MidiFileFormat Sequence Track 5 | MetaMessage ShortMessage]) 6 | (:use [overtone.midi :only (midi-msg)])) 7 | 8 | (defn- midi-division-type 9 | [info] 10 | (case (.getDivisionType info) 11 | Sequence/PPQ :ppq 12 | Sequence/SMPTE_24 :smpte-24fps 13 | Sequence/SMPTE_25 :smpte-25fps 14 | Sequence/SMPTE_30DROP :smpte-30drop 15 | Sequence/SMPTE_30 :smpte-30fps 16 | :unknown)) 17 | 18 | ; TODO: Figure out how to detect the strange end-of-track msg 19 | ; TODO: Find better documentation for the meta messages so we can 20 | ; either make sense of them or disregard them if unimportant. 21 | (defn- midi-event 22 | [event] 23 | (let [msg (.getMessage event) 24 | msg (cond 25 | (= (type msg) MetaMessage) {:type :meta-message} 26 | (instance? ShortMessage msg) (midi-msg msg) 27 | :default {:type :end-of-track})] 28 | (assoc msg :timestamp (.getTick event)))) 29 | 30 | (defn- midi-track 31 | [track] 32 | (let [size (.size track)] 33 | {:type :midi-track 34 | :size size 35 | :events (for [i (range size)] (midi-event (.get track i)))})) 36 | 37 | (defn midi-sequence 38 | [src] 39 | (let [mseq (MidiSystem/getSequence src) 40 | tracks (.getTracks mseq)] 41 | {:type :midi-sequence 42 | :tracks (map midi-track tracks)})) 43 | 44 | (defn midi-info 45 | [src] 46 | (let [info (MidiSystem/getMidiFileFormat src) 47 | div-type (midi-division-type info) 48 | res-type (if (= div-type :ppq) 49 | " ticks per beat" 50 | " ticks per frame") 51 | resolution (str (.getResolution info) res-type) 52 | mseq (MidiSystem/getSequence src) 53 | usecs (.getMicrosecondLength info) 54 | props (into {} (.properties info)) 55 | midi-seq (midi-sequence src)] 56 | {:type :midi-sequence 57 | :division-type div-type 58 | :resolution resolution 59 | :sequence mseq 60 | :usecs usecs 61 | :properties props})) 62 | 63 | (defn- midi-src 64 | [src] 65 | (merge 66 | (midi-info src) 67 | (midi-sequence src))) 68 | 69 | (defn midi-file 70 | [path] 71 | (let [f (File. path)] 72 | (midi-src f))) 73 | 74 | (defn midi-url 75 | [url] 76 | (let [src (URL. url)] 77 | (midi-src src))) 78 | 79 | --------------------------------------------------------------------------------