summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-10-14 08:52:12 -0400
committerJake Zerrer <him@jakezerrer.com>2025-10-14 10:55:16 -0400
commit408d6a9f49f4ecac6abccfd993e72a44b1bc8103 (patch)
tree960167f20ee73ff75b6b4901c3284776feaf3099 /src
parentebbfd10cb80ee61de047d7afafea0513b290ab82 (diff)
Convert set operations to note on/off values
Diffstat (limited to 'src')
-rw-r--r--src/main.clj114
-rw-r--r--src/notation.clj73
2 files changed, 165 insertions, 22 deletions
diff --git a/src/main.clj b/src/main.clj
index fdb306f..eb0e2d2 100644
--- a/src/main.clj
+++ b/src/main.clj
@@ -1,45 +1,115 @@
(ns main
(:require [missionary.core :as m]
- [clojure.set :refer [difference]]))
+ [clojure.set :refer [difference union]]))
;; How many times per second are output continuous values sampled and turned
;; into events?
-(def sample-rate (atom 1))
+(def sample-rate (atom 30))
+
+(defn set-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)))
-(defn play-note [v] (swap! notes-on conj v))
+(def playback-enabled? (atom true))
+
+(defn enable-playback []
+ (reset! playback-enabled? true))
+
+(defn disable-playback []
+ (reset! playback-enabled? false))
-(defn stop-note [v] (swap! notes-on disj v))
+(defn play-notes [& v]
+ (when @playback-enabled?
+ (swap! notes-on union (into #{} v))))
+
+(defn stop-notes [& v]
+ (when @playback-enabled?
+ (swap! notes-on difference (into #{} v))))
(def clock
(m/ap
(loop []
(m/amb
+ ;; TODO: This seems to be emitting twice per cycle
+ ;; TODO: Currently, there will be latency when changing the sample rate
+ ;; due to having to wait for the most recent sleep to complete
+ ;; Update clock to be reactive on sample-rate, too
(m/?
(m/sleep (/ 1000 @sample-rate)))
:tick
(recur)))))
-#_(play-note 1)
-#_(stop-note 1)
-
;; convert the continuous time >notes-on flow to a series of discrete midi note on and off events
(def output
- (m/eduction (map (fn [{:keys [note-on note-off]}] {:note-on note-on :note-off note-off}))
- (m/reductions (fn [{:keys [active note-on note-off]} [curr _]]
- {:note-on (difference (difference curr active) note-on)
- :note-off (difference (difference active curr) note-off)
- :active curr})
- #{}
- (m/sample
- vector
- >notes-on
- clock))))
-
-(def cancel
- ((m/reduce prn output) {} {}))
-
-(cancel)
+ (m/eduction
+ (comp (remove #(= (select-keys % [:note-on :note-off]) {:note-on #{} :note-off #{}}))
+ (dedupe))
+ (m/reductions (fn [{:keys [active note-on note-off]} [curr _]]
+ {:note-on (difference (difference curr active) note-on)
+ :note-off (difference (difference active curr) note-off)
+ :active curr})
+ {:note-on #{}
+ :note-off #{}
+ :active #{}}
+ ;; This actually does a bunch of unnecessary work
+ ;; What we really want is to sample at _at most_ sample rate,
+ ;; but not at all if nothing has changed
+ (m/sample
+ vector
+ >notes-on
+ clock))))
+
+(def toggle-playback
+ (m/ap
+ (let [local-playback-enabled? (atom nil)
+ local-active (atom #{})
+ [tag value] (m/amb=
+ [:playback-enabled? (m/?< (m/watch playback-enabled?))]
+ [:note-event (m/?< output)])]
+ (case tag
+ :playback-enabled?
+ (let [playback-enabled? value
+ active @local-active]
+ (reset! local-playback-enabled? playback-enabled?)
+ (if (not playback-enabled?)
+ (do
+ (reset! notes-on #{})
+ (reset! local-active #{})
+ {:note-on #{}
+ :note-off active
+ :active #{}})
+ (m/amb)))
+ :note-event
+ (let [{:keys [active]
+ :as note-event} value]
+ (reset! local-active active)
+ (if @local-playback-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) {} {})))))
+
+(defn stop-process []
+ (disable-playback)
+ (@process)
+ (reset! process nil))
+
+(comment
+ (start-process)
+ (stop-process)
+ (set-sample-rate 1)
+ (play-notes 1 2 3 4 5)
+ (stop-notes 3)
+ (play-notes 1 2 3 4 5)
+ (stop-notes 1 2 3 4 5)
+
+ (enable-playback)
+ (disable-playback))
diff --git a/src/notation.clj b/src/notation.clj
new file mode 100644
index 0000000..aff26e3
--- /dev/null
+++ b/src/notation.clj
@@ -0,0 +1,73 @@
+(ns notation
+ "Experimental notation")
+
+(comment
+ ;; Parallel groups
+ ;; Notes 1, 2, and 3 simultaneously
+ ;; = should remind you of amb=
+ ;; implicit duration of 1
+ [= 1 2 3]
+
+ ;; Same as above, but with duration 3
+ ([= 1 2 3] 3)
+
+ ;; Notes 1, 2, and 3 all with different durations
+ [=
+ (1 2)
+ (2 3)
+ (3 4)]
+
+ ;; Inner values override outer values
+ ;; In this chord, 1 would have a duration of 3 while 2 and 3 would have a duration of 2
+ ([= (1 3) 2 3] 2)
+
+ ;; Notes 1, 2, and 3 all with different durations and velocities
+ [=
+ (1 2 100)
+ (2 3 110)
+ (3 4 123)]
+
+ ;; Sequential groups
+ ;; Note 1, then note 2, then note 3
+ [1 2 3]
+
+ ;; Note 1 duration 1, then note 2 duration 2, then note 3 duration 1
+ [(1 1)
+ (2 2)
+ (3 1)]
+
+ ;; Three chords played sequentially
+ [[= 1 2 3]
+ [= 1 2 3]
+ [= 1 2 3]]
+
+
+ ;; Note 1, followed by a rest, followed by note 3
+ [1 (r) 3]
+
+ ;; Unlike notes, rests are at most 2-tuples
+ ;; (Think about it: Rests never have a note value)
+
+ ;; Assign the note sequence 1 2 3 to the name loop1
+ ;; The first argument is always the name; the last argument is always either
+ ;; a sequential or parallel group
+ (=loop1 [1 2 3])
+
+ ;; Use loop1
+ [1 (loop1) 2 3]
+
+ ;; Middle arguments are variable names
+ (=loop2 dur ([1 2 3] dur))
+
+
+ ;; TODO:
+ ;; - Note literals turn into numbers
+ ;; - Represent keyboard as byte array of shorts
+ ;; - play a note increments, stop a note decrements
+ ;; - Multiple instruments
+ ;; - Mapping inputs to vars
+ ;; - Inputs get declared at the top of a track
+ ;; - Devices get mapped to declared inputs
+ ;; - Notion of scenes that change mapping of inputs to vars
+ ;; - Loops
+ )