Fix playback stopping when fallback audio ends

This commit is contained in:
Olive Vaughn 2026-10-04 00:58:51 -04:00
parent ee603a351b
commit e5b61bfcdd
2 changed files with 50 additions and 10 deletions

View file

@ -192,6 +192,23 @@
(.arrayBuffer response))) (.arrayBuffer response)))
(.then decode-bytes!))) (.then decode-bytes!)))
(defn fit-buffer
"Fit fallback audio to the open timeline, padding with silence or trimming.
The buffer and clock must share a duration so looping wraps at the timeline's
end rather than repeating a short soundtrack underneath a longer animation."
[^js buffer seconds]
(let [rate (.-sampleRate buffer)
frames (max 1 (js/Math.ceil (* seconds rate)))]
(if (= frames (.-length buffer))
buffer
(let [channels (.-numberOfChannels buffer)
fitted (.createBuffer (js/OfflineAudioContext. channels 1 rate)
channels frames rate)]
(dotimes [c channels]
(.set (.getChannelData fitted c)
(.subarray (.getChannelData buffer c) 0 (min frames (.-length buffer)))))
fitted))))
(defn clock-source! (defn clock-source!
"Promise of `{:buffer :seconds}` — the audio the transport runs its clock on "Promise of `{:buffer :seconds}` — the audio the transport runs its clock on
while symbol `sid` is open, and how long that clock is. while symbol `sid` is open, and how long that clock is.
@ -216,7 +233,9 @@
(and fallback-url (= sid (clip/opens-on document))) (and fallback-url (= sid (clip/opens-on document)))
(-> (decode! fallback-url) (-> (decode! fallback-url)
(.then (fn [^js b] {:buffer b :seconds (.-duration b)}))) (.then (fn [b]
(let [seconds (/ (clip/output-frames document sid) (:fps document))]
{:buffer (fit-buffer b seconds) :seconds seconds}))))
:else :else
{:buffer nil {:buffer nil
@ -236,12 +255,8 @@
symbol played against the whole take's soundtrack would run ten frames and then symbol played against the whole take's soundtrack would run ten frames and then
keep the clock going for minutes." keep the clock going for minutes."
[document sid fallback-url store] [document sid fallback-url store]
(-> (buffer! document sid store) (-> (clock-source! document sid fallback-url store)
(.then (fn [buffer] (.then (fn [{:keys [buffer seconds]}]
(cond (wav-url (or buffer
buffer (wav-url buffer) (.createBuffer (js/OfflineAudioContext. 1 1 44100)
(and fallback-url (= sid (clip/opens-on document))) fallback-url 1 (max 1 (js/Math.ceil (* 44100 seconds))) 44100)))))))
:else (let [rate 44100
n (max 1 (js/Math.ceil (* rate (/ (clip/output-frames document sid)
(:fps document)))))]
(wav-url (.createBuffer (js/OfflineAudioContext. 1 1 rate) 1 n rate))))))))

View file

@ -0,0 +1,25 @@
(ns arthur.audio.mix-test
(:require [arthur.audio.mix :as mix]
[cljs.test :refer [deftest is]]))
(defn- buffer [samples]
(let [data (js/Float32Array. (clj->js samples))]
#js {:sampleRate 12 :length (count samples) :numberOfChannels 1
:getChannelData (fn [_] data)}))
(deftest fallback-audio-follows-the-timeline-length
(let [original (.-OfflineAudioContext js/globalThis)]
(set! (.-OfflineAudioContext js/globalThis)
(fn [_ _ _]
#js {:createBuffer (fn [_ frames _] (buffer (repeat frames 0)))}))
(try
(let [short (buffer (repeat 92 0.5))
fitted (mix/fit-buffer short (/ 5008 12))
data (.getChannelData fitted 0)]
(is (= 5008 (.-length fitted)))
(is (= 0.5 (aget data 91)))
(is (= 0 (aget data 92)))
(is (= 0 (aget data 5007)))
(is (identical? short (mix/fit-buffer short (/ 92 12))))
(is (= 20 (.-length (mix/fit-buffer short (/ 20 12))))))
(finally (set! (.-OfflineAudioContext js/globalThis) original)))))