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)))
(.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!
"Promise of `{:buffer :seconds}` — the audio the transport runs its clock on
while symbol `sid` is open, and how long that clock is.
@ -216,7 +233,9 @@
(and fallback-url (= sid (clip/opens-on document)))
(-> (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
{:buffer nil
@ -236,12 +255,8 @@
symbol played against the whole take's soundtrack would run ten frames and then
keep the clock going for minutes."
[document sid fallback-url store]
(-> (buffer! document sid store)
(.then (fn [buffer]
(cond
buffer (wav-url buffer)
(and fallback-url (= sid (clip/opens-on document))) fallback-url
: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))))))))
(-> (clock-source! document sid fallback-url store)
(.then (fn [{:keys [buffer seconds]}]
(wav-url (or buffer
(.createBuffer (js/OfflineAudioContext. 1 1 44100)
1 (max 1 (js/Math.ceil (* 44100 seconds))) 44100)))))))

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