diff options
Diffstat (limited to 'src/unheard/midi.clj')
| -rw-r--r-- | src/unheard/midi.clj | 183 |
1 files changed, 155 insertions, 28 deletions
diff --git a/src/unheard/midi.clj b/src/unheard/midi.clj index b9f20c4..d47e5b1 100644 --- a/src/unheard/midi.clj +++ b/src/unheard/midi.clj @@ -2,11 +2,34 @@ (:require [missionary.core :as m] [taoensso.trove :as log]) (:import [javax.sound.midi MidiSystem Receiver ShortMessage MidiDevice$Info MidiDevice Transmitter MidiMessage] - [uk.co.xfactorylibrarians.coremidi4j CoreMidiDeviceProvider])) + [uk.co.xfactorylibrarians.coremidi4j CoreMidiDeviceProvider CoreMidiNotification])) (defn get-all-midi-device-info [] (CoreMidiDeviceProvider/getMidiDeviceInfo)) +(def device-infos + "A publisher containing the latest result of MidiSystem#getMidiDeviceInfo." + (m/signal + (m/cp + (m/?< + (m/ap + ;; TODO: getMidiDeviceInfo could theoretically block + ;; Move to m/blk + (let [devices (atom (CoreMidiDeviceProvider/getMidiDeviceInfo)) + >devices (m/watch devices) + notification-listener + (reify CoreMidiNotification + (midiSystemUpdated [_this] + (reset! devices (CoreMidiDeviceProvider/getMidiDeviceInfo))))] + (m/amb= + (do + (m/? (m/via m/blk (CoreMidiDeviceProvider/addNotificationListener notification-listener))) + (m/amb)) + (m/?< >devices) + (try (m/? m/never) + (finally + (m/? (m/via m/blk (CoreMidiDeviceProvider/removeNotificationListener notification-listener)))))))))))) + ;; Move to tools.repl (defn print-all-midi-devices "Prints the names of all MIDI devices attached to the computer." @@ -14,6 +37,9 @@ (doseq [^MidiDevice$Info device-info (get-all-midi-device-info)] (println (.getName device-info)))) +(comment + (print-all-midi-devices)) + (defn select-devices "Given device info list `devices`, return seq where device name is `device-name`. If tx? is true, returned devices will have unlimited transmitters. @@ -81,15 +107,15 @@ (m/? (t (m/stream (m/ap - (loop [] - (m/amb - (do - (log/log! {:level :debug, :id :midi/tx-awaiting-value}) - (m/amb)) - (let [v (m/? rv)] - (log/log! {:level :debug, :id :midi/tx-received-value, :data {:value (str v)}}) - v) - (recur))))))) + (loop [] + (m/amb + (do + (log/log! {:level :debug, :id :midi/tx-awaiting-value}) + (m/amb)) + (let [v (m/? rv)] + (log/log! {:level :debug, :id :midi/tx-received-value, :data {:value (str v)}}) + v) + (recur))))))) (finally (log/log! {:level :info, :id :midi/closing-tx}) (m/? (m/via m/blk (.close transmitter))) @@ -130,14 +156,18 @@ "Opens device named `name`. Device will consume `flow`, a flow of Message objects." - [name flow] - (let [device - (first - (select-devices (get-all-midi-device-info) - name false true))] - (with-device device - (fn [d] - (with-rx d flow))))) + [>name flow] + (m/ap + (let [device + (first + (select-devices (get-all-midi-device-info) + (m/?< name) false true))] + (if device + (m/? + (with-device device + (fn [d] + (with-rx d flow)))) + (m/amb))))) (defn <bus "Opens device named `name`. @@ -145,17 +175,24 @@ Calls `flow-handler` with a flow of midi messages. `flow-handler` should return a flow." - [name flow-handler] - (let [device - (first - (select-devices (get-all-midi-device-info) - name true false))] - (with-device device - (fn [d] - (with-tx d - (fn [f] - (m/reduce prn nil (flow-handler f)))))))) + [>name flow-handler] + (m/ap + (try + (let [device + (first + (select-devices (get-all-midi-device-info) + (m/?< >name) true false))] + (if device + (m/? + (with-device device + (fn [d] + (with-tx d + (fn [f] + (m/reduce prn nil (flow-handler f))))))) + (m/amb))) + (catch missionary.Cancelled _ (m/amb))))) +;; TODO: Move elsewhere (defn echo "Echo test." [name from-ch to-ch] @@ -209,3 +246,93 @@ (map (fn [[group instance]] {group (into {} (map (fn [[id flow]] {id (flow f)}) instance))}) config))) + +;; TODO git-bug c947320 +(def short-message->notes + "A transducer filtering ShortMessages down to note messages, and returning + a set of active notes." + (fn [rf] + (let [prev (volatile! nil)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result ^ShortMessage input] + (let [command (.getCommand input)] + (cond + ;; Channel Mode Message "All notes off" + (and (= ShortMessage/CONTROL_CHANGE command) + (= 123 (.getData1 input)) + (= 0 (.getData2 input))) + (do + (vreset! prev #{}) + (rf result #{})) + (= ShortMessage/NOTE_ON command) + (let [prev-v @prev + next (conj (into #{} prev-v) (.getData1 input))] + (vreset! prev next) + (rf result next)) + (= ShortMessage/NOTE_OFF command) + (let [prev-v @prev + next (disj (into #{} prev-v) (.getData1 input))] + (vreset! prev next) + (rf result next)) + :else + result))))))) + +(defn test-msg [cmd ch d1 d2] + (ShortMessage. cmd ch d1 d2)) + +(comment + (into [] + (transduce + short-message->notes + conj + [] + (conj + (map (fn [[cmd d1]] (test-msg cmd 0 d1 9)) + [[ShortMessage/NOTE_ON 1] + [ShortMessage/CONTROL_CHANGE 1] + [ShortMessage/NOTE_ON 2] + [ShortMessage/NOTE_OFF 2] + [ShortMessage/NOTE_OFF 1]]))))) + +;; TODO: Move this logic into bus fn +(defn short-messages + [>device-name] + (m/stream + (m/ap + (let [short-messages (atom nil) + >short-messages (m/watch short-messages)] + (m/amb= + (do (reset! short-messages nil) + (m/? + (<bus >device-name + (fn [v] + (m/ap + (try (let [msg (m/?< v)] + (reset! short-messages msg)) + (catch missionary.Cancelled c + ;; When the upstream flow is cancelled, we emit "All notes off" to consumers + (doseq [ch (range 0 16)] + (reset! short-messages (ShortMessage. ShortMessage/CONTROL_CHANGE ch 123 0))) + (throw c)))))))) + (if-let [m (m/?< >short-messages)] + m + (m/amb))))))) + +(defn notes [short-messages] + (m/signal + (m/cp + (m/?< + (m/ap + (m/amb= #{} + (m/?< (m/eduction short-message->notes short-messages)))))))) + +(comment + (def dn (atom "CoreMIDI4J - Minilab3 MIDI")) + (def >dn (m/watch dn)) + + (def cancel ((m/reduce prn nil (notes (short-messages >dn))) prn prn)) + (reset! dn "CoreMIDI4J - IAC Bus") + (reset! dn nil) + (cancel)) |
