Make a symbol from a dropped video

Dropping footage on the stage or the timeline asks which frames and what name:
a dialog plays the video with start and end handles that seek it. Detection
runs on that range only (decoding stops at the end, frames before the start
are skipped) and the range is part of the analysis address, so a partial
analysis is never served as a whole one. The frozen take comes in as one
named symbol, carrying its sound as an audio node, placed where it was
dropped; tracking and tuning come with it when the document has no analysis
of its own.

clip/adopt copies symbols between documents, renaming ids that collide, and
clip/audio-tracks carries sounds out of nested instances so a placed symbol
is heard where it is placed.

Fixes the stage going blank after a conversion: ::store recomputed only when
the clip id changed, so blocks merged into the loaded entry were invisible to
the resolver. And the paint loop now schedules its next frame before
painting and reports a frame it cannot draw instead of stopping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-29 13:21:08 -04:00
parent 270c5a4369
commit 41b4bdf110
18 changed files with 562 additions and 120 deletions

View file

@ -270,3 +270,34 @@
(is (= [:outer 12] (clip/frame-inside c :outer [] 12)))
(is (= [:inner 7] (clip/frame-inside c :outer [id] 12))
"the instance starts at 5, so frame 12 outside is frame 7 inside")))
(deftest adopting-symbols-renames-what-collides
(let [here (nested)
there (-> (clip/blank)
(assoc-in [:symbols :inner] {:id :inner :frames 4 :nodes {}})
(clip/place-symbol :main :inner 0 #uuid "00000000-0000-4000-8000-0000000000aa" [0 0]))
{:keys [clip ids]} (clip/adopt here there [:main] {:main :take})]
(is (= {:main :take :inner :inner-2} ids)
"the root gets the name asked for; a taken id gets the next free one")
(is (= 10 (clip/frames clip :inner)) "what was already here is untouched")
(is (= :inner-2 (:of (first (vals (get-in clip [:symbols :take :nodes])))))
"and the copy's instance follows its renamed symbol")
(is (empty? (clip/problems clip)))))
(deftest a-placed-symbols-sound-is-heard-where-it-is-placed
(let [voice {:id :v :kind :audio :source {:footage "f"} :z "a1"
:span [10 40] :time {:mode :map :at 0 :rate 1}
:channels {[:audio :gain] (ch/keyed {0 0.0 5 1.0})}}
c (-> (clip/blank)
(assoc-in [:symbols :talk] {:id :talk :frames 30 :nodes {:v voice}})
(clip/place-symbol :main :talk 50 #uuid "00000000-0000-4000-8000-0000000000bb" [0 0]))
[t] (clip/audio-tracks c :main)]
(is (= [10 40] (:span t)) "the same frames of the source")
(is (= [50 80] (node/placed-span t)) "starting where the instance starts")
(is (= #{50 55} (set (keys (get-in t [:channels [:audio :gain] :keys]))))
"with its automation moved along")
(testing "and cut off where the instance's own span ends"
(let [c (assoc-in c [:symbols :main :nodes #uuid "00000000-0000-4000-8000-0000000000bb" :span] [0 12])
[t] (clip/audio-tracks c :main)]
(is (= [10 22] (:span t)))
(is (= [50 62] (node/placed-span t)))))))