109 lines
4.7 KiB
Text
109 lines
4.7 KiB
Text
|
|
(ns arthur.clock-test
|
||
|
|
"The clock is four lines of arithmetic and the whole take's sync depends on
|
||
|
|
them, so they are asserted rather than eyeballed. A drift bug is invisible for
|
||
|
|
the first second and unmistakable by the tenth, which is the worst possible
|
||
|
|
shape for a bug to have."
|
||
|
|
(:require [cljs.test :refer [deftest is testing]]
|
||
|
|
[arthur.clock :as clock]))
|
||
|
|
|
||
|
|
(defn- fake
|
||
|
|
"Stands in for the audio element. The clock only ever reads `currentTime`,
|
||
|
|
`paused`, `ended` and `playbackRate` and writes `currentTime`, which is the
|
||
|
|
entire coupling — so the whole of it can be asserted in node."
|
||
|
|
[& {:keys [t paused? ended? rate duration]
|
||
|
|
:or {t 0 paused? true ended? false rate 1.0 duration 7.6}}]
|
||
|
|
#js {:currentTime t :paused paused? :ended ended?
|
||
|
|
:playbackRate rate :duration duration})
|
||
|
|
|
||
|
|
(deftest the-frame-is-derived-from-the-audio-not-counted
|
||
|
|
;; frame = ⌊currentTime · fps⌋. The whole point: nothing accumulates, so
|
||
|
|
;; nothing can drift.
|
||
|
|
(clock/attach! (fake :t 0.0))
|
||
|
|
(is (= 0 (clock/frame 30 229)))
|
||
|
|
(doseq [[t want] [[0.0 0] [0.033 0] [0.034 1] [1.0 30] [1.999 59] [2.0 60]]]
|
||
|
|
(clock/attach! (fake :t t))
|
||
|
|
(is (= want (clock/frame 30 229)) (str t "s at 30fps"))))
|
||
|
|
|
||
|
|
(deftest a-dropped-frame-lands-where-the-audio-already-is
|
||
|
|
;; THE property the derivation buys. A loop that stalled for a third of a
|
||
|
|
;; second resumes at the frame the audio reached, not a third of a second
|
||
|
|
;; behind it — the failure is a visible stutter rather than an invisible slide
|
||
|
|
;; out of sync, and those are very different bugs to own.
|
||
|
|
(clock/attach! (fake :t 1.0))
|
||
|
|
(let [before (clock/frame 30 229)]
|
||
|
|
(clock/attach! (fake :t 1.5)) ; fifteen frames' worth of stall
|
||
|
|
(is (= 45 (clock/frame 30 229)))
|
||
|
|
(is (= 30 before) "and nothing was counted in between")))
|
||
|
|
|
||
|
|
(deftest the-frame-is-clamped-into-the-clip
|
||
|
|
;; Audio is 7.601s and the clip is 229 frames at 30fps = 7.633s, so the last
|
||
|
|
;; fraction of a second has no audio and the end of the audio has no frame
|
||
|
|
;; past the last. Neither may produce an out-of-range index.
|
||
|
|
(clock/attach! (fake :t 99.0))
|
||
|
|
(is (= 228 (clock/frame 30 229)))
|
||
|
|
(clock/attach! (fake :t -1.0))
|
||
|
|
(is (= 0 (clock/frame 30 229))))
|
||
|
|
|
||
|
|
(deftest a-seek-round-trips
|
||
|
|
;; Seeking to the START of a frame rather than its middle is what makes this
|
||
|
|
;; idempotent: seek to f, read back f, at every f.
|
||
|
|
(let [a (fake)]
|
||
|
|
(clock/attach! a)
|
||
|
|
(doseq [f [0 1 57 114 171 228]]
|
||
|
|
(clock/seek! 30 229 f)
|
||
|
|
(is (= f (clock/frame 30 229)) (str "seek to " f)))))
|
||
|
|
|
||
|
|
(deftest a-seek-past-either-end-is-clamped-before-it-reaches-the-element
|
||
|
|
(let [a (fake)]
|
||
|
|
(clock/attach! a)
|
||
|
|
(clock/seek! 30 229 9999)
|
||
|
|
(is (= 228 (clock/frame 30 229)))
|
||
|
|
(clock/seek! 30 229 -5)
|
||
|
|
(is (= 0 (clock/frame 30 229)))
|
||
|
|
(is (>= (.-currentTime a) 0) "and currentTime is never negative")))
|
||
|
|
|
||
|
|
(deftest rate-does-not-enter-the-frame-calculation
|
||
|
|
;; ½× and ¼× are playbackRate and nothing else. If rate appeared here as well
|
||
|
|
;; it would be applied twice — the picture would have one rate and the sound
|
||
|
|
;; another, which is precisely the desync the audio clock exists to prevent.
|
||
|
|
(doseq [r [1.0 0.5 0.25]]
|
||
|
|
(clock/attach! (fake :t 2.0 :rate r))
|
||
|
|
(is (= 60 (clock/frame 30 229)) (str "at " r "x, 2.0s is still frame 60"))))
|
||
|
|
|
||
|
|
(deftest playing-follows-the-element
|
||
|
|
(clock/attach! (fake :paused? true))
|
||
|
|
(is (not (clock/playing?)))
|
||
|
|
(clock/attach! (fake :paused? false))
|
||
|
|
(is (clock/playing?))
|
||
|
|
(testing "and a finished file is not playing, whatever `paused` says"
|
||
|
|
(clock/attach! (fake :paused? false :ended? true))
|
||
|
|
(is (not (clock/playing?)))))
|
||
|
|
|
||
|
|
(deftest the-audio-length-is-reported-rather-than-assumed
|
||
|
|
;; A clip longer than its audio is a legitimate thing to be told about and not
|
||
|
|
;; a thing to silently truncate.
|
||
|
|
(clock/attach! (fake :duration 7.601))
|
||
|
|
(is (= 229 (clock/duration-frames 30)))
|
||
|
|
(is (= 92 (clock/duration-frames 12)))
|
||
|
|
(testing "and an unloaded element has no opinion"
|
||
|
|
(clock/attach! (fake :duration js/NaN))
|
||
|
|
(is (nil? (clock/duration-frames 30)))))
|
||
|
|
|
||
|
|
(deftest exposure-is-applied-to-the-derived-frame
|
||
|
|
;; The transport shows which frame the grid holds the playhead back onto, so
|
||
|
|
;; that `exposure 2` is visibly doing something rather than only inside the
|
||
|
|
;; scene.
|
||
|
|
(is (= [0 0 2 2 4 4] (mapv #(clock/exposed-frame % 2) (range 6))))
|
||
|
|
(is (= [0 1 2 3 4 5] (mapv #(clock/exposed-frame % 1) (range 6)))))
|
||
|
|
|
||
|
|
(deftest with-no-element-attached-nothing-explodes
|
||
|
|
;; The loop starts before the :ref has fired, so every reader has to be safe
|
||
|
|
;; on a clock that has not been handed its element yet.
|
||
|
|
(clock/attach! nil)
|
||
|
|
(is (= 0 (clock/frame 30 229)))
|
||
|
|
(is (not (clock/playing?)))
|
||
|
|
(is (= 1.0 (clock/rate)))
|
||
|
|
(is (nil? (clock/duration-frames 30)))
|
||
|
|
(is (nil? (clock/seek! 30 229 5)))
|
||
|
|
(is (nil? (clock/pause!))))
|