summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-10-14 10:56:15 -0400
committerJake Zerrer <him@jakezerrer.com>2025-10-14 14:49:17 -0400
commita6e52696756056ccaf3a517a96b01f6756837d2f (patch)
tree578b8743abdd6e8211d442ddd4da2367e248b4ef /src
parent408d6a9f49f4ecac6abccfd993e72a44b1bc8103 (diff)
Toggle midi on and off
Diffstat (limited to 'src')
-rw-r--r--src/.main.clj.swpbin0 -> 16384 bytes
-rw-r--r--src/main.clj164
2 files changed, 125 insertions, 39 deletions
diff --git a/src/.main.clj.swp b/src/.main.clj.swp
new file mode 100644
index 0000000..b91b1b6
--- /dev/null
+++ b/src/.main.clj.swp
Binary files differ
diff --git a/src/main.clj b/src/main.clj
index eb0e2d2..7674cc1 100644
--- a/src/main.clj
+++ b/src/main.clj
@@ -2,30 +2,56 @@
(:require [missionary.core :as m]
[clojure.set :refer [difference union]]))
+(def >portal
+ (m/signal
+ (m/ap
+ (try
+ (m/?<
+ (m/observe
+ (fn [cb]
+ ((m/via m/blk ((requiring-resolve 'portal.api/open))) {} {})
+ (cb :open)
+ (fn []
+ ((m/via m/blk
+ ((requiring-resolve 'portal.api/close))
+ ((requiring-resolve 'portal.api/clear))) {} {})))))
+ (catch missionary.Cancelled _
+ (m/amb))))))
+
;; How many times per second are output continuous values sampled and turned
;; into events?
(def sample-rate (atom 30))
-(defn set-sample-rate [v] (reset! sample-rate v))
+(defn set-sample-rate
+ "Change the output sample rate."
+ [v]
+ (reset! sample-rate v))
-;; Temporary atom to explore the concept of note state as a continuous value
(def notes-on (atom #{}))
(def >notes-on (m/signal (m/watch notes-on)))
-(def playback-enabled? (atom true))
+(def midi-enabled? (atom true))
-(defn enable-playback []
- (reset! playback-enabled? true))
+(defn enable-midi
+ "Enable midi out."
+ []
+ (reset! midi-enabled? true))
-(defn disable-playback []
- (reset! playback-enabled? false))
+(defn disable-midi
+ "Stop all active notes and disable midi out."
+ []
+ (reset! midi-enabled? false))
-(defn play-notes [& v]
- (when @playback-enabled?
+(defn play-notes
+ "Play notes."
+ [& v]
+ (when @midi-enabled?
(swap! notes-on union (into #{} v))))
-(defn stop-notes [& v]
- (when @playback-enabled?
+(defn stop-notes
+ "Stop notes."
+ [& v]
+ (when @midi-enabled?
(swap! notes-on difference (into #{} v))))
(def clock
@@ -41,8 +67,9 @@
:tick
(recur)))))
-;; convert the continuous time >notes-on flow to a series of discrete midi note on and off events
(def output
+ "Convert the continuous time >notes-on flow to a series of discrete midi note
+ on and off events."
(m/eduction
(comp (remove #(= (select-keys % [:note-on :note-off]) {:note-on #{} :note-off #{}}))
(dedupe))
@@ -61,19 +88,22 @@
>notes-on
clock))))
-(def toggle-playback
+(def process-midi-toggle-events
+ "Listen for changes on midi-enabled?
+ When playback is disabled, send a note-off event for each active note
+ and then zero out notes-on."
(m/ap
- (let [local-playback-enabled? (atom nil)
+ (let [local-midi-enabled? (atom nil)
local-active (atom #{})
[tag value] (m/amb=
- [:playback-enabled? (m/?< (m/watch playback-enabled?))]
+ [:midi-enabled? (m/?< (m/watch midi-enabled?))]
[:note-event (m/?< output)])]
(case tag
- :playback-enabled?
- (let [playback-enabled? value
+ :midi-enabled?
+ (let [midi-enabled? value
active @local-active]
- (reset! local-playback-enabled? playback-enabled?)
- (if (not playback-enabled?)
+ (reset! local-midi-enabled? midi-enabled?)
+ (if (not midi-enabled?)
(do
(reset! notes-on #{})
(reset! local-active #{})
@@ -85,31 +115,87 @@
(let [{:keys [active]
:as note-event} value]
(reset! local-active active)
- (if @local-playback-enabled?
+ (if @local-midi-enabled?
note-event
(m/amb)))))))
-(defonce process (atom nil))
-
-(defn start-process []
- (when (not @process)
- (reset! process
- (do (enable-playback)
- ((m/reduce prn toggle-playback) {} {})))))
+(defonce engine (atom nil))
-(defn stop-process []
- (disable-playback)
- (@process)
- (reset! process nil))
+(def set->midi-events
+ "Convert set representation of notes to midi events"
+ (m/ap
+ (let [{:keys [note-on note-off]} (m/?< process-midi-toggle-events)]
+ (m/amb=
+ (loop [notes note-on]
+ (if (first notes)
+ (m/amb [:note-on (first notes)]
+ (recur (rest notes)))
+ (m/amb)))
+ (loop [notes note-off]
+ (if (first notes)
+ (m/amb [:note-off (first notes)]
+ (recur (rest notes)))
+ (m/amb)))))))
+
+(def enable-portal-submission? (atom false))
+(def >enable-portal-submission? (m/watch enable-portal-submission?))
+
+(defn enable-portal-submission []
+ (reset! enable-portal-submission? true))
+
+(defn disable-portal-submission []
+ (reset! enable-portal-submission? false))
+
+(def tap-flow
+ (m/ap
+ (let [local-enable-portal-submission? (atom nil)
+ [tag value]
+ (m/amb= [:enable-portal-submission? (m/?< >enable-portal-submission?)]
+ [:event (m/?< set->midi-events)])]
+ (case tag
+ :enable-portal-submission?
+ (do
+ (reset! local-enable-portal-submission? value)
+ (when value
+ (m/?< >portal))
+ (m/amb))
+ :event
+ (do
+ (when @local-enable-portal-submission?
+ (m/? (m/via m/blk ((requiring-resolve 'portal.api/submit) value))))
+ value)))))
+
+(defn start-engine
+ "Start playback engine."
+ []
+ (when (not @engine)
+ (reset! engine
+ (do (enable-midi)
+ ((m/reduce prn tap-flow) {} {})))))
+
+(defn stop-engine
+ "Stop playback engine."
+ []
+ (disable-midi)
+ (when @engine
+ (@engine)
+ (reset! engine nil)))
(comment
- (start-process)
- (stop-process)
- (set-sample-rate 1)
- (play-notes 1 2 3 4 5)
- (stop-notes 3)
+ (start-engine)
+
(play-notes 1 2 3 4 5)
- (stop-notes 1 2 3 4 5)
+ (stop-notes 4 5)
+ (enable-portal-submission)
+ (play-notes 1 2 3 4)
+
+ (disable-midi)
+ (play-notes 1 2 3 4)
+ (disable-portal-submission)
+
+ (enable-midi)
+ (play-notes 1 2 3 4)
+ (play-notes 6 7 8)
+ (play-notes 100)
- (enable-playback)
- (disable-playback))
+ (stop-engine))