Skip to content

Commit ec569b6

Browse files
committed
Update OSC Spirograph to latest clerk viewer API
1 parent 7664444 commit ec569b6

File tree

1 file changed

+111
-75
lines changed

1 file changed

+111
-75
lines changed

notebooks/osc_spirograph.clj

Lines changed: 111 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,34 @@
66
^{:nextjournal.clerk/visibility :hide-ns}
77
(ns osc-spirograph
88
(: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)
1112
(com.illposed.osc.messageselector JavaRegexAddressMessageSelector)
13+
(com.illposed.osc.transport OSCPortOut)
1214
(org.slf4j LoggerFactory)
1315
(java.net InetSocketAddress)
14-
(javax.imageio ImageIO)))
16+
(javax.imageio ImageIO)
17+
(java.util List ArrayList)))
1518

1619
^{::clerk/visibility :fold :nextjournal.clerk/viewer :hide-result}
1720
(def client-model-sync
1821
;; 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))]))})
2632

2733
;; This is the model representing the constituents of our spirograph.
2834
;; Three [phasors](https://en.wikipedia.org/wiki/Phasor), each one carrying an amplitude and an angular frequency.
2935
^{::clerk/viewer client-model-sync}
30-
(defonce model
36+
(def model
3137
(atom {:phasors [{:amplitude 0.41 :frequency 0.46}
3238
{:amplitude 0.46 :frequency -0.44}
3339
{:amplitude 1.00 :frequency -0.45}]}))
@@ -132,27 +138,7 @@
132138
;; 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
133139
;; the model. We're actually ignoring the message address.
134140
;;
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
156142
(defn osc->map [^OSCMessage m]
157143
(let [[v & path] (map #(cond-> % (string? %) keyword) (.getArguments m))]
158144
{:value (if (= :phasors (first path)) (float (/ v 100)) v)
@@ -164,72 +150,122 @@
164150
(binding [*ns* (find-ns 'osc-spirograph)]
165151
(clerk/recompute!)))
166152

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

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

177188
;; 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
178189
;; Clerk with `(nextjournal.clerk/serve! {})` and see it in action with `(nextjournal.clerk/show! "notebooks/osc_spirograph.clj")`.
179190
;;
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)
182194

183195
^{::clerk/visibility :hide ::clerk/viewer :hide-result}
184196
(comment
185-
(clerk/serve! {:port 7779})
197+
(clerk/serve! {:port 7777})
186198
(clerk/clear-cache!)
187199

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

190-
(.start osc)
191-
(.isListening osc)
192-
(.stopListening osc)
212+
(.start osc-in)
213+
(.isListening osc-in)
214+
(.stopListening osc-in)
193215

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

197219
;; save nice models
198220
@model
199221
(do
200222
(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}
204225
{:amplitude 1.0, :frequency -0.2}
226+
205227
{: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}]}
210232

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,
212240
:phasors [{:amplitude 0.41, :frequency 0.46}
213241
{:amplitude 0.46, :frequency -0.44}
214242
{: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
233269
(do (swap! model assoc :clean? true)
234270
(clerk/recompute!)
235271
(swap! model assoc :clean? false)

0 commit comments

Comments
 (0)