diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-10-14 08:52:12 -0400 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-10-14 10:55:16 -0400 |
| commit | 408d6a9f49f4ecac6abccfd993e72a44b1bc8103 (patch) | |
| tree | 960167f20ee73ff75b6b4901c3284776feaf3099 /src | |
| parent | ebbfd10cb80ee61de047d7afafea0513b290ab82 (diff) | |
Convert set operations to note on/off values
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.clj | 114 | ||||
| -rw-r--r-- | src/notation.clj | 73 |
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 + ) |
