summaryrefslogtreecommitdiff
path: root/src/midi.clj
blob: 41a87402f7deab4184605a6abfd9299d02002c8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
(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