(ns unheard.clock (:require [missionary.core :as m])) ;; TODO: Put in missionary util (defn poll [init task] (m/ap (m/? (m/?> (m/seed (concat [(m/sp init)] (repeat task))))))) ;; TODO: Put in missionary util (defn feedback [f init & args] (m/ap (m/amb init (let [rdv (m/rdv) x (m/?> (apply f (poll init rdv) args))] (m/amb (do (m/? (rdv x)) (m/amb)) x))))) ;; Return value ;; TODO: Make part of public API (defonce mono-clock-freq (atom 120)) ;; hz (defonce >mono-clock-freq (m/signal (m/watch mono-clock-freq))) (def >mono-clock "This is the base monotonic clock used for driving playback. It makes a best-effort attempt to tick at `mono-clock-freq`. It makes no guarantees that it will tick a given number of times in any unit of time. When `mono-clock-freq` changes, it will wait for the completion of the current tick prior to adopting the new frequency." ;; TODO: Change this behavior. I don't want to wait for ;; the current tick to complete during a frequency change.. (m/signal (m/relieve (feedback (fn [v] (m/ap (let [[t freq] (m/?< (m/latest vector v >mono-clock-freq))] (m/? (m/compel (m/sleep (/ 1000 freq)))) (System/nanoTime)))) (System/nanoTime))))) (comment (def cancel ((m/reduce prn nil >mono-clock) prn prn)) (reset! mono-clock-freq 50) (reset! mono-clock-freq 10) (reset! mono-clock-freq 1) (cancel)) (defonce bpm (atom 120)) (defonce >bpm (m/signal (m/watch bpm))) (def >beat-clock "Counts beats at `bpm`. Guaranteed not to lose or gain time." (m/signal (m/relieve (let [init-beat 0] (m/reductions {} init-beat (m/ap (let [state (object-array 2) last-beat-time-idx 0 last-beat-number-idx 1 _ (aset state last-beat-time-idx (System/nanoTime)) _ (aset state last-beat-number-idx init-beat) [t bpm] (m/?< (m/latest vector >mono-clock >bpm)) last-beat-time (aget state last-beat-time-idx) next-beat-time (+ last-beat-time (* 1000000000 (/ 60 bpm))) last-beat-number (aget state last-beat-number-idx) t-since-beat-start (- t last-beat-time) pct-through-current-beat (/ t-since-beat-start (- next-beat-time last-beat-time))] (when (<= next-beat-time t) ;; TODO confirm this won't lose or gain time (aset state last-beat-time-idx next-beat-time) (aset state last-beat-number-idx (inc last-beat-number))) (+ last-beat-number pct-through-current-beat)))))))) (comment (def cancel ((m/reduce prn nil >beat-clock) prn prn)) (reset! bpm 60) (reset! bpm 120) (reset! mono-clock-freq 120) (reset! mono-clock-freq 1) (cancel))