(ns midi (: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])) (defn get-all-midi-device-info [] (CoreMidiDeviceProvider/getMidiDeviceInfo)) (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. If rx? is true, returned devices will have unlimited receivers." [devices device-name tx? rx?] (->> devices (filter (fn [^MidiDevice$Info di] (let [d (MidiSystem/getMidiDevice di)] (and (= device-name (.getName di)) (or (not tx?) (= -1 (.getMaxTransmitters d))) (or (not rx?) (= -1 (.getMaxReceivers d))))))) (map #(MidiSystem/getMidiDevice %)))) (defn with-device "Open `device` and then run task returned by invoking `tfn` with `device` as its sole argument." [^MidiDevice device tfn] (m/sp (try (m/? (m/via m/blk (.open device))) (log/log! {:level :info, :id :midi/device-opened, :data {:device (str device)}}) (m/? (tfn device)) (finally (log/log! {:level :info, :id :midi/closing-device}) ;; NOTE: ;; Be careful, (.close device) will wait for (.send receiver ...) to return. ;; This can lead to deadlocks during cancellation. (m/? (m/via m/blk (.close device))) (log/log! {:level :info, :id :midi/device-closed, :data {:device (str device)}}))))) (defn with-tx "Feed a transmitter device (e.g. a MIDI keyboard) into a consumer `t`. `t` is a function taking a flow of MIDI ShortMessages as its sole argument and returning a task completing with consumption of that flow. When invoked, `with-tx` will open the transmitter. Upon cancellation or flow termination, the transmitter will be closed. Returns a task." [^MidiDevice device t] (m/sp (let [^Transmitter transmitter (m/? (m/via m/blk (.getTransmitter device))) rv (m/mbx) receiver (reify Receiver (send [_this midi-message _timestamp] (log/log! {:level :debug, :id :midi/sending-message}) ;; NOTE: ;; Be careful, (.close device) will wait for (.send receiver ...) to return. ;; This can lead to deadlocks during cancellation. ;; ;; TODO: git-bug a1652f9 (rv midi-message) (log/log! {:level :debug, :id :midi/message-sent})) (close [_this]))] (log/log! {:level :debug, :id :midi/rx-object-defined, :data {:receiver (str receiver)}}) (try (log/log! {:level :debug, :id :midi/setting-receiver}) (m/? (m/via m/blk (.setReceiver transmitter receiver))) (log/log! {:level :debug, :id :midi/receiver-set}) (m/? (t (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)))))) (finally (log/log! {:level :info, :id :midi/closing-tx}) (m/? (m/via m/blk (.close transmitter))) (log/log! {:level :info, :id :midi/tx-closed})))))) (def UNSCHEDULED-EVENT -1) (defn with-rx "Feed a flow of midi messages into a receiver device (e.g. a synthesizer). `f` is a flow of midi ShortMessages. When invoked, `with-rx` will open the receiver. Upon cancellation or flow termination, the receiver will be closed. Returns a task. " [^MidiDevice device f] (m/sp (let [^Receiver receiver (m/? (m/via m/blk (.getReceiver device)))] (log/log! {:level :info, :id :midi/receiver-mounted}) (try (m/? (m/reduce {} nil (m/ap (let [^MidiMessage v (m/?< f)] (log/log! {:level :debug, :id :midi/rx-received-value, :data {:value (str v)}}) (m/? (m/via m/blk (.send receiver v UNSCHEDULED-EVENT))) (log/log! {:level :debug, :id :midi/send-returned}))))) (finally (log/log! {:level :info, :id :midi/closing-rx}) (m/? (m/via m/blk (.close receiver))) (log/log! {:level :info, :id :midi/rx-closed})))))) (defn >bus "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))))) (defn bus name (m/ap (m/amb= (m/? m/never) (loop [] (log/log! {:level :debug, :id :midi/echo-rx-awaiting-value}) (m/amb (m/? rv) (recur))))))))))) (def run (echo "CoreMIDI4J - IAC Bus" 0 0)) ;; CoreMidiSource is TX Device ;; CoreMidiDestination is RX Device (def close (run prn prn)) (close) #_(defn >midi-messages->ch-stream [>midi-messages] (m/signal (m/ap (let [device (new-device)] (m/amb= device (do (let [v (m/?< >midi-messages)] (cond (instance? ShortMessage v) (let [channel (.getChannel ^ShortMessage v) command (.getCommand ^ShortMessage v) data-1 (.getData1 ^ShortMessage v)] (cond (= command ShortMessage/NOTE_ON) (swap! (first (get device channel)) conj data-1) (= command ShortMessage/NOTE_OFF) (swap! (first (get device channel)) disj data-1))) :else :other)) (m/amb))))))) #_(defn >ch-stream [>device ch] (m/cp (m/?< (second (get >device ch))))) ;; Goal: ;; Create function called `(receive-from-bus bus-name)`. ;; `bus-name` is the name of a midi bus on this machine. If a bus with that name ;; exists, the signal returned by `bus` will contain a map of {ch val}, where ch ;; is a midi channel number and val is a signal of sets representing active ;; notes on that channel. ;; ;; Create a nother function, `(send-to-bus bus-name sigs)`. `bus-name` is the ;; name of a midi bus on this machine. If a bus with that name exists, it will ;; start reading note values from `sigs`. ;; ;; Here, `sigs` is a map of {ch val}, where `ch` is a midi channel number and ;; `val` is a signal of sets representing active notes on that channel. ;;