Skip to content

Commit

Permalink
Initial creation of standalone repo.
Browse files Browse the repository at this point in the history
  • Loading branch information
rosejn committed Jan 15, 2010
0 parents commit 3dfd1ce
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
25 changes: 25 additions & 0 deletions README.md
@@ -0,0 +1,25 @@
midi-clj
==============

#### A streamlined midi API for Clojure

midi-clj is being developed for Project Overtone, and it is meant to simplify
the usage of midi devices and the midi system from within Clojure.

### Project Info:

#### Source Repository
Downloads and the source repository can be found on GitHub:

http://github.com/rosejn/midi-clj


#### Mailing List

For any questions, comments or patches, use the Overtone google group here:

http://groups.google.com/group/overtone

### Authors

* Jeff Rose
10 changes: 10 additions & 0 deletions project.clj
@@ -0,0 +1,10 @@
(defproject midi-clj "0.1"
:description "A high level midi library to play notes and interact with external midi devices."
:dependencies [[org.clojure/clojure
"1.1.0-master-SNAPSHOT"]
[org.clojure/clojure-contrib
"1.0-SNAPSHOT"]]
:dev-dependencies [[swank-clojure "1.1.0-SNAPSHOT"]
[lein-clojars "0.5.0-SNAPSHOT"]
[lein-nailgun "0.1.0"]])

222 changes: 222 additions & 0 deletions src/midi.clj
@@ -0,0 +1,222 @@
(ns midi
#^{:author "Jeff Rose"
:doc "A higher-level API on top of the Java MIDI apis. It makes it
easier to configure midi input/output devices, route between devices,
read/write control messages to devices, play notes, etc."}
(:import
(java.util.regex Pattern)
(javax.sound.midi Sequencer Synthesizer
MidiSystem MidiDevice Receiver Transmitter MidiEvent
MidiMessage ShortMessage SysexMessage
InvalidMidiDataException MidiUnavailableException)
(javax.swing JFrame JScrollPane JList
DefaultListModel ListSelectionModel)
(java.awt.event MouseAdapter)
(java.util.concurrent FutureTask))
(:use clojure.set))

;(defn byte-array [len]
; (make-array (. Byte TYPE) len))

(defn midi-devices []
"Get all of the currently available midi devices."
(for [info (MidiSystem/getMidiDeviceInfo)]
(let [device (MidiSystem/getMidiDevice info)]
(with-meta
{:name (.getName info)
:description (.getDescription info)
:vendor (.getVendor info)
:version (.getVersion info)
:sources (.getMaxTransmitters device)
:sinks (.getMaxReceivers device)
:info info
:device device}
{:type :midi-device}))))

(defn midi-device?
"Check whether obj is a midi device."
[obj]
(= :midi-device (type obj)))

(defn midi-ports
"Get the available midi I/O ports (hardware sound-card and virtual ports)."
[]
(filter #(and (not (instance? Sequencer (:device %1)))
(not (instance? Synthesizer (:device %1))))
(midi-devices)))

;; NOTE: devices use -1 to signify unlimited sources or sinks

(defn midi-sources []
"Get the midi input sources."
(filter #(not (zero? (:sources %1))) (midi-ports)))

(defn midi-sinks
"Get the midi output sinks."
[]
(filter #(not (zero? (:sinks %1))) (midi-ports)))

(defn midi-find-device
"Takes a set of devices returned from either (midi-sources) or (midi-sinks), and a
search string. Returns the first device where either the name or description
mathes using the search string as a regexp."
[devs dev-name]
(first (filter
#(let [pat (Pattern/compile dev-name Pattern/CASE_INSENSITIVE)]
(or (re-find pat (:name %1))
(re-find pat (:description %1))))
devs)))

(defn- list-model
"Create a swing list model based on a collection."
[items]
(let [model (DefaultListModel.)]
(doseq [item items]
(.addElement model item))
model))

(defn midi-port-chooser
"Brings up a GUI list of the provided midi ports and then calls handler with the port
that was double clicked."
[title ports]
(let [frame (JFrame. title)
model (list-model (for [port ports]
(str (:name port) " - " (:description port))))
options (JList. model)
pane (JScrollPane. options)
future-val (FutureTask. #(nth ports (.getSelectedIndex options)))
listener (proxy [MouseAdapter] []
(mouseClicked
[event]
(if (= (.getClickCount event) 2)
(.setVisible frame false)
(.run future-val))))]
(doto options
(.addMouseListener listener)
(.setSelectionMode ListSelectionModel/SINGLE_SELECTION))
(doto frame
(.add pane)
(.pack)
(.setSize 400 600)
(.setVisible true))
future-val))

(defn- with-receiver
"Add a midi receiver to the sink device info."
[sink-info]
(let [dev (:device sink-info)]
(if (not (.isOpen dev))
(.open dev))
(assoc sink-info :receiver (.getReceiver dev))))

(defn- with-transmitter
"Add a midi transmitter to the source info."
[source-info]
(let [dev (:device source-info)]
(if (not (.isOpen dev))
(.open dev))
(assoc source-info :transmitter (.getTransmitter dev))))

(defn midi-in
"Connect the sequencer to a midi input device."
([] (with-transmitter
(.get (midi-port-chooser "Midi Input Selector" (midi-sources)))))
([in]
(let [source (cond
(string? in) (midi-find-device (midi-sources) in)
(midi-device? in) in)]
(if source
(with-transmitter source)
(do
(println "Did not find a matching midi input device for: " in)
nil)))))

(defn midi-out
"Connect the sequencer to a midi output device."
([] (with-receiver
(.get (midi-port-chooser "Midi Output Selector" (midi-sinks)))))

([out] (let [sink (cond
(string? out) (midi-find-device (midi-sinks) out)
(midi-device? out) out)]
(if sink
(with-receiver sink)
(do
(println "Did not find a matching midi output device for: " out)
nil)))))

(defn midi-route
"Route midi messages from a source to a sink. Expects transmitter and receiver objects
returned from midi-in and midi-out."
[source sink]
(.setReceiver (:transmitter source) (:receiver sink)))

(defn midi-msg
"Make a clojure map out of a midi object."
[obj]
{:chan (.getChannel obj)
:cmd (.getCommand obj)
:note (.getData1 obj)
:vel (.getData2 obj)
:data1 (.getData1 obj)
:data2 (.getData2 obj)
})

(defn midi-handle-events
"Specify a single handler that will receive all midi events from the input device."
[input fun]
(let [receiver (proxy [Receiver] []
(close [] nil)
(send [msg timestamp] (fun (midi-msg msg) timestamp)))]
(.setReceiver (:transmitter input) receiver)))

;; NOTE: Unfortunately, it seems that either Pianoteq or the virmidi modules
;; don't actually make use of the timestamp...
(defn midi-note-on
"Send a midi on msg to the sink."
[sink note-num vel & [timestamp]]
(let [on-msg (ShortMessage.)]
(.setMessage on-msg ShortMessage/NOTE_ON 0 note-num vel)
(.send (:receiver sink) on-msg -1)))

(defn midi-note-off
"Send a midi off msg to the sink."
[sink note-num vel]
(let [off-msg (ShortMessage.)]
(.setMessage off-msg ShortMessage/NOTE_OFF 0 note-num 0)
(.send (:receiver sink) off-msg -1)))

(defn- byte-seq-to-array
"Turn a seq of bytes into a native byte-array."
[bseq]
(let [ary (byte-array (count bseq))]
(doseq [i (range (count bseq))]
(aset-byte ary i (nth bseq i)))
ary))

(defn midi-sysex
"Send a midi System Exclusive msg made up of the bytes in byte-seq to the sink."
[sink byte-seq]
(let [sys-msg (SysexMessage.)
bytes (byte-seq-to-array byte-seq)]
(.setMessage sys-msg bytes (count bytes))
(.send (:receiver sink) sys-msg -1)))

;(defn midi-note
; "Send a midi on/off msg pair to the sink."
; [sink note-num vel dur]
; (midi-note-on sink note-num vel)
; (schedule #(midi-note-off sink note-num 0) dur))
;
;(defn midi-play [out notes velocities durations]
; (loop [notes notes
; velocities velocities
; durations durations
; cur-time 0]
; (if notes
; (let [n (first notes)
; v (first velocities)
; d (first durations)]
; (schedule #(midi-note out n v d) cur-time)
; (recur (next notes) (next velocities) (next durations) (+ cur-time d))))))
;

0 comments on commit 3dfd1ce

Please sign in to comment.