/ parenscript / spectrum-analyzer.lisp
spectrum-analyzer.lisp
1 (in-package #:asteroid) 2 3 ;;; Spectrum Analyzer - Parenscript Implementation 4 ;;; Generates JavaScript for real-time audio visualization 5 6 (define-api asteroid/spectrum-analyzer.js () () 7 "Serve the spectrum analyzer JavaScript generated from Parenscript" 8 (setf (content-type *response*) "application/javascript") 9 (ps:ps 10 (defvar *audio-context* nil) 11 (defvar *analyser* nil) 12 (defvar *canvas* nil) 13 (defvar *canvas-ctx* nil) 14 (defvar *animation-id* nil) 15 (defvar *media-source* nil) 16 (defvar *current-audio-element* nil) 17 (defvar *current-theme* "green") 18 (defvar *current-style* "bars") 19 20 ;; Color themes for spectrum analyzer 21 (defvar *themes* 22 (ps:create 23 "monotone" (ps:create "top" "#0047ab" "mid" "#002966" "bottom" "#000d1a") 24 "green" (ps:create "top" "#00ff00" "mid" "#00aa00" "bottom" "#005500") 25 "blue" (ps:create "top" "#00ffff" "mid" "#0088ff" "bottom" "#0044aa") 26 "purple" (ps:create "top" "#ff00ff" "mid" "#aa00aa" "bottom" "#550055") 27 "red" (ps:create "top" "#ff0000" "mid" "#aa0000" "bottom" "#550000") 28 "amber" (ps:create "top" "#ffaa00" "mid" "#ff6600" "bottom" "#aa3300") 29 "rainbow" (ps:create "top" "#ff00ff" "mid" "#00ffff" "bottom" "#00ff00"))) 30 31 (defun reset-spectrum-analyzer () 32 "Reset the spectrum analyzer to allow reconnection after audio element reload" 33 (when *animation-id* 34 (cancel-animation-frame *animation-id*) 35 (setf *animation-id* nil)) 36 ;; Close the old AudioContext if it exists 37 (when *audio-context* 38 (ps:try 39 (ps:chain *audio-context* (close)) 40 (:catch (e) 41 (ps:chain console (log "Error closing AudioContext:" e))))) 42 (setf *audio-context* nil) 43 (setf *analyser* nil) 44 (setf *media-source* nil) 45 (setf *current-audio-element* nil) 46 (ps:chain console (log "Spectrum analyzer reset for reconnection"))) 47 48 (defun init-spectrum-analyzer () 49 "Initialize the spectrum analyzer" 50 (let ((audio-element nil) 51 (canvas-element (ps:chain document (get-element-by-id "spectrum-canvas")))) 52 53 ;; Try to find audio element in current frame first 54 (setf audio-element (or (ps:chain document (get-element-by-id "live-audio")) 55 (ps:chain document (get-element-by-id "persistent-audio")))) 56 57 ;; If not found and we're in a frame, try to access from parent frameset 58 (when (and (not audio-element) 59 (ps:@ window parent) 60 (not (eq (ps:@ window parent) window))) 61 (ps:chain console (log "Trying to access audio from parent frame...")) 62 (ps:try 63 (progn 64 ;; Try accessing via parent.frames 65 (let ((player-frame (ps:getprop (ps:@ window parent) "player-frame"))) 66 (when player-frame 67 (setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio"))) 68 (ps:chain console (log "Found audio in player-frame:" audio-element))))) 69 (:catch (e) 70 (ps:chain console (log "Cross-frame access error:" e))))) 71 72 (when (and audio-element canvas-element) 73 ;; Store current audio element 74 (setf *current-audio-element* audio-element) 75 76 ;; Only create audio context and media source once 77 (when (not *audio-context*) 78 ;; Create Audio Context 79 (setf *audio-context* (ps:new (or (ps:@ window |AudioContext|) 80 (ps:@ window |webkitAudioContext|)))) 81 82 ;; Create Analyser Node 83 (setf *analyser* (ps:chain *audio-context* (create-analyser))) 84 (setf (ps:@ *analyser* |fftSize|) 256) 85 (setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8) 86 87 ;; Connect audio source to analyser (can only be done once per element) 88 (setf *media-source* (ps:chain *audio-context* (create-media-element-source audio-element))) 89 (ps:chain *media-source* (connect *analyser*)) 90 (ps:chain *analyser* (connect (ps:@ *audio-context* destination))) 91 92 (ps:chain console (log "Spectrum analyzer audio context created"))) 93 94 ;; Setup canvas 95 (setf *canvas* canvas-element) 96 (setf *canvas-ctx* (ps:chain *canvas* (get-context "2d"))) 97 98 ;; Start visualization if not already running 99 (when (not *animation-id*) 100 (draw-spectrum))))) 101 102 (defun draw-spectrum () 103 "Draw the spectrum analyzer visualization" 104 (setf *animation-id* (request-animation-frame draw-spectrum)) 105 106 (let* ((buffer-length (ps:@ *analyser* |frequencyBinCount|)) 107 (data-array (ps:new (|Uint8Array| buffer-length))) 108 (width (ps:@ *canvas* width)) 109 (height (ps:@ *canvas* height)) 110 (bar-width (/ width buffer-length)) 111 (bar-height 0) 112 (x 0) 113 (is-muted (and *current-audio-element* (ps:@ *current-audio-element* muted)))) 114 115 (ps:chain *analyser* (get-byte-frequency-data data-array)) 116 117 ;; Clear canvas with fade effect 118 (setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(0, 0, 0, 0.2)") 119 (ps:chain *canvas-ctx* (fill-rect 0 0 width height)) 120 121 ;; Get current theme colors 122 (let ((theme (ps:getprop *themes* *current-theme*))) 123 (cond 124 ;; Bar graph style 125 ((= *current-style* "bars") 126 (setf x 0) 127 (dotimes (i buffer-length) 128 (setf bar-height (/ (* (aref data-array i) height) 256)) 129 130 ;; Create gradient for each bar using theme colors 131 (let ((gradient (ps:chain *canvas-ctx* 132 (create-linear-gradient 0 (- height bar-height) 0 height)))) 133 (ps:chain gradient (add-color-stop 0 (ps:@ theme top))) 134 (ps:chain gradient (add-color-stop 0.5 (ps:@ theme mid))) 135 (ps:chain gradient (add-color-stop 1 (ps:@ theme bottom))) 136 137 (setf (ps:@ *canvas-ctx* |fillStyle|) gradient) 138 (ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height)) 139 140 (incf x bar-width)))) 141 142 ;; Wave/line style 143 ((= *current-style* "wave") 144 (setf x 0) 145 (ps:chain *canvas-ctx* (begin-path)) 146 (setf (ps:@ *canvas-ctx* |lineWidth|) 2) 147 (setf (ps:@ *canvas-ctx* |strokeStyle|) (ps:@ theme top)) 148 149 (dotimes (i buffer-length) 150 (setf bar-height (/ (* (aref data-array i) height) 256)) 151 (let ((y (- height bar-height))) 152 (if (= i 0) 153 (ps:chain *canvas-ctx* (move-to x y)) 154 (ps:chain *canvas-ctx* (line-to x y))) 155 (incf x bar-width))) 156 157 (ps:chain *canvas-ctx* (stroke))) 158 159 ;; Dots/particles style 160 ((= *current-style* "dots") 161 (setf x 0) 162 (setf (ps:@ *canvas-ctx* |fillStyle|) (ps:@ theme top)) 163 (dotimes (i buffer-length) 164 (let* ((value (aref data-array i)) 165 (normalized-height (/ (* value height) 256)) 166 (y (- height normalized-height)) 167 (dot-radius (ps:max 2 (/ normalized-height 20)))) 168 169 (when (> value 0) 170 (ps:chain *canvas-ctx* (begin-path)) 171 (ps:chain *canvas-ctx* (arc x y dot-radius 0 6.283185307179586)) 172 (ps:chain *canvas-ctx* (fill))) 173 174 (incf x bar-width)))))) 175 176 ;; Draw MUTED indicator if audio is muted 177 (when is-muted 178 (setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(255, 0, 0, 0.8)") 179 (setf (ps:@ *canvas-ctx* font) "bold 20px monospace") 180 (setf (ps:@ *canvas-ctx* |textAlign|) "right") 181 (setf (ps:@ *canvas-ctx* |textBaseline|) "top") 182 (ps:chain *canvas-ctx* (fill-text "MUTED" (- width 10) 10))))) 183 184 (defun stop-spectrum-analyzer () 185 "Stop the spectrum analyzer" 186 (when *animation-id* 187 (cancel-animation-frame *animation-id*) 188 (setf *animation-id* nil))) 189 190 (defun set-spectrum-theme (theme-name) 191 "Change the spectrum analyzer color theme and update dropdown colors" 192 (when (ps:getprop *themes* theme-name) 193 (setf *current-theme* theme-name) 194 (ps:chain local-storage (set-item "spectrum-theme" theme-name)) 195 196 (let ((theme (ps:getprop *themes* theme-name))) 197 ;; Update canvas border color to match theme 198 (when *canvas* 199 (setf (ps:@ *canvas* style border-color) (ps:@ theme top))) 200 201 ;; Update dropdown box colors 202 (let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector"))) 203 (style-selector (ps:chain document (get-element-by-id "spectrum-style-selector")))) 204 (when theme-selector 205 (setf (ps:@ theme-selector style color) (ps:@ theme top)) 206 (setf (ps:@ theme-selector style border-color) (ps:@ theme top))) 207 (when style-selector 208 (setf (ps:@ style-selector style color) (ps:@ theme top)) 209 (setf (ps:@ style-selector style border-color) (ps:@ theme top))))) 210 211 (ps:chain console (log (+ "Spectrum theme changed to: " theme-name))))) 212 213 (defun get-available-themes () 214 "Return array of available theme names" 215 (ps:chain |Object| (keys *themes*))) 216 217 (defun set-spectrum-style (style-name) 218 "Change the spectrum analyzer visualization style" 219 (when (or (= style-name "bars") (= style-name "wave") (= style-name "dots")) 220 (setf *current-style* style-name) 221 (ps:chain local-storage (set-item "spectrum-style" style-name)) 222 (ps:chain console (log (+ "Spectrum style changed to: " style-name))))) 223 224 (defun get-available-styles () 225 "Return array of available visualization styles" 226 (array "bars" "wave" "dots")) 227 228 ;; Initialize when audio starts playing 229 (ps:chain document (add-event-listener "DOMContentLoaded" 230 (lambda () 231 ;; Load saved theme and style preferences 232 (let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme"))) 233 (saved-style (ps:chain local-storage (get-item "spectrum-style")))) 234 (when (and saved-theme (ps:getprop *themes* saved-theme)) 235 (setf *current-theme* saved-theme)) 236 (when (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots"))) 237 (setf *current-style* saved-style)) 238 239 ;; Update UI selectors, canvas border, and dropdown colors 240 (let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector"))) 241 (style-selector (ps:chain document (get-element-by-id "spectrum-style-selector"))) 242 (canvas (ps:chain document (get-element-by-id "spectrum-canvas"))) 243 (theme (ps:getprop *themes* *current-theme*))) 244 (when theme-selector 245 (setf (ps:@ theme-selector value) *current-theme*) 246 (setf (ps:@ theme-selector style color) (ps:@ theme top)) 247 (setf (ps:@ theme-selector style border-color) (ps:@ theme top))) 248 (when style-selector 249 (setf (ps:@ style-selector value) *current-style*) 250 (setf (ps:@ style-selector style color) (ps:@ theme top)) 251 (setf (ps:@ style-selector style border-color) (ps:@ theme top))) 252 253 ;; Set initial canvas border color 254 (when canvas 255 (setf (ps:@ canvas style border-color) (ps:@ theme top))))) 256 257 (let ((audio-element (or (ps:chain document (get-element-by-id "live-audio")) 258 (ps:chain document (get-element-by-id "persistent-audio"))))) 259 260 ;; If not found and we're in a frame, try parent 261 (when (and (not audio-element) 262 (ps:@ window parent) 263 (not (eq (ps:@ window parent) window))) 264 (ps:try 265 (let ((player-frame (ps:getprop (ps:@ window parent) "player-frame"))) 266 (when player-frame 267 (setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio"))))) 268 (:catch (e) 269 (ps:chain console (log "Event listener cross-frame error:" e))))) 270 271 (when audio-element 272 (ps:chain audio-element (add-event-listener "play" init-spectrum-analyzer)) 273 (ps:chain audio-element (add-event-listener "pause" stop-spectrum-analyzer)))))))))