(ns midi (:require [missionary.core :as m]) (:import [javax.sound.midi MidiSystem Receiver ShortMessage MidiDevice$Info MidiDevice Transmitter])) (def >midi-devices (m/stream (m/ap (loop [] (m/amb= (MidiSystem/getMidiDeviceInfo) (do (m/? (m/sleep 5000)) (recur))))))) ;; NOTE: Seems that there is a JVM bug that prevents device rescanning ;; (def >midi-device-info "A flow of maps of device name -> device properties" (m/signal (m/eduction (dedupe) (m/ap (let [devices (m/?< >midi-devices)] (into {} (map (fn [^MidiDevice$Info d] [(.getName d) {:description (.getDescription d) :vendor (.getVendor d) :version (.getVersion d) :device-info d}]) devices))))))) (defn >device-info [device-name] (m/ap (let [device-info (m/?< >midi-device-info)] (get-in device-info [device-name :device-info])))) (defn >device "Returns a device for given device name. Returns nil if device not found." [device-name] (m/stream (m/ap (let [device-info (m/?< (>device-info device-name)) ^MidiDevice device (MidiSystem/getMidiDevice ^MidiDevice$Info device-info)] ;; Essential problem: Combining taking element from flow to put in ;; conditional, and cleaning up in catch. ;; NOTE: You need the MidiDevice type hint when you call open and close! (m/amb= (do (println "opening device") (m/? (m/via m/blk (.open device))) (println "device opened") (m/amb device)) (try (m/? m/never) (finally (println "closing device") (m/compel (m/? (m/via m/blk (.close device)))) (println "device closed")))))))) ;; Key insight: Cancelation shouldn't actually start with the device (defn >midi-messages "A flow of java midi messages" [device-name] (m/stream (m/ap (let [^MidiDevice device (m/?< (>device device-name)) ^Transmitter transmitter (m/? (m/via m/blk (.getTransmitter device))) transmit (atom nil) >transmit (m/eduction (filter some?) (m/ap (try (m/?< (m/watch transmit)) (catch missionary.Cancelled _ (m/amb))))) receiver (reify Receiver (send [_this midi-message _timestamp] (println "HI") (reset! transmit midi-message)) ;; TODO: Close (close [this]))] (m/amb= (do (println "Connecting to transmitter") (m/? (m/via m/blk (.setReceiver transmitter receiver))) (println "Connected to transmitter") (m/?< >transmit)) (try (m/? m/never) (finally (println "Disconnecting from transmitter") (m/? (m/compel (m/via m/blk (.close receiver)))) (println "Disconnected from transmitter") (throw (missionary.Cancelled. "Transmitter cancelled."))))))))) (defn new-device "Generate a new midi device. Currently, this is a map of channel-num to [atom signal]." [] (into {} (map (fn [i] [i (let [v (atom #{})] [v (m/signal (m/watch v))])]) (range 0 127)))) (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))))) (def bus-sel (atom nil)) (def >bus-sel (m/eduction (dedupe) (m/watch bus-sel))) (reset! bus-sel "Bus 1") #_(reset! bus-sel "Bus 2") #_(reset! bus-sel nil) (def run (m/ap (prn (m/?< (>device (m/?< >bus-sel)))) )) (def close ((m/reduce prn {} run) {} {})) #_ (close) ;; OH! You hreally have to think about the supervision tree at all times ;; It informs your function composition ;; ;;It helps to write in a continuation style - see `with-messages` ;;>device is my most mature fn