151 lines
6.6 KiB
Common Lisp
151 lines
6.6 KiB
Common Lisp
;; Quick Test - Fully Explicit Streaming Version
|
|
;;
|
|
;; The interpreter is completely generic - knows nothing about video/audio.
|
|
;; All domain logic is explicit via primitives.
|
|
;;
|
|
;; Run with built-in sources/audio:
|
|
;; python3 -m streaming.stream_sexp_generic effects/quick_test_explicit.sexp --fps 30
|
|
;;
|
|
;; Run with external config files:
|
|
;; python3 -m streaming.stream_sexp_generic effects/quick_test_explicit.sexp \
|
|
;; --sources configs/sources-default.sexp \
|
|
;; --audio configs/audio-dizzy.sexp \
|
|
;; --fps 30
|
|
|
|
(stream "quick_test_explicit"
|
|
:fps 30
|
|
:width 1920
|
|
:height 1080
|
|
:seed 42
|
|
|
|
;; Load standard primitives and effects
|
|
(include :path "../templates/standard-primitives.sexp")
|
|
(include :path "../templates/standard-effects.sexp")
|
|
|
|
;; Load reusable templates
|
|
(include :path "../templates/stream-process-pair.sexp")
|
|
(include :path "../templates/crossfade-zoom.sexp")
|
|
|
|
;; === SOURCES AS ARRAY ===
|
|
(def sources [
|
|
(streaming:make-video-source "monday.webm" 30)
|
|
(streaming:make-video-source "escher.webm" 30)
|
|
(streaming:make-video-source "2.webm" 30)
|
|
(streaming:make-video-source "disruptors.webm" 30)
|
|
(streaming:make-video-source "4.mp4" 30)
|
|
(streaming:make-video-source "ecstacy.mp4" 30)
|
|
(streaming:make-video-source "dopple.webm" 30)
|
|
(streaming:make-video-source "5.mp4" 30)
|
|
])
|
|
|
|
;; Per-pair config: [rot-dir, rot-a-max, rot-b-max, zoom-a-max, zoom-b-max]
|
|
;; Pairs 3,6: reversed (negative rot-a, positive rot-b, shrink zoom-a, grow zoom-b)
|
|
;; Pair 5: smaller ranges
|
|
(def pair-configs [
|
|
{:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} ;; 0: monday
|
|
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} ;; 1: escher
|
|
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} ;; 2: vid2
|
|
{:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5} ;; 3: disruptors (reversed)
|
|
{:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} ;; 4: vid4
|
|
{:dir 1 :rot-a 30 :rot-b -30 :zoom-a 1.3 :zoom-b 0.7} ;; 5: ecstacy (smaller)
|
|
{:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5} ;; 6: dopple (reversed)
|
|
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} ;; 7: vid5
|
|
])
|
|
|
|
;; Audio analyzer
|
|
(def music (streaming:make-audio-analyzer "dizzy.mp3"))
|
|
|
|
;; Audio playback
|
|
(audio-playback "../dizzy.mp3")
|
|
|
|
;; === GLOBAL SCANS ===
|
|
|
|
;; Cycle state: which source is active (recipe-specific)
|
|
;; clen = beats per source (8-24 beats = ~4-12 seconds)
|
|
(scan cycle (streaming:audio-beat music t)
|
|
:init {:active 0 :beat 0 :clen 16}
|
|
:step (if (< (+ beat 1) clen)
|
|
(dict :active active :beat (+ beat 1) :clen clen)
|
|
(dict :active (mod (+ active 1) (len sources)) :beat 0
|
|
:clen (+ 8 (mod (* (streaming:audio-beat-count music t) 7) 17)))))
|
|
|
|
;; Reusable scans from templates (require 'music' to be defined)
|
|
(include :path "../templates/scan-oscillating-spin.sexp")
|
|
(include :path "../templates/scan-ripple-drops.sexp")
|
|
|
|
;; === PER-PAIR STATE (dynamically sized based on sources) ===
|
|
;; Each pair has: inv-a, inv-b, hue-a, hue-b, mix, rot-angle
|
|
(scan pairs (streaming:audio-beat music t)
|
|
:init {:states (map (core:range (len sources)) (lambda (_)
|
|
{:inv-a 0 :inv-b 0 :hue-a 0 :hue-b 0 :hue-a-val 0 :hue-b-val 0 :mix 0.5 :mix-rem 5 :angle 0 :rot-beat 0 :rot-clen 25}))}
|
|
:step (dict :states (map states (lambda (p)
|
|
(let [;; Invert toggles (10% chance, lasts 1-4 beats)
|
|
new-inv-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-a) 1)))
|
|
new-inv-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-b) 1)))
|
|
;; Hue shifts (10% chance, lasts 1-4 beats) - use countdown like invert
|
|
old-hue-a (get p :hue-a)
|
|
old-hue-b (get p :hue-b)
|
|
new-hue-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-a 1)))
|
|
new-hue-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-b 1)))
|
|
;; Pick random hue value when triggering (stored separately)
|
|
new-hue-a-val (if (> new-hue-a old-hue-a) (+ 30 (* (core:rand) 300)) (get p :hue-a-val))
|
|
new-hue-b-val (if (> new-hue-b old-hue-b) (+ 30 (* (core:rand) 300)) (get p :hue-b-val))
|
|
;; Mix (holds for 1-10 beats, then picks 0, 0.5, or 1)
|
|
mix-rem (get p :mix-rem)
|
|
old-mix (get p :mix)
|
|
new-mix-rem (if (> mix-rem 0) (- mix-rem 1) (+ 1 (core:rand-int 1 10)))
|
|
new-mix (if (> mix-rem 0) old-mix (* (core:rand-int 0 2) 0.5))
|
|
;; Rotation (accumulates, reverses direction when cycle completes)
|
|
rot-beat (get p :rot-beat)
|
|
rot-clen (get p :rot-clen)
|
|
old-angle (get p :angle)
|
|
;; Note: dir comes from pair-configs, but we store rotation state here
|
|
new-rot-beat (if (< (+ rot-beat 1) rot-clen) (+ rot-beat 1) 0)
|
|
new-rot-clen (if (< (+ rot-beat 1) rot-clen) rot-clen (+ 20 (core:rand-int 0 10)))
|
|
new-angle (+ old-angle (/ 360 rot-clen))]
|
|
(dict :inv-a new-inv-a :inv-b new-inv-b
|
|
:hue-a new-hue-a :hue-b new-hue-b
|
|
:hue-a-val new-hue-a-val :hue-b-val new-hue-b-val
|
|
:mix new-mix :mix-rem new-mix-rem
|
|
:angle new-angle :rot-beat new-rot-beat :rot-clen new-rot-clen))))))
|
|
|
|
;; === FRAME PIPELINE ===
|
|
(frame
|
|
(let [now t
|
|
e (streaming:audio-energy music now)
|
|
|
|
;; Get cycle state
|
|
active (bind cycle :active)
|
|
beat-pos (bind cycle :beat)
|
|
clen (bind cycle :clen)
|
|
|
|
;; Transition logic: last third of cycle crossfades to next
|
|
phase3 (* beat-pos 3)
|
|
fading (and (>= phase3 (* clen 2)) (< phase3 (* clen 3)))
|
|
fade-amt (if fading (/ (- phase3 (* clen 2)) clen) 0)
|
|
next-idx (mod (+ active 1) (len sources))
|
|
|
|
;; Get pair states array (required by process-pair macro)
|
|
pair-states (bind pairs :states)
|
|
|
|
;; Process active pair using macro from template
|
|
active-frame (process-pair active)
|
|
|
|
;; Crossfade with zoom during transition (using macro)
|
|
result (if fading
|
|
(crossfade-zoom active-frame (process-pair next-idx) fade-amt)
|
|
active-frame)
|
|
|
|
;; Final: global spin + ripple
|
|
spun (rotate result :angle (bind spin :angle))
|
|
rip-gate (bind ripple-state :gate)
|
|
rip-amp (* rip-gate (core:map-range e 0 1 5 50))]
|
|
|
|
(ripple spun
|
|
:amplitude rip-amp
|
|
:center_x (bind ripple-state :cx)
|
|
:center_y (bind ripple-state :cy)
|
|
:frequency 8
|
|
:decay 2
|
|
:speed 5))))
|