summaryrefslogtreecommitdiff
path: root/src/main.clj
blob: 57eedad3a3de868f67383404b63200ad893819c1 (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
(ns main
  (:require [missionary.core :as m]
            [clojure.set :refer [difference union]]
            [portal :refer [>portal-main rec show-portal hide-portal +cap -cap cap]]
            [notation :as notation]
            [example-song :refer [melody]]))

;; How many times per second are output continuous values sampled and turned
;; into events?
(def sample-rate (atom 30))

(defn set-sample-rate
  "Change the output sample rate."
  [v]
  (reset! sample-rate v))

(def notes-on (atom #{}))
(def >notes-on (rec :notes-set (m/signal (m/watch notes-on))))

(def midi-enabled? (atom true))
(def >midi-enabled? (rec :midi-enabled?
                         (m/signal (m/watch midi-enabled?))))

(defn enable-midi
  "Enable midi out."
  []
  (reset! midi-enabled? true))

(defn disable-midi
  "Stop all active notes and disable midi out."
  []
  (reset! midi-enabled? false))

(defn play-notes
  "Play notes."
  [& v]
  (when @midi-enabled?
    (swap! notes-on union (into #{} v))))

(defn stop-notes
  "Stop notes."
  [& v]
  (when @midi-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
      (do (m/?
           (m/sleep (/ 1000 @sample-rate)))
          (m/amb))
      :tick
      (recur)))))

(defn output
  "Convert the continuous time >notes-on flow to a series of discrete midi note
  on and off events."
  [composition]
  (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
                  composition
                  clock))))

(defn 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."
  [composition]
  (m/ap
   (let [local-midi-enabled? (atom nil)
         local-active (atom #{})
         [tag value] (m/amb=
                      [:midi-enabled? (m/?< >midi-enabled?)]
                      [:note-event (m/?< (output composition))])]
     (case tag
       :midi-enabled?
       (let [midi-enabled? value
             active @local-active]
         (reset! local-midi-enabled? midi-enabled?)
         (if (not midi-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-midi-enabled?
           note-event
           (m/amb)))))))

(defonce engine (atom nil))

(defn set->midi-events
  "Convert set representation of notes to midi events"
  [composition]
  (m/ap
   (let [{:keys [note-on note-off]} (m/?< (process-midi-toggle-events composition))]
     (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)))))))

(defn main
  [composition]
  (m/ap (m/amb= (m/?< (rec :root (set->midi-events composition)))
                (m/?< >portal-main))))

(defn start-engine
  "Start playback engine."
  [composition]
  (when (not @engine)
    (reset! engine
            (do (enable-midi)
                ((m/reduce {} {} (main composition)) {} {})))))

(defn stop-engine
  "Stop playback engine."
  []
  (disable-midi)
  (when @engine
    (@engine)
    (reset! engine nil)))

(comment
  (let [[>clock set-clock] (notation/clock)]
       (def >clock >clock)
       (def set-clock set-clock))
  (+cap :root :notes-set)
  (start-engine (melody >clock))

  (set-clock 0)
  (show-portal)

  (cap)

  (play-notes 1 2 3 4 5)
  (stop-notes 4 5)

  (play-notes 1 2 3 4)

  (-cap :root)

  (disable-midi)
  (play-notes 1 2 3 4)
  (hide-portal)

  (enable-midi)
  (play-notes 1 2 3 4)
  (play-notes 6 7 8)
  (play-notes 100)

  (stop-engine))