summaryrefslogtreecommitdiff
path: root/src/main.clj
blob: eb0e2d24dc384eeff3513b787eeb35d2ea292de0 (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
(ns main
  (:require [missionary.core :as m]
            [clojure.set :refer [difference union]]))

;; 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))

;; 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))

(defn enable-playback []
  (reset! playback-enabled? true))

(defn disable-playback []
  (reset! playback-enabled? false))

(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)))))

;; convert the continuous time >notes-on flow to a series of discrete midi note on and off events
(def output
  (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))