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