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