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