Add generic streaming interpreter with configurable sources/audio
- Add stream_sexp_generic.py: fully generic sexp interpreter - Add streaming primitives for video sources and audio analysis - Add config system for external sources and audio files - Add templates for reusable scans and macros - Fix video/audio stream mapping in file output - Add dynamic source cycling based on sources array length - Remove old Python effect files (migrated to sexp) - Update sexp effects to use namespaced primitives Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +1,31 @@
|
||||
;; Blend effect - combines two video streams
|
||||
;; Multi-input effect: uses frame-a and frame-b
|
||||
;; Blend effect - combines two video frames
|
||||
;; Streaming-compatible: frame is background, overlay is second frame
|
||||
;; Usage: (blend background overlay :opacity 0.5 :mode "alpha")
|
||||
;;
|
||||
;; Params:
|
||||
;; mode - blend mode (add, multiply, screen, overlay, difference, lighten, darken, alpha)
|
||||
;; opacity - blend amount (0-1)
|
||||
;; resize_mode - how to resize frame-b to match frame-a (fit, crop, stretch)
|
||||
;; priority - which dimension takes priority (width, height)
|
||||
;; pad_color - color for padding in fit mode [r g b]
|
||||
|
||||
(require-primitives "image" "blending")
|
||||
(require-primitives "image" "blending" "core")
|
||||
|
||||
(define-effect blend
|
||||
:params (
|
||||
(mode :type string :default "overlay")
|
||||
(overlay :type frame :default nil)
|
||||
(mode :type string :default "alpha")
|
||||
(opacity :type float :default 0.5)
|
||||
(resize_mode :type string :default "fit")
|
||||
(priority :type string :default "width")
|
||||
(pad_color :type list :default (quote [0 0 0]))
|
||||
)
|
||||
(let [a frame-a
|
||||
a-w (width a)
|
||||
a-h (height a)
|
||||
b-raw frame-b
|
||||
b-w (width b-raw)
|
||||
b-h (height b-raw)
|
||||
;; Calculate scale based on resize mode and priority
|
||||
scale-w (/ a-w b-w)
|
||||
scale-h (/ a-h b-h)
|
||||
scale (if (= resize_mode "stretch")
|
||||
1 ;; Will use explicit dimensions
|
||||
(if (= resize_mode "crop")
|
||||
(max scale-w scale-h) ;; Scale to cover, then crop
|
||||
(if (= priority "width")
|
||||
scale-w
|
||||
scale-h)))
|
||||
;; For stretch, use target dimensions directly
|
||||
new-w (if (= resize_mode "stretch") a-w (round (* b-w scale)))
|
||||
new-h (if (= resize_mode "stretch") a-h (round (* b-h scale)))
|
||||
;; Resize b
|
||||
b-resized (resize b-raw new-w new-h "linear")
|
||||
;; Handle fit (pad) or crop to exact size
|
||||
b (if (= resize_mode "crop")
|
||||
;; Crop to center
|
||||
(let [cx (/ (- new-w a-w) 2)
|
||||
cy (/ (- new-h a-h) 2)]
|
||||
(crop b-resized cx cy a-w a-h))
|
||||
(if (and (= resize_mode "fit") (or (!= new-w a-w) (!= new-h a-h)))
|
||||
;; Pad to center
|
||||
(let [pad-x (/ (- a-w new-w) 2)
|
||||
pad-y (/ (- a-h new-h) 2)
|
||||
canvas (make-image a-w a-h pad_color)]
|
||||
(paste canvas b-resized pad-x pad-y))
|
||||
b-resized))]
|
||||
(if (= mode "alpha")
|
||||
(blend-images a b opacity)
|
||||
(blend-images a (blend-mode a b mode) opacity))))
|
||||
(if (core:is-nil overlay)
|
||||
frame
|
||||
(let [a frame
|
||||
b overlay
|
||||
a-h (image:height a)
|
||||
a-w (image:width a)
|
||||
b-h (image:height b)
|
||||
b-w (image:width b)
|
||||
;; Resize b to match a if needed
|
||||
b-sized (if (and (= a-w b-w) (= a-h b-h))
|
||||
b
|
||||
(image:resize b a-w a-h "linear"))]
|
||||
(if (= mode "alpha")
|
||||
(blending:blend-images a b-sized opacity)
|
||||
(blending:blend-images a (blending:blend-mode a b-sized mode) opacity)))))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
;; N-way weighted blend effect
|
||||
;;
|
||||
;; Takes N input frames via `inputs` and N per-frame weights.
|
||||
;; Produces a single frame: the normalised weighted composite.
|
||||
;; Streaming-compatible: pass inputs as a list of frames
|
||||
;; Usage: (blend_multi :inputs [(read a) (read b) (read c)] :weights [0.3 0.4 0.3])
|
||||
;;
|
||||
;; Parameters:
|
||||
;; inputs - list of N frames to blend
|
||||
;; weights - list of N floats, one per input (resolved per-frame)
|
||||
;; mode - blend mode applied when folding each frame in:
|
||||
;; "alpha" — pure weighted average (default)
|
||||
@@ -30,14 +30,15 @@
|
||||
|
||||
(define-effect blend_multi
|
||||
:params (
|
||||
(weights :type list :default (quote []))
|
||||
(inputs :type list :default [])
|
||||
(weights :type list :default [])
|
||||
(mode :type string :default "alpha")
|
||||
(resize_mode :type string :default "fit")
|
||||
)
|
||||
(let [n (len inputs)
|
||||
;; Target dimensions from first frame
|
||||
target-w (width (nth inputs 0))
|
||||
target-h (height (nth inputs 0))
|
||||
target-w (image:width (nth inputs 0))
|
||||
target-h (image:height (nth inputs 0))
|
||||
;; Fold over indices 1..n-1
|
||||
;; Accumulator is (list blended-frame running-weight-sum)
|
||||
seed (list (nth inputs 0) (nth weights 0))
|
||||
@@ -48,10 +49,10 @@
|
||||
w (nth weights i)
|
||||
new-running (+ running w)
|
||||
opacity (/ w (max new-running 0.001))
|
||||
f (resize (nth inputs i) target-w target-h "linear")
|
||||
f (image:resize (nth inputs i) target-w target-h "linear")
|
||||
;; Apply blend mode then mix with opacity
|
||||
blended (if (= mode "alpha")
|
||||
(blend-images acc f opacity)
|
||||
(blend-images acc (blend-mode acc f mode) opacity))]
|
||||
(blending:blend-images acc f opacity)
|
||||
(blending:blend-images acc (blending:blend-mode acc f mode) opacity))]
|
||||
(list blended new-running))))]
|
||||
(nth result 0)))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Bloom effect - glow on bright areas
|
||||
(require-primitives "image" "blending")
|
||||
|
||||
(define-effect bloom
|
||||
:params (
|
||||
@@ -11,5 +12,5 @@
|
||||
(if (> (luminance c) threshold)
|
||||
c
|
||||
(rgb 0 0 0)))))
|
||||
(blurred (blur bright radius)))
|
||||
(blend-mode frame blurred "add")))
|
||||
(blurred (image:blur bright radius)))
|
||||
(blending:blend-mode frame blurred "add")))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
;; Blur effect - gaussian blur
|
||||
|
||||
(require-primitives "filters" "math")
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect blur
|
||||
:params (
|
||||
(radius :type int :default 5 :range [1 50])
|
||||
)
|
||||
(blur frame (max 1 radius)))
|
||||
(image:blur frame (max 1 radius)))
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
;; Brightness effect - adjusts overall brightness
|
||||
;; Uses vectorized adjust primitive for fast processing
|
||||
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect brightness
|
||||
:params (
|
||||
(amount :type int :default 0 :range [-255 255])
|
||||
)
|
||||
(adjust frame amount 1))
|
||||
(color_ops:adjust-brightness frame amount))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Color adjustment effect - replaces TRANSFORM node
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect color-adjust
|
||||
:params (
|
||||
@@ -7,5 +8,6 @@
|
||||
(saturation :type float :default 1 :range [0 2] :desc "Saturation multiplier")
|
||||
)
|
||||
(-> frame
|
||||
(adjust :brightness brightness :contrast contrast)
|
||||
(shift-hsv :s saturation)))
|
||||
(color_ops:adjust-brightness brightness)
|
||||
(color_ops:adjust-contrast contrast)
|
||||
(color_ops:adjust-saturation saturation)))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Color Cycle effect - animated hue rotation
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect color_cycle
|
||||
:params (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
;; Contrast effect - adjusts image contrast
|
||||
;; Uses vectorized adjust primitive for fast processing
|
||||
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect contrast
|
||||
:params (
|
||||
(amount :type int :default 1 :range [0.5 3])
|
||||
)
|
||||
(adjust frame 0 amount))
|
||||
(color_ops:adjust-contrast frame amount))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; CRT effect - old monitor simulation
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect crt
|
||||
:params (
|
||||
@@ -6,8 +7,8 @@
|
||||
(line_opacity :type float :default 0.3 :range [0 1])
|
||||
(vignette_amount :type float :default 0.2)
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (/ w 2))
|
||||
(cy (/ h 2))
|
||||
(max-dist (sqrt (+ (* cx cx) (* cy cy)))))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Echo effect - motion trails using frame buffer
|
||||
(require-primitives "blending")
|
||||
|
||||
(define-effect echo
|
||||
:params (
|
||||
@@ -15,4 +16,4 @@
|
||||
(let ((result (copy frame)))
|
||||
;; Simple blend of first two frames for now
|
||||
;; Full version would fold over all frames
|
||||
(blend-images frame (nth new-buffer 1) (* decay 0.5)))))))
|
||||
(blending:blend-images frame (nth new-buffer 1) (* decay 0.5)))))))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
;; Edge detection effect - highlights edges
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect edge_detect
|
||||
:params (
|
||||
(low :type int :default 50 :range [10 100])
|
||||
(high :type int :default 150 :range [50 300])
|
||||
)
|
||||
(edges frame low high))
|
||||
(image:edge-detect frame low high))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Emboss effect - creates raised/3D appearance
|
||||
(require-primitives "blending")
|
||||
|
||||
(define-effect emboss
|
||||
:params (
|
||||
@@ -9,4 +10,4 @@
|
||||
(list (- strength) 1 strength)
|
||||
(list 0 strength strength)))
|
||||
(embossed (convolve frame kernel)))
|
||||
(blend-images embossed frame blend)))
|
||||
(blending:blend-images embossed frame blend)))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Film Grain effect - adds film grain texture
|
||||
(require-primitives "core")
|
||||
|
||||
(define-effect film_grain
|
||||
:params (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Fisheye effect - barrel/pincushion lens distortion
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect fisheye
|
||||
:params (
|
||||
@@ -7,9 +8,9 @@
|
||||
(center_y :type float :default 0.5 :range [0 1])
|
||||
(zoom_correct :type bool :default true)
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (* w center_x))
|
||||
(cy (* h center_y))
|
||||
(coords (fisheye-displace w h strength cx cy zoom_correct)))
|
||||
(remap frame (coords-x coords) (coords-y coords))))
|
||||
(coords (geometry:fisheye-coords w h strength cx cy zoom_correct)))
|
||||
(geometry:remap frame (geometry:coords-x coords) (geometry:coords-y coords))))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Flip effect - flips image horizontally or vertically
|
||||
(require-primitives "geometry")
|
||||
|
||||
(define-effect flip
|
||||
:params (
|
||||
@@ -7,9 +8,9 @@
|
||||
)
|
||||
(let ((result frame))
|
||||
(if horizontal
|
||||
(set! result (flip-h result))
|
||||
(set! result (geometry:flip-img result "horizontal"))
|
||||
nil)
|
||||
(if vertical
|
||||
(set! result (flip-v result))
|
||||
(set! result (geometry:flip-img result "vertical"))
|
||||
nil)
|
||||
result))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
;; Grayscale effect - converts to grayscale
|
||||
;; Uses vectorized mix-gray primitive for fast processing
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect grayscale
|
||||
:params ()
|
||||
(mix-gray frame 1))
|
||||
(image:grayscale frame))
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
(speed :type int :default 0 :desc "rotation per second")
|
||||
)
|
||||
(let ((shift (+ degrees (* speed t))))
|
||||
(shift-hsv frame shift 1 1)))
|
||||
(color_ops:shift-hsv frame shift 1 1)))
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(define-effect invert
|
||||
:params ((amount :type float :default 1 :range [0 1]))
|
||||
(if (> amount 0.5) (invert-img frame) frame))
|
||||
(if (> amount 0.5) (color_ops:invert-img frame) frame))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Kaleidoscope effect - mandala-like symmetry patterns
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect kaleidoscope
|
||||
:params (
|
||||
@@ -9,11 +10,11 @@
|
||||
(center_y :type float :default 0.5 :range [0 1])
|
||||
(zoom :type int :default 1 :range [0.5 3])
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (* w center_x))
|
||||
(cy (* h center_y))
|
||||
;; Total rotation including time-based animation
|
||||
(total_rot (+ rotation (* rotation_speed (or _time 0))))
|
||||
(coords (kaleidoscope-displace w h segments total_rot cx cy zoom)))
|
||||
(remap frame (coords-x coords) (coords-y coords))))
|
||||
(coords (geometry:kaleidoscope-coords w h segments total_rot cx cy zoom)))
|
||||
(geometry:remap frame (geometry:coords-x coords) (geometry:coords-y coords))))
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
;; Layer effect - composite frame-b over frame-a at position
|
||||
;; Multi-input effect: uses frame-a (background) and frame-b (overlay)
|
||||
;; Params: x, y (position), opacity (0-1), mode (blend mode)
|
||||
;; Layer effect - composite overlay over background at position
|
||||
;; Streaming-compatible: frame is background, overlay is foreground
|
||||
;; Usage: (layer background overlay :x 10 :y 20 :opacity 0.8)
|
||||
;;
|
||||
;; Params:
|
||||
;; overlay - frame to composite on top
|
||||
;; x, y - position to place overlay
|
||||
;; opacity - blend amount (0-1)
|
||||
;; mode - blend mode (alpha, multiply, screen, etc.)
|
||||
|
||||
(require-primitives "image" "blending")
|
||||
(require-primitives "image" "blending" "core")
|
||||
|
||||
(define-effect layer
|
||||
:params (
|
||||
(overlay :type frame :default nil)
|
||||
(x :type int :default 0)
|
||||
(y :type int :default 0)
|
||||
(opacity :type float :default 1.0)
|
||||
(mode :type string :default "alpha")
|
||||
)
|
||||
(let [bg (copy frame-a)
|
||||
fg frame-b
|
||||
;; Resize fg if needed to fit
|
||||
fg-w (width fg)
|
||||
fg-h (height fg)]
|
||||
(if (= opacity 1.0)
|
||||
;; Simple paste
|
||||
(paste bg fg x y)
|
||||
;; Blend with opacity
|
||||
(let [blended (if (= mode "alpha")
|
||||
(blend-images (crop bg x y fg-w fg-h) fg opacity)
|
||||
(blend-images (crop bg x y fg-w fg-h)
|
||||
(blend-mode (crop bg x y fg-w fg-h) fg mode)
|
||||
opacity))]
|
||||
(paste bg blended x y)))))
|
||||
(if (core:is-nil overlay)
|
||||
frame
|
||||
(let [bg (copy frame)
|
||||
fg overlay
|
||||
fg-w (image:width fg)
|
||||
fg-h (image:height fg)]
|
||||
(if (= opacity 1.0)
|
||||
;; Simple paste
|
||||
(paste bg fg x y)
|
||||
;; Blend with opacity
|
||||
(let [blended (if (= mode "alpha")
|
||||
(blending:blend-images (image:crop bg x y fg-w fg-h) fg opacity)
|
||||
(blending:blend-images (image:crop bg x y fg-w fg-h)
|
||||
(blending:blend-mode (image:crop bg x y fg-w fg-h) fg mode)
|
||||
opacity))]
|
||||
(paste bg blended x y))))))
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
;; Mirror effect - mirrors half of image
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect mirror
|
||||
:params (
|
||||
(mode :type string :default "left_right")
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(hw (floor (/ w 2)))
|
||||
(hh (floor (/ h 2))))
|
||||
(cond
|
||||
((= mode "left_right")
|
||||
(let ((left (crop frame 0 0 hw h))
|
||||
(let ((left (image:crop frame 0 0 hw h))
|
||||
(result (copy frame)))
|
||||
(paste result (flip-h left) hw 0)))
|
||||
(paste result (geometry:flip-img left "horizontal") hw 0)))
|
||||
|
||||
((= mode "right_left")
|
||||
(let ((right (crop frame hw 0 hw h))
|
||||
(let ((right (image:crop frame hw 0 hw h))
|
||||
(result (copy frame)))
|
||||
(paste result (flip-h right) 0 0)))
|
||||
(paste result (geometry:flip-img right "horizontal") 0 0)))
|
||||
|
||||
((= mode "top_bottom")
|
||||
(let ((top (crop frame 0 0 w hh))
|
||||
(let ((top (image:crop frame 0 0 w hh))
|
||||
(result (copy frame)))
|
||||
(paste result (flip-v top) 0 hh)))
|
||||
(paste result (geometry:flip-img top "vertical") 0 hh)))
|
||||
|
||||
((= mode "bottom_top")
|
||||
(let ((bottom (crop frame 0 hh w hh))
|
||||
(let ((bottom (image:crop frame 0 hh w hh))
|
||||
(result (copy frame)))
|
||||
(paste result (flip-v bottom) 0 0)))
|
||||
(paste result (geometry:flip-img bottom "vertical") 0 0)))
|
||||
|
||||
(else frame))))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Neon Glow effect - glowing edge effect
|
||||
(require-primitives "image" "blending")
|
||||
|
||||
(define-effect neon_glow
|
||||
:params (
|
||||
@@ -8,15 +9,15 @@
|
||||
(glow_intensity :type int :default 2 :range [0.5 5])
|
||||
(background :type float :default 0.3 :range [0 1])
|
||||
)
|
||||
(let* ((edge-img (edges frame edge_low edge_high))
|
||||
(glow (blur edge-img glow_radius))
|
||||
(let* ((edge-img (image:edge-detect frame edge_low edge_high))
|
||||
(glow (image:blur edge-img glow_radius))
|
||||
;; Intensify the glow
|
||||
(bright-glow (map-pixels glow
|
||||
(lambda (x y c)
|
||||
(rgb (clamp (* (red c) glow_intensity) 0 255)
|
||||
(clamp (* (green c) glow_intensity) 0 255)
|
||||
(clamp (* (blue c) glow_intensity) 0 255))))))
|
||||
(blend-mode (blend-images frame (make-image (width frame) (height frame) (list 0 0 0))
|
||||
(blending:blend-mode (blending:blend-images frame (make-image (image:width frame) (image:height frame) (list 0 0 0))
|
||||
(- 1 background))
|
||||
bright-glow
|
||||
"screen")))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Outline effect - shows only edges
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect outline
|
||||
:params (
|
||||
@@ -7,14 +8,14 @@
|
||||
(color :type list :default (list 0 0 0)
|
||||
)
|
||||
(fill_mode "original"))
|
||||
(let* ((edge-img (edges frame (/ threshold 2) threshold))
|
||||
(let* ((edge-img (image:edge-detect frame (/ threshold 2) threshold))
|
||||
(dilated (if (> thickness 1)
|
||||
(dilate edge-img thickness)
|
||||
edge-img))
|
||||
(base (cond
|
||||
((= fill_mode "original") (copy frame))
|
||||
((= fill_mode "white") (make-image (width frame) (height frame) (list 255 255 255)))
|
||||
(else (make-image (width frame) (height frame) (list 0 0 0))))))
|
||||
((= fill_mode "white") (make-image (image:width frame) (image:height frame) (list 255 255 255)))
|
||||
(else (make-image (image:width frame) (image:height frame) (list 0 0 0))))))
|
||||
(map-pixels base
|
||||
(lambda (x y c)
|
||||
(let ((edge-val (luminance (pixel dilated x y))))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
;; Pixelate effect - creates blocky pixels
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect pixelate
|
||||
:params (
|
||||
(block_size :type int :default 8 :range [2 64])
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(small-w (max 1 (floor (/ w block_size))))
|
||||
(small-h (max 1 (floor (/ h block_size))))
|
||||
(small (resize frame small-w small-h "area")))
|
||||
(resize small w h "nearest")))
|
||||
(small (image:resize frame small-w small-h "area")))
|
||||
(image:resize small w h "nearest")))
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
;; Posterize effect - reduces color levels
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect posterize
|
||||
:params (
|
||||
(levels :type int :default 8 :range [2 32])
|
||||
)
|
||||
(let ((step (floor (/ 256 levels))))
|
||||
(map-pixels frame
|
||||
(lambda (x y c)
|
||||
(rgb (* (floor (/ (red c) step)) step)
|
||||
(* (floor (/ (green c) step)) step)
|
||||
(* (floor (/ (blue c) step)) step))))))
|
||||
(color_ops:posterize frame levels))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
;; Resize effect - replaces RESIZE node
|
||||
;; Note: uses target-w/target-h to avoid conflict with width/height primitives
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect resize-frame
|
||||
:params (
|
||||
@@ -7,4 +8,4 @@
|
||||
(target-h :type int :default 480 :desc "Target height in pixels")
|
||||
(mode :type string :default "linear" :choices [linear nearest area] :desc "Interpolation mode")
|
||||
)
|
||||
(resize frame target-w target-h mode))
|
||||
(image:resize frame target-w target-h mode))
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
(decay :type int :default 1 :range [0 5])
|
||||
(speed :type int :default 1 :range [0 10])
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (* w center_x))
|
||||
(cy (* h center_y))
|
||||
(phase (* (or _time 0) speed 2 pi))
|
||||
(coords (ripple-displace w h frequency amplitude cx cy decay phase)))
|
||||
(remap frame (coords-x coords) (coords-y coords))))
|
||||
(phase (* (or t 0) speed 2 pi))
|
||||
(coords (geometry:ripple-displace w h frequency amplitude cx cy decay phase)))
|
||||
(geometry:remap frame (geometry:coords-x coords) (geometry:coords-y coords))))
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
(speed :type int :default 0 :desc "rotation per second")
|
||||
)
|
||||
(let ((total-angle (+ angle (* speed t))))
|
||||
(rotate-img frame total-angle)))
|
||||
(geometry:rotate-img frame total-angle)))
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
;; Saturation effect - adjusts color saturation
|
||||
;; Uses vectorized shift-hsv primitive for fast processing
|
||||
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect saturation
|
||||
:params (
|
||||
(amount :type int :default 1 :range [0 3])
|
||||
)
|
||||
(shift-hsv frame 0 amount 1))
|
||||
(color_ops:adjust-saturation frame amount))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Scanlines effect - VHS-style horizontal line shifting
|
||||
(require-primitives "core")
|
||||
|
||||
(define-effect scanlines
|
||||
:params (
|
||||
@@ -9,6 +10,6 @@
|
||||
(map-rows frame
|
||||
(lambda (y row)
|
||||
(let* ((sine-shift (* amplitude (sin (/ (* y 6.28) (max 1 frequency)))))
|
||||
(rand-shift (random (- amplitude) amplitude))
|
||||
(rand-shift (core:rand-range (- amplitude) amplitude))
|
||||
(shift (floor (lerp sine-shift rand-shift randomness))))
|
||||
(roll row shift 0)))))
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
;; Sepia effect - applies sepia tone
|
||||
;; Classic warm vintage look
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect sepia
|
||||
:params ()
|
||||
(color-matrix frame
|
||||
(list (list 0.393 0.769 0.189)
|
||||
(list 0.349 0.686 0.168)
|
||||
(list 0.272 0.534 0.131))))
|
||||
(color_ops:sepia frame))
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
;; Sharpen effect - sharpens edges
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect sharpen
|
||||
:params (
|
||||
(amount :type int :default 1 :range [0 5])
|
||||
)
|
||||
(let ((kernel (list (list 0 (- amount) 0)
|
||||
(list (- amount) (+ 1 (* 4 amount)) (- amount))
|
||||
(list 0 (- amount) 0))))
|
||||
(convolve frame kernel)))
|
||||
(image:sharpen frame amount))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Strobe effect - holds frames for choppy look
|
||||
(require-primitives "core")
|
||||
|
||||
(define-effect strobe
|
||||
:params (
|
||||
@@ -7,7 +8,7 @@
|
||||
(let* ((held (state-get 'held nil))
|
||||
(held-until (state-get 'held-until 0))
|
||||
(frame-duration (/ 1 frame_rate)))
|
||||
(if (or (= held nil) (>= t held-until))
|
||||
(if (or (core:is-nil held) (>= t held-until))
|
||||
(begin
|
||||
(state-set 'held (copy frame))
|
||||
(state-set 'held-until (+ t frame-duration))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Swirl effect - spiral vortex distortion
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect swirl
|
||||
:params (
|
||||
@@ -8,9 +9,9 @@
|
||||
(center_y :type float :default 0.5 :range [0 1])
|
||||
(falloff :type string :default "quadratic")
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (* w center_x))
|
||||
(cy (* h center_y))
|
||||
(coords (swirl-displace w h strength radius cx cy falloff)))
|
||||
(remap frame (coords-x coords) (coords-y coords))))
|
||||
(coords (geometry:swirl-coords w h strength radius cx cy falloff)))
|
||||
(geometry:remap frame (geometry:coords-x coords) (geometry:coords-y coords))))
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
;; Threshold effect - converts to black and white
|
||||
(require-primitives "color_ops")
|
||||
|
||||
(define-effect threshold
|
||||
:params (
|
||||
(level :type int :default 128 :range [0 255])
|
||||
(invert :type bool :default false)
|
||||
)
|
||||
(map-pixels frame
|
||||
(lambda (x y c)
|
||||
(let* ((lum (luminance c))
|
||||
(above (if invert (< lum level) (> lum level))))
|
||||
(if above
|
||||
(rgb 255 255 255)
|
||||
(rgb 0 0 0))))))
|
||||
(color_ops:threshold frame level invert))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Tile Grid effect - tiles image in grid
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect tile_grid
|
||||
:params (
|
||||
@@ -6,11 +7,11 @@
|
||||
(cols :type int :default 2 :range [1 10])
|
||||
(gap :type int :default 0 :range [0 50])
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(tile-w (floor (/ (- w (* gap (- cols 1))) cols)))
|
||||
(tile-h (floor (/ (- h (* gap (- rows 1))) rows)))
|
||||
(tile (resize frame tile-w tile-h "area"))
|
||||
(tile (image:resize frame tile-w tile-h "area"))
|
||||
(result (make-image w h (list 0 0 0))))
|
||||
(begin
|
||||
;; Manually place tiles using nested iteration
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Trails effect - persistent motion trails
|
||||
(require-primitives "image" "blending")
|
||||
|
||||
(define-effect trails
|
||||
:params (
|
||||
@@ -10,10 +11,10 @@
|
||||
(begin
|
||||
(state-set 'buffer (copy frame))
|
||||
frame)
|
||||
(let* ((faded (blend-images buffer
|
||||
(make-image (width frame) (height frame) (list 0 0 0))
|
||||
(let* ((faded (blending:blend-images buffer
|
||||
(make-image (image:width frame) (image:height frame) (list 0 0 0))
|
||||
(- 1 persistence)))
|
||||
(result (blend-mode faded current "lighten")))
|
||||
(result (blending:blend-mode faded current "lighten")))
|
||||
(begin
|
||||
(state-set 'buffer result)
|
||||
result)))))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
;; Vignette effect - darkens corners
|
||||
(require-primitives "image")
|
||||
|
||||
(define-effect vignette
|
||||
:params (
|
||||
(strength :type float :default 0.5 :range [0 1])
|
||||
(radius :type int :default 1 :range [0.5 2])
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
(cx (/ w 2))
|
||||
(cy (/ h 2))
|
||||
(max-dist (* (sqrt (+ (* cx cx) (* cy cy))) radius)))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Wave effect - sine wave displacement distortion
|
||||
(require-primitives "geometry" "image")
|
||||
|
||||
(define-effect wave
|
||||
:params (
|
||||
@@ -7,8 +8,8 @@
|
||||
(speed :type int :default 1 :range [0 10])
|
||||
(direction :type string :default "horizontal")
|
||||
)
|
||||
(let* ((w (width frame))
|
||||
(h (height frame))
|
||||
(let* ((w (image:width frame))
|
||||
(h (image:height frame))
|
||||
;; Use _time for animation phase
|
||||
(phase (* (or _time 0) speed 2 pi))
|
||||
;; Calculate frequency: waves per dimension
|
||||
@@ -17,5 +18,5 @@
|
||||
((= direction "horizontal") "x")
|
||||
((= direction "vertical") "y")
|
||||
(else "both")))
|
||||
(coords (wave-displace w h axis freq amplitude phase)))
|
||||
(remap frame (coords-x coords) (coords-y coords))))
|
||||
(coords (geometry:wave-coords w h axis freq amplitude phase)))
|
||||
(geometry:remap frame (geometry:coords-x coords) (geometry:coords-y coords))))
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
:params (
|
||||
(amount :type int :default 1 :range [0.1 5])
|
||||
)
|
||||
(scale-img frame amount amount))
|
||||
(geometry:scale-img frame amount amount))
|
||||
|
||||
@@ -189,6 +189,30 @@ def prim_range(*args):
|
||||
return []
|
||||
|
||||
|
||||
# Random
|
||||
import random
|
||||
_rng = random.Random()
|
||||
|
||||
def prim_rand():
|
||||
"""Return random float in [0, 1)."""
|
||||
return _rng.random()
|
||||
|
||||
def prim_rand_int(lo, hi):
|
||||
"""Return random integer in [lo, hi]."""
|
||||
return _rng.randint(int(lo), int(hi))
|
||||
|
||||
def prim_rand_range(lo, hi):
|
||||
"""Return random float in [lo, hi)."""
|
||||
return lo + _rng.random() * (hi - lo)
|
||||
|
||||
def prim_map_range(val, from_lo, from_hi, to_lo, to_hi):
|
||||
"""Map value from one range to another."""
|
||||
if from_hi == from_lo:
|
||||
return to_lo
|
||||
t = (val - from_lo) / (from_hi - from_lo)
|
||||
return to_lo + t * (to_hi - to_lo)
|
||||
|
||||
|
||||
# Core primitives dict
|
||||
PRIMITIVES = {
|
||||
# Arithmetic
|
||||
@@ -231,10 +255,17 @@ PRIMITIVES = {
|
||||
'list?': prim_is_list,
|
||||
'dict?': prim_is_dict,
|
||||
'nil?': prim_is_nil,
|
||||
'is-nil': prim_is_nil,
|
||||
|
||||
# Higher-order / iteration
|
||||
'reduce': prim_reduce,
|
||||
'fold': prim_reduce,
|
||||
'map': prim_map,
|
||||
'range': prim_range,
|
||||
|
||||
# Random
|
||||
'rand': prim_rand,
|
||||
'rand-int': prim_rand_int,
|
||||
'rand-range': prim_rand_range,
|
||||
'map-range': prim_map_range,
|
||||
}
|
||||
|
||||
304
sexp_effects/primitive_libs/streaming.py
Normal file
304
sexp_effects/primitive_libs/streaming.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Streaming primitives for video/audio processing.
|
||||
|
||||
These primitives handle video source reading and audio analysis,
|
||||
keeping the interpreter completely generic.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class VideoSource:
|
||||
"""Video source with persistent streaming pipe for fast sequential reads."""
|
||||
|
||||
def __init__(self, path: str, fps: float = 30):
|
||||
self.path = Path(path)
|
||||
self.fps = fps # Output fps for the stream
|
||||
self._frame_size = None
|
||||
self._duration = None
|
||||
self._proc = None # Persistent ffmpeg process
|
||||
self._stream_time = 0.0 # Current position in stream
|
||||
self._frame_time = 1.0 / fps # Time per frame at output fps
|
||||
self._last_read_time = -1
|
||||
self._cached_frame = None
|
||||
|
||||
# Get video info
|
||||
cmd = ["ffprobe", "-v", "quiet", "-print_format", "json",
|
||||
"-show_streams", str(self.path)]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
info = json.loads(result.stdout)
|
||||
|
||||
for stream in info.get("streams", []):
|
||||
if stream.get("codec_type") == "video":
|
||||
self._frame_size = (stream.get("width", 720), stream.get("height", 720))
|
||||
# Try direct duration field first
|
||||
if "duration" in stream:
|
||||
self._duration = float(stream["duration"])
|
||||
# Fall back to tags.DURATION (webm format: "00:01:00.124000000")
|
||||
elif "tags" in stream and "DURATION" in stream["tags"]:
|
||||
dur_str = stream["tags"]["DURATION"]
|
||||
parts = dur_str.split(":")
|
||||
if len(parts) == 3:
|
||||
h, m, s = parts
|
||||
self._duration = int(h) * 3600 + int(m) * 60 + float(s)
|
||||
break
|
||||
|
||||
if not self._frame_size:
|
||||
self._frame_size = (720, 720)
|
||||
|
||||
def _start_stream(self, seek_time: float = 0):
|
||||
"""Start or restart the ffmpeg streaming process."""
|
||||
if self._proc:
|
||||
self._proc.kill()
|
||||
self._proc = None
|
||||
|
||||
w, h = self._frame_size
|
||||
cmd = [
|
||||
"ffmpeg", "-v", "quiet",
|
||||
"-ss", f"{seek_time:.3f}",
|
||||
"-i", str(self.path),
|
||||
"-f", "rawvideo", "-pix_fmt", "rgb24",
|
||||
"-s", f"{w}x{h}",
|
||||
"-r", str(self.fps), # Output at specified fps
|
||||
"-"
|
||||
]
|
||||
self._proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
self._stream_time = seek_time
|
||||
|
||||
def _read_frame_from_stream(self) -> np.ndarray:
|
||||
"""Read one frame from the stream."""
|
||||
w, h = self._frame_size
|
||||
frame_size = w * h * 3
|
||||
|
||||
if not self._proc or self._proc.poll() is not None:
|
||||
return None
|
||||
|
||||
data = self._proc.stdout.read(frame_size)
|
||||
if len(data) < frame_size:
|
||||
return None
|
||||
|
||||
return np.frombuffer(data, dtype=np.uint8).reshape((h, w, 3)).copy()
|
||||
|
||||
def read(self) -> np.ndarray:
|
||||
"""Read frame (uses last cached or t=0)."""
|
||||
if self._cached_frame is not None:
|
||||
return self._cached_frame
|
||||
return self.read_at(0)
|
||||
|
||||
def read_at(self, t: float) -> np.ndarray:
|
||||
"""Read frame at specific time using streaming with smart seeking."""
|
||||
# Cache check - return same frame for same time
|
||||
if t == self._last_read_time and self._cached_frame is not None:
|
||||
return self._cached_frame
|
||||
|
||||
w, h = self._frame_size
|
||||
|
||||
# Loop time if video is shorter
|
||||
seek_time = t
|
||||
if self._duration and self._duration > 0:
|
||||
seek_time = t % self._duration
|
||||
|
||||
# Decide whether to seek or continue streaming
|
||||
# Seek if: no stream, going backwards (more than 1 frame), or jumping more than 2 seconds ahead
|
||||
# Allow small backward tolerance to handle floating point and timing jitter
|
||||
need_seek = (
|
||||
self._proc is None or
|
||||
self._proc.poll() is not None or
|
||||
seek_time < self._stream_time - self._frame_time or # More than 1 frame backward
|
||||
seek_time > self._stream_time + 2.0
|
||||
)
|
||||
|
||||
if need_seek:
|
||||
import sys
|
||||
reason = "no proc" if self._proc is None else "proc dead" if self._proc.poll() is not None else "backward" if seek_time < self._stream_time else "jump"
|
||||
print(f"SEEK {self.path.name}: t={t:.4f} seek={seek_time:.4f} stream={self._stream_time:.4f} ({reason})", file=sys.stderr)
|
||||
self._start_stream(seek_time)
|
||||
|
||||
# Skip frames to reach target time
|
||||
while self._stream_time + self._frame_time <= seek_time:
|
||||
frame = self._read_frame_from_stream()
|
||||
if frame is None:
|
||||
# Stream ended, restart from seek point
|
||||
self._start_stream(seek_time)
|
||||
break
|
||||
self._stream_time += self._frame_time
|
||||
|
||||
# Read the target frame
|
||||
frame = self._read_frame_from_stream()
|
||||
if frame is None:
|
||||
import sys
|
||||
print(f"NULL FRAME {self.path.name}: t={t:.2f} seek={seek_time:.2f}", file=sys.stderr)
|
||||
frame = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
else:
|
||||
self._stream_time += self._frame_time
|
||||
|
||||
self._last_read_time = t
|
||||
self._cached_frame = frame
|
||||
return frame
|
||||
|
||||
def skip(self):
|
||||
"""No-op for seek-based reading."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._frame_size
|
||||
|
||||
def close(self):
|
||||
if self._proc:
|
||||
self._proc.kill()
|
||||
self._proc = None
|
||||
|
||||
|
||||
class AudioAnalyzer:
|
||||
"""Audio analyzer for energy and beat detection."""
|
||||
|
||||
def __init__(self, path: str, sample_rate: int = 22050):
|
||||
self.path = Path(path)
|
||||
self.sample_rate = sample_rate
|
||||
|
||||
# Load audio via ffmpeg
|
||||
cmd = ["ffmpeg", "-v", "quiet", "-i", str(self.path),
|
||||
"-f", "f32le", "-ac", "1", "-ar", str(sample_rate), "-"]
|
||||
result = subprocess.run(cmd, capture_output=True)
|
||||
self._audio = np.frombuffer(result.stdout, dtype=np.float32)
|
||||
|
||||
# Get duration
|
||||
cmd = ["ffprobe", "-v", "quiet", "-print_format", "json",
|
||||
"-show_format", str(self.path)]
|
||||
info = json.loads(subprocess.run(cmd, capture_output=True, text=True).stdout)
|
||||
self.duration = float(info.get("format", {}).get("duration", 60))
|
||||
|
||||
# Beat detection state
|
||||
self._flux_history = []
|
||||
self._last_beat_time = -1
|
||||
self._beat_count = 0
|
||||
self._last_beat_check_time = -1
|
||||
# Cache beat result for current time (so multiple scans see same result)
|
||||
self._beat_cache_time = -1
|
||||
self._beat_cache_result = False
|
||||
|
||||
def get_energy(self, t: float) -> float:
|
||||
"""Get energy level at time t (0-1)."""
|
||||
idx = int(t * self.sample_rate)
|
||||
start = max(0, idx - 512)
|
||||
end = min(len(self._audio), idx + 512)
|
||||
if start >= end:
|
||||
return 0.0
|
||||
return min(1.0, np.sqrt(np.mean(self._audio[start:end] ** 2)) * 3.0)
|
||||
|
||||
def get_beat(self, t: float) -> bool:
|
||||
"""Check if there's a beat at time t."""
|
||||
# Return cached result if same time (multiple scans query same frame)
|
||||
if t == self._beat_cache_time:
|
||||
return self._beat_cache_result
|
||||
|
||||
idx = int(t * self.sample_rate)
|
||||
size = 2048
|
||||
|
||||
start, end = max(0, idx - size//2), min(len(self._audio), idx + size//2)
|
||||
if end - start < size/2:
|
||||
self._beat_cache_time = t
|
||||
self._beat_cache_result = False
|
||||
return False
|
||||
curr = self._audio[start:end]
|
||||
|
||||
pstart, pend = max(0, start - 512), max(0, end - 512)
|
||||
if pend <= pstart:
|
||||
self._beat_cache_time = t
|
||||
self._beat_cache_result = False
|
||||
return False
|
||||
prev = self._audio[pstart:pend]
|
||||
|
||||
curr_spec = np.abs(np.fft.rfft(curr * np.hanning(len(curr))))
|
||||
prev_spec = np.abs(np.fft.rfft(prev * np.hanning(len(prev))))
|
||||
|
||||
n = min(len(curr_spec), len(prev_spec))
|
||||
flux = np.sum(np.maximum(0, curr_spec[:n] - prev_spec[:n])) / (n + 1)
|
||||
|
||||
self._flux_history.append((t, flux))
|
||||
if len(self._flux_history) > 50:
|
||||
self._flux_history = self._flux_history[-50:]
|
||||
|
||||
if len(self._flux_history) < 5:
|
||||
self._beat_cache_time = t
|
||||
self._beat_cache_result = False
|
||||
return False
|
||||
|
||||
recent = [f for _, f in self._flux_history[-20:]]
|
||||
threshold = np.mean(recent) + 1.5 * np.std(recent)
|
||||
|
||||
is_beat = flux > threshold and (t - self._last_beat_time) > 0.1
|
||||
if is_beat:
|
||||
self._last_beat_time = t
|
||||
if t > self._last_beat_check_time:
|
||||
self._beat_count += 1
|
||||
self._last_beat_check_time = t
|
||||
|
||||
# Cache result for this time
|
||||
self._beat_cache_time = t
|
||||
self._beat_cache_result = is_beat
|
||||
return is_beat
|
||||
|
||||
def get_beat_count(self, t: float) -> int:
|
||||
"""Get cumulative beat count up to time t."""
|
||||
# Ensure beat detection has run up to this time
|
||||
self.get_beat(t)
|
||||
return self._beat_count
|
||||
|
||||
|
||||
# === Primitives ===
|
||||
|
||||
def prim_make_video_source(path: str, fps: float = 30):
|
||||
"""Create a video source from a file path."""
|
||||
return VideoSource(path, fps)
|
||||
|
||||
|
||||
def prim_source_read(source: VideoSource, t: float = None):
|
||||
"""Read a frame from a video source."""
|
||||
import sys
|
||||
if t is not None:
|
||||
frame = source.read_at(t)
|
||||
# Debug: show source and time
|
||||
if int(t * 10) % 10 == 0: # Every second
|
||||
print(f"READ {source.path.name}: t={t:.2f} stream={source._stream_time:.2f}", file=sys.stderr)
|
||||
return frame
|
||||
return source.read()
|
||||
|
||||
|
||||
def prim_source_skip(source: VideoSource):
|
||||
"""Skip a frame (keep pipe in sync)."""
|
||||
source.skip()
|
||||
|
||||
|
||||
def prim_source_size(source: VideoSource):
|
||||
"""Get (width, height) of source."""
|
||||
return source.size
|
||||
|
||||
|
||||
def prim_make_audio_analyzer(path: str):
|
||||
"""Create an audio analyzer from a file path."""
|
||||
return AudioAnalyzer(path)
|
||||
|
||||
|
||||
def prim_audio_energy(analyzer: AudioAnalyzer, t: float) -> float:
|
||||
"""Get energy level (0-1) at time t."""
|
||||
return analyzer.get_energy(t)
|
||||
|
||||
|
||||
def prim_audio_beat(analyzer: AudioAnalyzer, t: float) -> bool:
|
||||
"""Check if there's a beat at time t."""
|
||||
return analyzer.get_beat(t)
|
||||
|
||||
|
||||
def prim_audio_beat_count(analyzer: AudioAnalyzer, t: float) -> int:
|
||||
"""Get cumulative beat count up to time t."""
|
||||
return analyzer.get_beat_count(t)
|
||||
|
||||
|
||||
def prim_audio_duration(analyzer: AudioAnalyzer) -> float:
|
||||
"""Get audio duration in seconds."""
|
||||
return analyzer.duration
|
||||
Reference in New Issue
Block a user