(ns main (:require [missionary.core :as m] [clojure.set :refer [difference union]])) (def >portal (m/signal (m/ap (try (m/?< (m/observe (fn [cb] ((m/via m/blk ((requiring-resolve 'portal.api/open))) {} {}) (cb :open) (fn [] ((m/via m/blk ((requiring-resolve 'portal.api/close)) ((requiring-resolve 'portal.api/clear))) {} {}))))) (catch missionary.Cancelled _ (m/amb)))))) ;; 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 (m/signal (m/watch notes-on))) (def midi-enabled? (atom true)) (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 (m/? (m/sleep (/ 1000 @sample-rate))) :tick (recur))))) (def output "Convert the continuous time >notes-on flow to a series of discrete midi note on and off events." (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 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." (m/ap (let [local-midi-enabled? (atom nil) local-active (atom #{}) [tag value] (m/amb= [:midi-enabled? (m/?< (m/watch midi-enabled?))] [:note-event (m/?< output)])] (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)) (def set->midi-events "Convert set representation of notes to midi events" (m/ap (let [{:keys [note-on note-off]} (m/?< process-midi-toggle-events)] (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))))))) (def enable-portal-submission? (atom false)) (def >enable-portal-submission? (m/watch enable-portal-submission?)) (defn enable-portal-submission [] (reset! enable-portal-submission? true)) (defn disable-portal-submission [] (reset! enable-portal-submission? false)) (def tap-flow (m/ap (let [local-enable-portal-submission? (atom nil) [tag value] (m/amb= [:enable-portal-submission? (m/?< >enable-portal-submission?)] [:event (m/?< set->midi-events)])] (case tag :enable-portal-submission? (do (reset! local-enable-portal-submission? value) (when value (m/?< >portal)) (m/amb)) :event (do (when @local-enable-portal-submission? (m/? (m/via m/blk ((requiring-resolve 'portal.api/submit) value)))) value))))) (defn start-engine "Start playback engine." [] (when (not @engine) (reset! engine (do (enable-midi) ((m/reduce prn tap-flow) {} {}))))) (defn stop-engine "Stop playback engine." [] (disable-midi) (when @engine (@engine) (reset! engine nil))) (comment (start-engine) (play-notes 1 2 3 4 5) (stop-notes 4 5) (enable-portal-submission) (play-notes 1 2 3 4) (disable-midi) (play-notes 1 2 3 4) (disable-portal-submission) (enable-midi) (play-notes 1 2 3 4) (play-notes 6 7 8) (play-notes 100) (stop-engine))