Compare commits
6 Commits
3c44bd5f37
...
5163a577b3
| Author | SHA1 | Date |
|---|---|---|
|
|
5163a577b3 | |
|
|
d771bb41f4 | |
|
|
931a9a90d4 | |
|
|
4ec90c0f27 | |
|
|
7c7b2c921e | |
|
|
6e8260172f |
|
|
@ -558,11 +558,9 @@
|
||||||
(cond
|
(cond
|
||||||
;; Serve ParenScript-compiled auth-ui.js
|
;; Serve ParenScript-compiled auth-ui.js
|
||||||
((string= path "js/auth-ui.js")
|
((string= path "js/auth-ui.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT auth-ui.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-auth-ui-js)))
|
(let ((js (generate-auth-ui-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating auth-ui.js: ~a~%" e)
|
(format t "ERROR generating auth-ui.js: ~a~%" e)
|
||||||
|
|
@ -570,11 +568,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled front-page.js
|
;; Serve ParenScript-compiled front-page.js
|
||||||
((string= path "js/front-page.js")
|
((string= path "js/front-page.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT front-page.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-front-page-js)))
|
(let ((js (generate-front-page-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating front-page.js: ~a~%" e)
|
(format t "ERROR generating front-page.js: ~a~%" e)
|
||||||
|
|
@ -582,11 +578,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled profile.js
|
;; Serve ParenScript-compiled profile.js
|
||||||
((string= path "js/profile.js")
|
((string= path "js/profile.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT profile.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-profile-js)))
|
(let ((js (generate-profile-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating profile.js: ~a~%" e)
|
(format t "ERROR generating profile.js: ~a~%" e)
|
||||||
|
|
@ -594,11 +588,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled users.js
|
;; Serve ParenScript-compiled users.js
|
||||||
((string= path "js/users.js")
|
((string= path "js/users.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT users.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-users-js)))
|
(let ((js (generate-users-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating users.js: ~a~%" e)
|
(format t "ERROR generating users.js: ~a~%" e)
|
||||||
|
|
@ -606,11 +598,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled admin.js
|
;; Serve ParenScript-compiled admin.js
|
||||||
((string= path "js/admin.js")
|
((string= path "js/admin.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT admin.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-admin-js)))
|
(let ((js (generate-admin-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating admin.js: ~a~%" e)
|
(format t "ERROR generating admin.js: ~a~%" e)
|
||||||
|
|
@ -618,11 +608,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled player.js
|
;; Serve ParenScript-compiled player.js
|
||||||
((string= path "js/player.js")
|
((string= path "js/player.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT player.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-player-js)))
|
(let ((js (generate-player-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating player.js: ~a~%" e)
|
(format t "ERROR generating player.js: ~a~%" e)
|
||||||
|
|
@ -630,11 +618,9 @@
|
||||||
|
|
||||||
;; Serve ParenScript-compiled recently-played.js
|
;; Serve ParenScript-compiled recently-played.js
|
||||||
((string= path "js/recently-played.js")
|
((string= path "js/recently-played.js")
|
||||||
(format t "~%=== SERVING PARENSCRIPT recently-played.js ===~%")
|
|
||||||
(setf (content-type *response*) "application/javascript")
|
(setf (content-type *response*) "application/javascript")
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((js (generate-recently-played-js)))
|
(let ((js (generate-recently-played-js)))
|
||||||
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
|
||||||
(if js js "// Error: No JavaScript generated"))
|
(if js js "// Error: No JavaScript generated"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "ERROR generating recently-played.js: ~a~%" e)
|
(format t "ERROR generating recently-played.js: ~a~%" e)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
(response (drakma:http-request icecast-url
|
(response (drakma:http-request icecast-url
|
||||||
:want-stream nil
|
:want-stream nil
|
||||||
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
||||||
(format t "DEBUG: Fetching Icecast stats from ~a~%" icecast-url)
|
|
||||||
(when response
|
(when response
|
||||||
(let ((xml-string (if (stringp response)
|
(let ((xml-string (if (stringp response)
|
||||||
response
|
response
|
||||||
|
|
@ -34,7 +33,6 @@
|
||||||
(aref groups 0)
|
(aref groups 0)
|
||||||
"Unknown")))
|
"Unknown")))
|
||||||
"Unknown")))
|
"Unknown")))
|
||||||
(format t "DEBUG: Parsed title=~a, total-listeners=~a~%" title total-listeners)
|
|
||||||
|
|
||||||
;; Track recently played if title changed
|
;; Track recently played if title changed
|
||||||
(when (and title
|
(when (and title
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,11 @@
|
||||||
|
|
||||||
;; Live stream info update
|
;; Live stream info update
|
||||||
(defun update-live-stream-info ()
|
(defun update-live-stream-info ()
|
||||||
|
;; Don't update if stream is paused
|
||||||
|
(let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio"))))
|
||||||
|
(when (and live-audio (ps:@ live-audio paused))
|
||||||
|
(return)))
|
||||||
|
|
||||||
(ps:chain
|
(ps:chain
|
||||||
(fetch "/api/asteroid/partial/now-playing-inline")
|
(fetch "/api/asteroid/partial/now-playing-inline")
|
||||||
(then (lambda (response)
|
(then (lambda (response)
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,56 @@
|
||||||
(ps:chain audio-element (load))
|
(ps:chain audio-element (load))
|
||||||
(ps:chain (ps:chain audio-element (play))
|
(ps:chain (ps:chain audio-element (play))
|
||||||
(catch (lambda (err)
|
(catch (lambda (err)
|
||||||
(ps:chain console (log "Reload failed:" err))))))))))
|
(ps:chain console (log "Reload failed:" err))))))))
|
||||||
|
|
||||||
|
(let ((pause-timestamp nil)
|
||||||
|
(is-reconnecting false)
|
||||||
|
(needs-reconnect false)
|
||||||
|
(pause-reconnect-threshold 10000))
|
||||||
|
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "pause"
|
||||||
|
(lambda ()
|
||||||
|
(setf pause-timestamp (ps:chain |Date| (now)))
|
||||||
|
(ps:chain console (log "Stream paused at:" pause-timestamp)))))
|
||||||
|
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "play"
|
||||||
|
(lambda ()
|
||||||
|
(when (and (not is-reconnecting)
|
||||||
|
pause-timestamp
|
||||||
|
(> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold))
|
||||||
|
(setf needs-reconnect true)
|
||||||
|
(ps:chain console (log "Long pause detected, will reconnect when playing starts...")))
|
||||||
|
(setf pause-timestamp nil))))
|
||||||
|
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "playing"
|
||||||
|
(lambda ()
|
||||||
|
(when (and needs-reconnect (not is-reconnecting))
|
||||||
|
(setf is-reconnecting true)
|
||||||
|
(setf needs-reconnect false)
|
||||||
|
(ps:chain console (log "Reconnecting stream after long pause to clear stale buffers..."))
|
||||||
|
|
||||||
|
(ps:chain audio-element (pause))
|
||||||
|
|
||||||
|
(when (ps:@ window |resetSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (reset-spectrum-analyzer)))
|
||||||
|
|
||||||
|
(ps:chain audio-element (load))
|
||||||
|
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain audio-element (play)
|
||||||
|
(catch (lambda (err)
|
||||||
|
(ps:chain console (log "Reconnect play failed:" err)))))
|
||||||
|
|
||||||
|
(when (ps:@ window |initSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (init-spectrum-analyzer))
|
||||||
|
(ps:chain console (log "Spectrum analyzer reinitialized after reconnect")))
|
||||||
|
|
||||||
|
(setf is-reconnecting false))
|
||||||
|
200))))))))
|
||||||
|
|
||||||
;; Check frameset preference
|
;; Check frameset preference
|
||||||
(let ((path (ps:@ window location pathname))
|
(let ((path (ps:@ window location pathname))
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,62 @@
|
||||||
(update-player-display)
|
(update-player-display)
|
||||||
(update-volume)
|
(update-volume)
|
||||||
|
|
||||||
;; Setup live stream with reduced buffering
|
;; Setup live stream with reduced buffering and reconnect logic
|
||||||
(let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio"))))
|
(let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio"))))
|
||||||
(when live-audio
|
(when live-audio
|
||||||
;; Reduce buffer to minimize delay
|
;; Reduce buffer to minimize delay
|
||||||
(setf (ps:@ live-audio preload) "none")))
|
(setf (ps:@ live-audio preload) "none")
|
||||||
|
|
||||||
|
;; Add reconnect logic for long pauses
|
||||||
|
(let ((pause-timestamp nil)
|
||||||
|
(is-reconnecting false)
|
||||||
|
(needs-reconnect false)
|
||||||
|
(pause-reconnect-threshold 10000))
|
||||||
|
|
||||||
|
(ps:chain live-audio
|
||||||
|
(add-event-listener "pause"
|
||||||
|
(lambda ()
|
||||||
|
(setf pause-timestamp (ps:chain |Date| (now)))
|
||||||
|
(ps:chain console (log "Live stream paused at:" pause-timestamp)))))
|
||||||
|
|
||||||
|
(ps:chain live-audio
|
||||||
|
(add-event-listener "play"
|
||||||
|
(lambda ()
|
||||||
|
(when (and (not is-reconnecting)
|
||||||
|
pause-timestamp
|
||||||
|
(> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold))
|
||||||
|
(setf needs-reconnect true)
|
||||||
|
(ps:chain console (log "Long pause detected, will reconnect when playing starts...")))
|
||||||
|
(setf pause-timestamp nil))))
|
||||||
|
|
||||||
|
(ps:chain live-audio
|
||||||
|
(add-event-listener "playing"
|
||||||
|
(lambda ()
|
||||||
|
(when (and needs-reconnect (not is-reconnecting))
|
||||||
|
(setf is-reconnecting true)
|
||||||
|
(setf needs-reconnect false)
|
||||||
|
(ps:chain console (log "Reconnecting live stream after long pause to clear stale buffers..."))
|
||||||
|
|
||||||
|
(ps:chain live-audio (pause))
|
||||||
|
|
||||||
|
(when (ps:@ window |resetSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (reset-spectrum-analyzer)))
|
||||||
|
|
||||||
|
(ps:chain live-audio (load))
|
||||||
|
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain live-audio (play)
|
||||||
|
(catch (lambda (err)
|
||||||
|
(ps:chain console (log "Reconnect play failed:" err)))))
|
||||||
|
|
||||||
|
(when (ps:@ window |initSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (init-spectrum-analyzer))
|
||||||
|
(ps:chain console (log "Spectrum analyzer reinitialized after reconnect")))
|
||||||
|
|
||||||
|
(setf is-reconnecting false))
|
||||||
|
200)))))
|
||||||
|
)))
|
||||||
|
|
||||||
;; Restore user quality preference
|
;; Restore user quality preference
|
||||||
(let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))
|
(let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,30 @@
|
||||||
(defvar *canvas* nil)
|
(defvar *canvas* nil)
|
||||||
(defvar *canvas-ctx* nil)
|
(defvar *canvas-ctx* nil)
|
||||||
(defvar *animation-id* nil)
|
(defvar *animation-id* nil)
|
||||||
|
(defvar *media-source* nil)
|
||||||
|
(defvar *current-audio-element* nil)
|
||||||
|
(defvar *current-theme* "green")
|
||||||
|
(defvar *current-style* "bars")
|
||||||
|
|
||||||
|
;; Color themes for spectrum analyzer
|
||||||
|
(defvar *themes*
|
||||||
|
(ps:create
|
||||||
|
"green" (ps:create "top" "#00ff00" "mid" "#00aa00" "bottom" "#005500")
|
||||||
|
"blue" (ps:create "top" "#00ffff" "mid" "#0088ff" "bottom" "#0044aa")
|
||||||
|
"purple" (ps:create "top" "#ff00ff" "mid" "#aa00aa" "bottom" "#550055")
|
||||||
|
"red" (ps:create "top" "#ff0000" "mid" "#aa0000" "bottom" "#550000")
|
||||||
|
"amber" (ps:create "top" "#ffaa00" "mid" "#ff6600" "bottom" "#aa3300")
|
||||||
|
"rainbow" (ps:create "top" "#ff00ff" "mid" "#00ffff" "bottom" "#00ff00")))
|
||||||
|
|
||||||
|
(defun reset-spectrum-analyzer ()
|
||||||
|
"Reset the spectrum analyzer to allow reconnection after audio element reload"
|
||||||
|
(when *animation-id*
|
||||||
|
(cancel-animation-frame *animation-id*)
|
||||||
|
(setf *animation-id* nil))
|
||||||
|
(setf *audio-context* nil)
|
||||||
|
(setf *analyser* nil)
|
||||||
|
(setf *media-source* nil)
|
||||||
|
(ps:chain console (log "Spectrum analyzer reset for reconnection")))
|
||||||
|
|
||||||
(defun init-spectrum-analyzer ()
|
(defun init-spectrum-analyzer ()
|
||||||
"Initialize the spectrum analyzer"
|
"Initialize the spectrum analyzer"
|
||||||
|
|
@ -37,27 +61,35 @@
|
||||||
(:catch (e)
|
(:catch (e)
|
||||||
(ps:chain console (log "Cross-frame access error:" e)))))
|
(ps:chain console (log "Cross-frame access error:" e)))))
|
||||||
|
|
||||||
(when (and audio-element canvas-element (not *audio-context*))
|
(when (and audio-element canvas-element)
|
||||||
;; Create Audio Context
|
;; Store current audio element
|
||||||
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
(setf *current-audio-element* audio-element)
|
||||||
(ps:@ window |webkitAudioContext|))))
|
|
||||||
|
|
||||||
;; Create Analyser Node
|
;; Only create audio context and media source once
|
||||||
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
(when (not *audio-context*)
|
||||||
(setf (ps:@ *analyser* |fftSize|) 256)
|
;; Create Audio Context
|
||||||
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
||||||
|
(ps:@ window |webkitAudioContext|))))
|
||||||
;; Connect audio source to analyser
|
|
||||||
(let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
|
;; Create Analyser Node
|
||||||
(ps:chain source (connect *analyser*))
|
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
||||||
(ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
|
(setf (ps:@ *analyser* |fftSize|) 256)
|
||||||
|
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
||||||
|
|
||||||
|
;; Connect audio source to analyser (can only be done once per element)
|
||||||
|
(setf *media-source* (ps:chain *audio-context* (create-media-element-source audio-element)))
|
||||||
|
(ps:chain *media-source* (connect *analyser*))
|
||||||
|
(ps:chain *analyser* (connect (ps:@ *audio-context* destination)))
|
||||||
|
|
||||||
|
(ps:chain console (log "Spectrum analyzer audio context created")))
|
||||||
|
|
||||||
;; Setup canvas
|
;; Setup canvas
|
||||||
(setf *canvas* canvas-element)
|
(setf *canvas* canvas-element)
|
||||||
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
|
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
|
||||||
|
|
||||||
;; Start visualization
|
;; Start visualization if not already running
|
||||||
(draw-spectrum))))
|
(when (not *animation-id*)
|
||||||
|
(draw-spectrum)))))
|
||||||
|
|
||||||
(defun draw-spectrum ()
|
(defun draw-spectrum ()
|
||||||
"Draw the spectrum analyzer visualization"
|
"Draw the spectrum analyzer visualization"
|
||||||
|
|
@ -69,7 +101,8 @@
|
||||||
(height (ps:@ *canvas* height))
|
(height (ps:@ *canvas* height))
|
||||||
(bar-width (/ width buffer-length))
|
(bar-width (/ width buffer-length))
|
||||||
(bar-height 0)
|
(bar-height 0)
|
||||||
(x 0))
|
(x 0)
|
||||||
|
(is-muted (and *current-audio-element* (ps:@ *current-audio-element* muted))))
|
||||||
|
|
||||||
(ps:chain *analyser* (get-byte-frequency-data data-array))
|
(ps:chain *analyser* (get-byte-frequency-data data-array))
|
||||||
|
|
||||||
|
|
@ -77,21 +110,68 @@
|
||||||
(setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(0, 0, 0, 0.2)")
|
(setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(0, 0, 0, 0.2)")
|
||||||
(ps:chain *canvas-ctx* (fill-rect 0 0 width height))
|
(ps:chain *canvas-ctx* (fill-rect 0 0 width height))
|
||||||
|
|
||||||
;; Draw bars
|
;; Get current theme colors
|
||||||
(dotimes (i buffer-length)
|
(let ((theme (ps:getprop *themes* *current-theme*)))
|
||||||
(setf bar-height (/ (* (aref data-array i) height) 256))
|
(cond
|
||||||
|
;; Bar graph style
|
||||||
;; Create gradient for each bar
|
((= *current-style* "bars")
|
||||||
(let ((gradient (ps:chain *canvas-ctx*
|
(setf x 0)
|
||||||
(create-linear-gradient 0 (- height bar-height) 0 height))))
|
(dotimes (i buffer-length)
|
||||||
(ps:chain gradient (add-color-stop 0 "#00ff00"))
|
(setf bar-height (/ (* (aref data-array i) height) 256))
|
||||||
(ps:chain gradient (add-color-stop 0.5 "#00aa00"))
|
|
||||||
(ps:chain gradient (add-color-stop 1 "#005500"))
|
;; Create gradient for each bar using theme colors
|
||||||
|
(let ((gradient (ps:chain *canvas-ctx*
|
||||||
|
(create-linear-gradient 0 (- height bar-height) 0 height))))
|
||||||
|
(ps:chain gradient (add-color-stop 0 (ps:@ theme top)))
|
||||||
|
(ps:chain gradient (add-color-stop 0.5 (ps:@ theme mid)))
|
||||||
|
(ps:chain gradient (add-color-stop 1 (ps:@ theme bottom)))
|
||||||
|
|
||||||
|
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
|
||||||
|
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
|
||||||
|
|
||||||
|
(incf x bar-width))))
|
||||||
|
|
||||||
|
;; Wave/line style
|
||||||
|
((= *current-style* "wave")
|
||||||
|
(setf x 0)
|
||||||
|
(ps:chain *canvas-ctx* (begin-path))
|
||||||
|
(setf (ps:@ *canvas-ctx* |lineWidth|) 2)
|
||||||
|
(setf (ps:@ *canvas-ctx* |strokeStyle|) (ps:@ theme top))
|
||||||
|
|
||||||
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
|
(dotimes (i buffer-length)
|
||||||
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
|
(setf bar-height (/ (* (aref data-array i) height) 256))
|
||||||
|
(let ((y (- height bar-height)))
|
||||||
|
(if (= i 0)
|
||||||
|
(ps:chain *canvas-ctx* (move-to x y))
|
||||||
|
(ps:chain *canvas-ctx* (line-to x y)))
|
||||||
|
(incf x bar-width)))
|
||||||
|
|
||||||
(incf x bar-width)))))
|
(ps:chain *canvas-ctx* (stroke)))
|
||||||
|
|
||||||
|
;; Dots/particles style
|
||||||
|
((= *current-style* "dots")
|
||||||
|
(setf x 0)
|
||||||
|
(setf (ps:@ *canvas-ctx* |fillStyle|) (ps:@ theme top))
|
||||||
|
(dotimes (i buffer-length)
|
||||||
|
(let* ((value (aref data-array i))
|
||||||
|
(normalized-height (/ (* value height) 256))
|
||||||
|
(y (- height normalized-height))
|
||||||
|
(dot-radius (ps:max 2 (/ normalized-height 20))))
|
||||||
|
|
||||||
|
(when (> value 0)
|
||||||
|
(ps:chain *canvas-ctx* (begin-path))
|
||||||
|
(ps:chain *canvas-ctx* (arc x y dot-radius 0 6.283185307179586))
|
||||||
|
(ps:chain *canvas-ctx* (fill)))
|
||||||
|
|
||||||
|
(incf x bar-width))))))
|
||||||
|
|
||||||
|
;; Draw MUTED indicator if audio is muted
|
||||||
|
(when is-muted
|
||||||
|
(setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(255, 0, 0, 0.8)")
|
||||||
|
(setf (ps:@ *canvas-ctx* font) "bold 20px monospace")
|
||||||
|
(setf (ps:@ *canvas-ctx* |textAlign|) "right")
|
||||||
|
(setf (ps:@ *canvas-ctx* |textBaseline|) "top")
|
||||||
|
(ps:chain *canvas-ctx* (fill-text "MUTED" (- width 10) 10)))))
|
||||||
|
|
||||||
(defun stop-spectrum-analyzer ()
|
(defun stop-spectrum-analyzer ()
|
||||||
"Stop the spectrum analyzer"
|
"Stop the spectrum analyzer"
|
||||||
|
|
@ -99,9 +179,47 @@
|
||||||
(cancel-animation-frame *animation-id*)
|
(cancel-animation-frame *animation-id*)
|
||||||
(setf *animation-id* nil)))
|
(setf *animation-id* nil)))
|
||||||
|
|
||||||
|
(defun set-spectrum-theme (theme-name)
|
||||||
|
"Change the spectrum analyzer color theme"
|
||||||
|
(when (ps:getprop *themes* theme-name)
|
||||||
|
(setf *current-theme* theme-name)
|
||||||
|
(ps:chain local-storage (set-item "spectrum-theme" theme-name))
|
||||||
|
(ps:chain console (log (+ "Spectrum theme changed to: " theme-name)))))
|
||||||
|
|
||||||
|
(defun get-available-themes ()
|
||||||
|
"Return array of available theme names"
|
||||||
|
(ps:chain |Object| (keys *themes*)))
|
||||||
|
|
||||||
|
(defun set-spectrum-style (style-name)
|
||||||
|
"Change the spectrum analyzer visualization style"
|
||||||
|
(when (or (= style-name "bars") (= style-name "wave") (= style-name "dots"))
|
||||||
|
(setf *current-style* style-name)
|
||||||
|
(ps:chain local-storage (set-item "spectrum-style" style-name))
|
||||||
|
(ps:chain console (log (+ "Spectrum style changed to: " style-name)))))
|
||||||
|
|
||||||
|
(defun get-available-styles ()
|
||||||
|
"Return array of available visualization styles"
|
||||||
|
(array "bars" "wave" "dots"))
|
||||||
|
|
||||||
;; Initialize when audio starts playing
|
;; Initialize when audio starts playing
|
||||||
(ps:chain document (add-event-listener "DOMContentLoaded"
|
(ps:chain document (add-event-listener "DOMContentLoaded"
|
||||||
(lambda ()
|
(lambda ()
|
||||||
|
;; Load saved theme and style preferences
|
||||||
|
(let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme")))
|
||||||
|
(saved-style (ps:chain local-storage (get-item "spectrum-style"))))
|
||||||
|
(when (and saved-theme (ps:getprop *themes* saved-theme))
|
||||||
|
(setf *current-theme* saved-theme))
|
||||||
|
(when (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots")))
|
||||||
|
(setf *current-style* saved-style))
|
||||||
|
|
||||||
|
;; Update UI selectors if they exist
|
||||||
|
(let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector")))
|
||||||
|
(style-selector (ps:chain document (get-element-by-id "spectrum-style-selector"))))
|
||||||
|
(when theme-selector
|
||||||
|
(setf (ps:@ theme-selector value) *current-theme*))
|
||||||
|
(when style-selector
|
||||||
|
(setf (ps:@ style-selector value) *current-style*))))
|
||||||
|
|
||||||
(let ((audio-element (or (ps:chain document (get-element-by-id "live-audio"))
|
(let ((audio-element (or (ps:chain document (get-element-by-id "live-audio"))
|
||||||
(ps:chain document (get-element-by-id "persistent-audio")))))
|
(ps:chain document (get-element-by-id "persistent-audio")))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1161,10 +1161,12 @@
|
||||||
:opacity 1
|
:opacity 1
|
||||||
:background-color #(color-accented-black)))
|
:background-color #(color-accented-black)))
|
||||||
|
|
||||||
;; Live stream blink animation
|
;; Live stream pulse animation (like old MacBook sleep indicator)
|
||||||
(.live-stream-indicator
|
(.live-stream-indicator
|
||||||
:animation "asteroid-blink 1s steps(5, start) infinite")
|
:animation "asteroid-pulse 2s ease-in-out infinite")
|
||||||
|
|
||||||
(:keyframes asteroid-blink
|
(:keyframes asteroid-pulse
|
||||||
(to :visibility "hidden"))
|
(0% :opacity 1)
|
||||||
|
(50% :opacity 0.3)
|
||||||
|
(100% :opacity 1))
|
||||||
) ;; End of let block
|
) ;; End of let block
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,27 @@
|
||||||
<!-- Spectrum Analyzer Canvas -->
|
<!-- Spectrum Analyzer Canvas -->
|
||||||
<div style="text-align: center; margin: 15px 0;">
|
<div style="text-align: center; margin: 15px 0;">
|
||||||
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
||||||
|
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||||
|
<label style="margin-right: 10px;">
|
||||||
|
Style:
|
||||||
|
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="bars">Bars</option>
|
||||||
|
<option value="wave">Wave</option>
|
||||||
|
<option value="dots">Dots</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Theme:
|
||||||
|
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="green">Green</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
<option value="purple">Purple</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="amber">Amber</option>
|
||||||
|
<option value="rainbow">Rainbow</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,27 @@
|
||||||
<!-- Spectrum Analyzer Canvas -->
|
<!-- Spectrum Analyzer Canvas -->
|
||||||
<div style="text-align: center; margin: 15px 0;">
|
<div style="text-align: center; margin: 15px 0;">
|
||||||
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
||||||
|
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||||
|
<label style="margin-right: 10px;">
|
||||||
|
Style:
|
||||||
|
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="bars">Bars</option>
|
||||||
|
<option value="wave">Wave</option>
|
||||||
|
<option value="dots">Dots</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Theme:
|
||||||
|
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="green">Green</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
<option value="purple">Purple</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="amber">Amber</option>
|
||||||
|
<option value="rainbow">Rainbow</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,27 @@
|
||||||
<!-- Spectrum Analyzer Canvas -->
|
<!-- Spectrum Analyzer Canvas -->
|
||||||
<div style="text-align: center; margin: 15px 0;">
|
<div style="text-align: center; margin: 15px 0;">
|
||||||
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
||||||
|
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||||
|
<label style="margin-right: 10px;">
|
||||||
|
Style:
|
||||||
|
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="bars">Bars</option>
|
||||||
|
<option value="wave">Wave</option>
|
||||||
|
<option value="dots">Dots</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Theme:
|
||||||
|
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
|
||||||
|
<option value="green">Green</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
<option value="purple">Purple</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="amber">Amber</option>
|
||||||
|
<option value="rainbow">Rainbow</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue