summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-11-11 16:58:11 -0500
committerJake Zerrer <him@jakezerrer.com>2025-11-12 13:58:44 -0500
commit6641ad40b168ed505659b305531da05bfe9d6e6d (patch)
tree838493514e495114b9520c4871c45eaf6675d46b
parent63b89561d3d4da125cbea5eb98572621f76b08fd (diff)
Prepare for talk
-rw-r--r--dev/scratch.clj8
-rw-r--r--src/unheard/instrument/omx_27.clj3
-rw-r--r--src/unheard/midi.clj183
3 files changed, 161 insertions, 33 deletions
diff --git a/dev/scratch.clj b/dev/scratch.clj
index 0115233..315df29 100644
--- a/dev/scratch.clj
+++ b/dev/scratch.clj
@@ -4,7 +4,8 @@
[unheard.instrument.minilab3 :as minilab3]
[unheard.instrument.omx-27 :as omx-27]
[unheard.theory :refer [note poly]]
- [missionary.core :as m]))
+ [missionary.core :as m])
+ (:import [javax.sound.midi ShortMessage]))
#_(print-all-midi-devices)
@@ -22,6 +23,7 @@
(poly
(triad clock tonic)
+ #_
(triad clock (m/latest #(+ % 12) tonic))
;; The rest of the "song" is a drum pattern.
@@ -64,7 +66,5 @@
p (song song-config)]
(m/?< (m/latest vector clock p))))))))
-#_(def cancel
- (run prn prn))
-#_(cancel)
+(def cancel (run {} {}))
diff --git a/src/unheard/instrument/omx_27.clj b/src/unheard/instrument/omx_27.clj
index 33c18c8..ab3f28d 100644
--- a/src/unheard/instrument/omx_27.clj
+++ b/src/unheard/instrument/omx_27.clj
@@ -2,7 +2,8 @@
(:require [unheard.instrument.util :refer [matching-control]]))
(def device-name "CoreMIDI4J - omx-27")
-(def omx-27
+
+(def config
{:knob
{1 (matching-control 0 0 21)
2 (matching-control 0 0 22)
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))