 (defprotocol ISnapshot
   (snapshot-beat-phase [snapshot] [snapshot beat-ratio]
     "Determine the phase with respect to a multiple or fraction of beats.
-Calling this with a beat-ratio of 1 (the default if not provided) is
-equivalent to beat-phase, a beat-ratio of bpb is equivalent to
-bar-phase (unless the bar start has been moved away from the beat
-start), 1/2 oscillates twice as fast as 1, 3/4 oscillates 4 times
-every three beats... Phases range from [0-1).")
+    Calling this with a beat-ratio of 1 (the default if not provided)
+    is equivalent to beat-phase, a beat-ratio of bpb is equivalent to
+    bar-phase (unless the bar start has been moved away from the beat
+    start), 1/2 oscillates twice as fast as 1, 3/4 oscillates 4 times
+    every three beats... Phases range from [0-1).")
   (snapshot-bar-phase [snapshot] [snapshot bar-ratio]
     "Determine the phase with respect to a multiple or fraction of bars.
-Calling this with a beat-ratio of 1 (the default if not provided) is
-equivalent to bar-phase, 1/2 oscillates twice as fast as 1, 3/4
-oscillates 4 times every three bars... Phases range from [0-1).")
+    Calling this with a beat-ratio of 1 (the default if not provided) is
+    equivalent to bar-phase, 1/2 oscillates twice as fast as 1, 3/4
+    oscillates 4 times every three bars... Phases range from [0-1).")
   (snapshot-beat-within-bar [snapshot]
     "Returns the beat number relative to the start of the bar: The down
-beat is 1, and the range goes up to the beats-per-bar of the metronome.")
+    beat is 1, and the range goes up to the beats-per-bar of the metronome.")
   (snapshot-down-beat? [snapshot]
-    "True if the current beat is the first beat in the current bar."))
+    "True if the current beat is the first beat in the current bar.")
+  (snapshot-phrase-phase [snapshot] [snapshot phrase-ratio]
+    "Determine the phase with respect to a multiple or fraction of
+    phrases. Calling this with a phrase-ratio of 1 (the default if
+    not provided) is equivalent to phrase-phase, 1/2 oscillates twice
+    as fast as 1, 3/4 oscillates 4 times every three bars... Phases
+    range from [0-1).")
+  (snapshot-bar-within-phrase [snapshot]
+    "Returns the bar number relative to the start of the phrase:
+    Ranges from 1 to the bars-per-phrase of the metronome.")
+  (snapshot-phrase-start? [snapshot]
+    "True if the current beat is the first beat in the current
+    phrase."))
 
 (defn- enhanced-phase
   "Calculate a phase with respect to multiples or fractions of a

          adjusted-phase (* base-phase denominator)]
      (- (double adjusted-phase) (long adjusted-phase))))

(defrecord MetronomeSnapshot [start bar-start bpm bpb instant beat bar beat-phase bar-phase]
  ISnapshot
  (snapshot-beat-phase [snapshot]
    (snapshot-beat-phase snapshot 1))

  (:require [overtone.at-at :refer [now]]
            [clojure.math.numeric-tower :refer [round floor]]))

(defprotocol IMetronome
  (metro-start [metro] [metro start-beat]
    "Returns the start time of the metronome. Also restarts the
     metronome at start-beat if given.")
  (metro-bar-start [metro start-bar]
    "Restarts the metronome at start-bar, keeping the beat phase
    unchanged in case it is being synced to an external source.")
  (metro-phrase-start [metro start-phrase]
    "Restarts the metronome at start-phrase, keeping the beat phase
    unchanged in case it is being synced to an external source.")
  (metro-tick [metro]
    "Returns the duration of one beat in milleseconds.")
  (metro-tock [metro]
    "Returns the duration of one bar in milliseconds.")
  (metro-ding [metro]
    "Returns the duration of one phrase in milliseconds.")
  (metro-beat [metro] [metro beat]
    "Returns the next beat number or the timestamp (in milliseconds) of the
     given beat.")
  (metro-beat-phase [metro] [metro phase]
    "Returns the distance traveled into the current beat [0.0, 1.0), or
       adjusts the phase to match the one supplied.")
  (metro-bar [metro] [metro bar]
    "Returns the next bar number or the timestamp (in milliseconds) of the
     given bar")
  (metro-bar-phase [metro] [metro phase]
    "Returns the distance traveled into the current bar [0.0, 1.0), or
       adjusts the phase to match the one supplied.")
  (metro-phrase [metro] [metro phrase]
    "Returns the next phrase number or the timestamp (in milliseconds)
     of the given phrase")
  (metro-phrase-phase [metro] [metro phase]
    "Returns the distance traveled into the current phrase [0.0, 1.0),
       or adjusts the phase to match the one supplied.")
  (metro-bpb [metro] [metro new-bpb]
    "Get the current beats per bar or change it to new-bpb")
  (metro-bpp [metro] [metro new-bpp]
    "Get the current bars per phrase, or change it to new-bpp")
  (metro-bpm [metro] [metro new-bpm]
    "Get the current bpm or change the bpm to new-bpm.")
  (metro-snapshot [metro]
    "Take a snapshot of the current beat, bar, phrase, and phase state.")
  (metro-marker [metro]
    "Returns the current time as phrase.bar.beat"))


;; Rhythm

;; * a resting heart rate is 60-80 bpm
;; * around 150 induces an excited state

;; A rhythm system should let us refer to time in terms of rhythmic units
;; like beat, bar, measure, and it should convert these units to real time
;; units (ms) based on the current BPM and signature settings.

(defn beat-ms
  "Convert 'b' beats to milliseconds at the given 'bpm'."
  [b bpm] (* (/ 60000.0 bpm) b))

(defn- marker-number
  "Helper function to calculate the beat, bar, or phrase number in
  effect at a given moment, given a starting point and beat, bar,
  or phrase interval."
  [instant start interval]
  (inc (long (/ (- instant start) interval))))

(defn- marker-phase
  "Helper function to calculate the beat, bar, or phrase phase at a
  given moment, given a starting point and beat interval."
  [instant start interval]
  (let [marker-ratio (/ (- instant start) interval)]
    (- (double marker-ratio) (long marker-ratio))))
