|
6 | 6 | ^{:nextjournal.clerk/visibility :hide-ns}
|
7 | 7 | (ns osc-spirograph
|
8 | 8 | (:require [nextjournal.clerk :as clerk]
|
9 |
| - [clojure.java.io :as io]) |
10 |
| - (:import (com.illposed.osc ConsoleEchoServer OSCMessageListener OSCMessageEvent OSCMessage) |
| 9 | + [clojure.java.io :as io] |
| 10 | + [nextjournal.clerk.viewer :as v]) |
| 11 | + (:import (com.illposed.osc ConsoleEchoServer OSCMessageListener OSCMessageEvent OSCMessage OSCBundle) |
11 | 12 | (com.illposed.osc.messageselector JavaRegexAddressMessageSelector)
|
| 13 | + (com.illposed.osc.transport OSCPortOut) |
12 | 14 | (org.slf4j LoggerFactory)
|
13 | 15 | (java.net InetSocketAddress)
|
14 |
| - (javax.imageio ImageIO))) |
| 16 | + (javax.imageio ImageIO) |
| 17 | + (java.util List ArrayList))) |
15 | 18 |
|
16 | 19 | ^{::clerk/visibility :fold :nextjournal.clerk/viewer :hide-result}
|
17 | 20 | (def client-model-sync
|
18 | 21 | ;; This viewer is used to sync models between clojure values and those on the client side
|
19 |
| - {:fetch-fn (fn [_ x] x) |
20 |
| - :transform-fn (fn [{::clerk/keys [var-from-def]}] {:value @@var-from-def}) |
21 |
| - :render-fn '(fn [{:keys [value]}] |
22 |
| - (defonce model (atom nil)) |
23 |
| - (-> (swap! model (partial merge-with (fn [old new] (if (vector? old) (mapv merge old new) new))) value) |
24 |
| - (update :phasors (partial mapv #(dissoc % :group))) |
25 |
| - (dissoc :drawing :curve)))}) |
| 22 | + {:transform-fn (comp v/mark-presented (v/update-val (comp deref deref ::clerk/var-from-def))) |
| 23 | + :render-fn '(fn [val] |
| 24 | + (defonce model (atom nil)) |
| 25 | + (v/html |
| 26 | + [v/inspect-paginated |
| 27 | + (-> (swap! model |
| 28 | + (partial merge-with (fn [old new] (if (vector? old) (mapv merge old new) new))) |
| 29 | + val) |
| 30 | + (update :phasors (partial mapv #(dissoc % :group))) |
| 31 | + (dissoc :drawing :curve))]))}) |
26 | 32 |
|
27 | 33 | ;; This is the model representing the constituents of our spirograph.
|
28 | 34 | ;; Three [phasors](https://en.wikipedia.org/wiki/Phasor), each one carrying an amplitude and an angular frequency.
|
29 | 35 | ^{::clerk/viewer client-model-sync}
|
30 |
| -(defonce model |
| 36 | +(def model |
31 | 37 | (atom {:phasors [{:amplitude 0.41 :frequency 0.46}
|
32 | 38 | {:amplitude 0.46 :frequency -0.44}
|
33 | 39 | {:amplitude 1.00 :frequency -0.45}]}))
|
|
132 | 138 | ;; arguments of the form `[value & path]` where the first entry is an integer in the range `0` to `100` while the tail is a valid path in
|
133 | 139 | ;; the model. We're actually ignoring the message address.
|
134 | 140 | ;;
|
135 |
| -;; In order to receive OSC messages, we instantiate an OSC Server. We're overlaying an extra broadcast layer on top of the simple echo server |
136 |
| -;; provided by the [JavaOSC library](https://github.com/hoijui/JavaOSC). This will, in addition, allow to debug incoming messages in the terminal. |
137 |
| -;; |
138 |
| -;; Received events are tapped into the JVM for them to be handled with clojure functions, this piece shows Java interop at its best! |
139 |
| -(when-not (System/getenv "NOSC") |
140 |
| - (defonce osc |
141 |
| - (doto (proxy [ConsoleEchoServer] |
142 |
| - [(InetSocketAddress. "0.0.0.0" 6669) |
143 |
| - (LoggerFactory/getLogger ConsoleEchoServer)] |
144 |
| - (start [] |
145 |
| - (proxy-super start) |
146 |
| - (.. this |
147 |
| - getDispatcher |
148 |
| - (addListener (JavaRegexAddressMessageSelector. ".*") |
149 |
| - (reify |
150 |
| - OSCMessageListener |
151 |
| - (^void acceptMessage [_this ^OSCMessageEvent event] |
152 |
| - (tap> (.getMessage event)))))))) |
153 |
| - .start))) |
154 |
| - |
155 |
| -;; Next, we need a function to convert OSC messages into normalized clojure data |
| 141 | +;; That said, here's a function to convert OSC messages into clojure data, normalized to rational points inside the unit interval |
156 | 142 | (defn osc->map [^OSCMessage m]
|
157 | 143 | (let [[v & path] (map #(cond-> % (string? %) keyword) (.getArguments m))]
|
158 | 144 | {:value (if (= :phasors (first path)) (float (/ v 100)) v)
|
|
164 | 150 | (binding [*ns* (find-ns 'osc-spirograph)]
|
165 | 151 | (clerk/recompute!)))
|
166 | 152 |
|
167 |
| -;; finally, a message handler to be added to tap callbacks |
168 |
| -(defn osc-message-handler [osc-message] |
169 |
| - (let [{:keys [path value]} (osc->map osc-message)] |
170 |
| - (update-model! #(assoc-in % path value)))) |
| 153 | +;; Finally, in order to receive OSC messages, we instantiate an OSC Server. We're overlaying an extra broadcast layer on top of the simple echo server |
| 154 | +;; provided by the [JavaOSC library](https://github.com/hoijui/JavaOSC). This will, in addition, allow to debug incoming messages in the terminal. |
| 155 | +;; Well also sync back to the device every time we compute the notebook. |
| 156 | +(defonce osc-in |
| 157 | + (proxy [ConsoleEchoServer] |
| 158 | + [(InetSocketAddress. "0.0.0.0" 6669) (LoggerFactory/getLogger ConsoleEchoServer)] |
| 159 | + (start [] |
| 160 | + (proxy-super start) |
| 161 | + (.. this |
| 162 | + getDispatcher |
| 163 | + (addListener (JavaRegexAddressMessageSelector. ".*") |
| 164 | + (reify |
| 165 | + OSCMessageListener |
| 166 | + (^void acceptMessage [_this ^OSCMessageEvent event] |
| 167 | + (let [{:keys [path value]} (osc->map (.getMessage event))] |
| 168 | + (update-model! #(assoc-in % path value)))))))))) |
| 169 | + |
| 170 | +(defonce osc-out (OSCPortOut. (InetSocketAddress. "10.33.8.65" 7777))) |
171 | 171 |
|
172 |
| -;; Clerk won't cache forms returning nil values, hence the do here to ensure we register our handler just once when the notebook is evaluated |
173 |
| -(do |
174 |
| - (add-tap osc-message-handler) |
175 |
| - true) |
| 172 | +(defn sync-osc [{:keys [phasors mode]}] |
| 173 | + (.send osc-out |
| 174 | + (OSCBundle. |
| 175 | + (ArrayList. |
| 176 | + (cond->> (sequence (comp (map-indexed (fn [idx {:keys [amplitude frequency]}] |
| 177 | + [(OSCMessage. (str "/phasors/" idx "/amplitude") (List/of (int (* 100 amplitude)))) |
| 178 | + (OSCMessage. (str "/phasors/" idx "/frequency") (List/of (int (* 100 frequency))))])) cat) |
| 179 | + phasors) |
| 180 | + (some? mode) |
| 181 | + (cons (OSCMessage. "/mode" (List/of (int mode))))))))) |
| 182 | + |
| 183 | +(defonce ^::clerk/no-cache started |
| 184 | + (when-not (System/getenv "NOSC") |
| 185 | + (.start osc-in) |
| 186 | + (sync-osc @model))) |
176 | 187 |
|
177 | 188 | ;; And that's it I guess. Now, if you're looking at a static version of this notebook, you might want to clone [this repo](https://github.com/zampino/osc-spirograph), launch
|
178 | 189 | ;; Clerk with `(nextjournal.clerk/serve! {})` and see it in action with `(nextjournal.clerk/show! "notebooks/osc_spirograph.clj")`.
|
179 | 190 | ;;
|
180 |
| -;; This project has been partly inspired by Jack Schaedler's interactive article ["SEEING CIRCLES, SINES, AND SIGNALS"](https://jackschaedler.github.io/circles-sines-signals/index.html) |
181 |
| -;; to which I refer the reader to explore the implications of Fourier analysis with digital signal processing. |
| 191 | +;; This project has been inspired by - let alone my curiosity for a nic(h)e protocol - Jack Schaedler's interactive article ["SEEING CIRCLES, SINES, AND SIGNALS"](https://jackschaedler.github.io/circles-sines-signals/index.html) |
| 192 | +;; to which I refer the reader to further explore the implications of Fourier analysis with digital signal processing. |
| 193 | +;; My article should definitely expand to also contain some sound, probably using overtone. Suggestions anyone? [@lo_zampino](https://twitter.com/lo_zampino) |
182 | 194 |
|
183 | 195 | ^{::clerk/visibility :hide ::clerk/viewer :hide-result}
|
184 | 196 | (comment
|
185 |
| - (clerk/serve! {:port 7779}) |
| 197 | + (clerk/serve! {:port 7777}) |
186 | 198 | (clerk/clear-cache!)
|
187 | 199 |
|
188 |
| - (remove-tap osc-message-handler) |
| 200 | + @model |
| 201 | + (def local (OSCPortOut. (InetSocketAddress. "127.0.0.1" 6660))) |
| 202 | + (.send local |
| 203 | + (let [{:keys [phasors mode]} {:mode 0}] |
| 204 | + (OSCBundle. |
| 205 | + (ArrayList. |
| 206 | + (cons (OSCMessage. "/mode" (List/of (int mode))) |
| 207 | + (some->> phasors |
| 208 | + (sequence (comp (map-indexed (fn [idx {:keys [amplitude frequency]}] |
| 209 | + [(OSCMessage. (str "/phasors/" idx "/amplitude") (List/of (int (* 100 amplitude)))) |
| 210 | + (OSCMessage. (str "/phasors/" idx "/frequency") (List/of (int (* 100 frequency))))])) cat)))))))) |
189 | 211 |
|
190 |
| - (.start osc) |
191 |
| - (.isListening osc) |
192 |
| - (.stopListening osc) |
| 212 | + (.start osc-in) |
| 213 | + (.isListening osc-in) |
| 214 | + (.stopListening osc-in) |
193 | 215 |
|
194 |
| - (update-model (fn [m] (assoc-in m [:phasors 0 :amplitude] 2.0))) |
195 |
| - (update-model (fn [m] (assoc-in m [:phasors 1 :frequency] 0.9))) |
| 216 | + (update-model! (fn [m] (assoc-in m [:phasors 0 :amplitude] 0.9))) |
| 217 | + (update-model! (fn [m] (assoc-in m [:phasors 1 :frequency] 0.1))) |
196 | 218 |
|
197 | 219 | ;; save nice models
|
198 | 220 | @model
|
199 | 221 | (do
|
200 | 222 | (reset! model
|
201 |
| - #_ |
202 |
| - {:mode 0, |
203 |
| - :phasors [{:amplitude 0.4, :frequency 0.2} |
| 223 | + #_{:mode 0, |
| 224 | + :phasors [{:amplitude 0.4, :frequency 0.2} |
204 | 225 | {:amplitude 1.0, :frequency -0.2}
|
| 226 | + |
205 | 227 | {:amplitude 0.4, :frequency 0.6}]}
|
206 |
| - #_ {:mode 0 |
207 |
| - :phasors [{:amplitude 0.41, :frequency 0.46} |
208 |
| - {:amplitude 0.71, :frequency -0.44} |
209 |
| - {:amplitude 0.6, :frequency -0.45}]} |
| 228 | + #_{:mode 0 |
| 229 | + :phasors [{:amplitude 0.41, :frequency 0.46} |
| 230 | + {:amplitude 0.71, :frequency -0.44} |
| 231 | + {:amplitude 0.6, :frequency -0.45}]} |
210 | 232 |
|
211 |
| - #_ {:mode 0, |
| 233 | + #_{:mode 0, |
| 234 | + :phasors [{:amplitude 0.35, :frequency -0.3} |
| 235 | + {:amplitude 0.83, :frequency 0.2} |
| 236 | + {:amplitude 1.0, :frequency 0.35}]} |
| 237 | + |
| 238 | + |
| 239 | + {:mode 0, |
212 | 240 | :phasors [{:amplitude 0.41, :frequency 0.46}
|
213 | 241 | {:amplitude 0.46, :frequency -0.44}
|
214 | 242 | {:amplitude 1.0, :frequency -0.45}]}
|
215 |
| - #_ |
216 |
| - {:mode 0 |
217 |
| - :phasors [{:amplitude 0.57, :frequency 0.39} |
218 |
| - {:amplitude 0.5, :frequency -0.27} |
219 |
| - {:amplitude 0.125, :frequency 0.27}]} |
220 |
| - |
221 |
| - #_ {:mode 0, |
222 |
| - :phasors [{:amplitude 0.72, :frequency -0.25} |
223 |
| - {:amplitude 0.59, :frequency 0.45} |
224 |
| - {:amplitude 0.52, :frequency 0.3}]} |
225 |
| - |
226 |
| - {:mode 0, |
227 |
| - :phasors [{:amplitude 0.80, :frequency 0.55} |
228 |
| - {:amplitude 0.5, :frequency -0.27} |
229 |
| - {:amplitude 0.75, :frequency 0.27}]}) |
230 |
| - (clerk/recompute!)) |
231 |
| - |
232 |
| - ;; clean |
| 243 | + |
| 244 | + #_{:mode 0 |
| 245 | + :phasors [{:amplitude 0.57, :frequency 0.39} |
| 246 | + {:amplitude 0.5, :frequency -0.27} |
| 247 | + {:amplitude 0.125, :frequency 0.27}]} |
| 248 | + |
| 249 | + #_{:phasors [{:amplitude 0.9, :frequency 0.06} |
| 250 | + {:amplitude 0.46, :frequency 0.1} |
| 251 | + {:amplitude 0.46, :frequency -0.45}]} |
| 252 | + |
| 253 | + #_{:mode 0, |
| 254 | + :phasors [{:amplitude 0.70, :frequency -0.25} |
| 255 | + {:amplitude 0.60, :frequency 0.45} |
| 256 | + {:amplitude 0.50, :frequency 0.25}]} |
| 257 | + |
| 258 | + #_{:mode 0, |
| 259 | + :phasors [{:amplitude 0.57, :frequency 0.33} |
| 260 | + {:amplitude 1.0, :frequency -0.35} |
| 261 | + {:amplitude 0.31, :frequency 0.14}]}) |
| 262 | + (swap! model assoc :clean? true) |
| 263 | + (clerk/recompute!) |
| 264 | + (swap! model assoc :clean? false) |
| 265 | + (clerk/recompute!) |
| 266 | + (sync-osc @model)) |
| 267 | + |
| 268 | + ;; just clean |
233 | 269 | (do (swap! model assoc :clean? true)
|
234 | 270 | (clerk/recompute!)
|
235 | 271 | (swap! model assoc :clean? false)
|
|
0 commit comments