Compare commits
38 Commits
5163a577b3
...
3c44bd5f37
| Author | SHA1 | Date |
|---|---|---|
|
|
3c44bd5f37 | |
|
|
53658fb023 | |
|
|
6817e27483 | |
|
|
7493885e4e | |
|
|
4bfc31a3c3 | |
|
|
61570fe2e6 | |
|
|
070e8d8dac | |
|
|
ed13589202 | |
|
|
30ff73a3e2 | |
|
|
f691a1edc8 | |
|
|
27f362f954 | |
|
|
e3e3a144d4 | |
|
|
578306f06f | |
|
|
6bbc3d0b6a | |
|
|
a08e42f752 | |
|
|
24e6859aa0 | |
|
|
d540c87cfc | |
|
|
16d81e8ccc | |
|
|
5f77b4cd4f | |
|
|
d0e40cccad | |
|
|
263dc8a800 | |
|
|
022b1d8b96 | |
|
|
cc79ba7330 | |
|
|
3d7b08119a | |
|
|
c35ae5a1f0 | |
|
|
0d50f01a07 | |
|
|
3c2ddf84c0 | |
|
|
b12e366d2c | |
|
|
3c76436e81 | |
|
|
8b839daf0a | |
|
|
ec00843a90 | |
|
|
aa4ed06d7f | |
|
|
cec3763403 | |
|
|
c198775083 | |
|
|
d187a01641 | |
|
|
f498008d2a | |
|
|
96a3ce2b64 | |
|
|
63d606b39b |
|
|
@ -558,9 +558,11 @@
|
||||||
(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)
|
||||||
|
|
@ -568,9 +570,11 @@
|
||||||
|
|
||||||
;; 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)
|
||||||
|
|
@ -578,9 +582,11 @@
|
||||||
|
|
||||||
;; 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)
|
||||||
|
|
@ -588,9 +594,11 @@
|
||||||
|
|
||||||
;; 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)
|
||||||
|
|
@ -598,9 +606,11 @@
|
||||||
|
|
||||||
;; 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)
|
||||||
|
|
@ -608,9 +618,11 @@
|
||||||
|
|
||||||
;; 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)
|
||||||
|
|
@ -618,9 +630,11 @@
|
||||||
|
|
||||||
;; 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,6 +10,7 @@
|
||||||
(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
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
(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,11 +367,6 @@
|
||||||
|
|
||||||
;; 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,56 +188,7 @@
|
||||||
(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,62 +34,11 @@
|
||||||
(update-player-display)
|
(update-player-display)
|
||||||
(update-volume)
|
(update-volume)
|
||||||
|
|
||||||
;; Setup live stream with reduced buffering and reconnect logic
|
;; Setup live stream with reduced buffering
|
||||||
(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,30 +12,6 @@
|
||||||
(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"
|
||||||
|
|
@ -61,35 +37,27 @@
|
||||||
(: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)
|
(when (and audio-element canvas-element (not *audio-context*))
|
||||||
;; Store current audio element
|
;; Create Audio Context
|
||||||
(setf *current-audio-element* audio-element)
|
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
||||||
|
(ps:@ window |webkitAudioContext|))))
|
||||||
|
|
||||||
;; Only create audio context and media source once
|
;; Create Analyser Node
|
||||||
(when (not *audio-context*)
|
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
||||||
;; Create Audio Context
|
(setf (ps:@ *analyser* |fftSize|) 256)
|
||||||
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
||||||
(ps:@ window |webkitAudioContext|))))
|
|
||||||
|
|
||||||
;; Create Analyser Node
|
;; Connect audio source to analyser
|
||||||
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
(let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
|
||||||
(setf (ps:@ *analyser* |fftSize|) 256)
|
(ps:chain source (connect *analyser*))
|
||||||
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
(ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
|
||||||
|
|
||||||
;; 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 if not already running
|
;; Start visualization
|
||||||
(when (not *animation-id*)
|
(draw-spectrum))))
|
||||||
(draw-spectrum)))))
|
|
||||||
|
|
||||||
(defun draw-spectrum ()
|
(defun draw-spectrum ()
|
||||||
"Draw the spectrum analyzer visualization"
|
"Draw the spectrum analyzer visualization"
|
||||||
|
|
@ -101,8 +69,7 @@
|
||||||
(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))
|
||||||
|
|
||||||
|
|
@ -110,68 +77,21 @@
|
||||||
(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))
|
||||||
|
|
||||||
;; Get current theme colors
|
;; Draw bars
|
||||||
(let ((theme (ps:getprop *themes* *current-theme*)))
|
(dotimes (i buffer-length)
|
||||||
(cond
|
(setf bar-height (/ (* (aref data-array i) height) 256))
|
||||||
;; Bar graph style
|
|
||||||
((= *current-style* "bars")
|
|
||||||
(setf x 0)
|
|
||||||
(dotimes (i buffer-length)
|
|
||||||
(setf bar-height (/ (* (aref data-array i) height) 256))
|
|
||||||
|
|
||||||
;; Create gradient for each bar using theme colors
|
;; Create gradient for each bar
|
||||||
(let ((gradient (ps:chain *canvas-ctx*
|
(let ((gradient (ps:chain *canvas-ctx*
|
||||||
(create-linear-gradient 0 (- height bar-height) 0 height))))
|
(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 "#00ff00"))
|
||||||
(ps:chain gradient (add-color-stop 0.5 (ps:@ theme mid)))
|
(ps:chain gradient (add-color-stop 0.5 "#00aa00"))
|
||||||
(ps:chain gradient (add-color-stop 1 (ps:@ theme bottom)))
|
(ps:chain gradient (add-color-stop 1 "#005500"))
|
||||||
|
|
||||||
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
|
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
|
||||||
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
|
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
|
||||||
|
|
||||||
(incf x bar-width))))
|
(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))
|
|
||||||
|
|
||||||
(dotimes (i buffer-length)
|
|
||||||
(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)))
|
|
||||||
|
|
||||||
(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"
|
||||||
|
|
@ -179,47 +99,9 @@
|
||||||
(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,12 +1161,10 @@
|
||||||
:opacity 1
|
:opacity 1
|
||||||
:background-color #(color-accented-black)))
|
:background-color #(color-accented-black)))
|
||||||
|
|
||||||
;; Live stream pulse animation (like old MacBook sleep indicator)
|
;; Live stream blink animation
|
||||||
(.live-stream-indicator
|
(.live-stream-indicator
|
||||||
:animation "asteroid-pulse 2s ease-in-out infinite")
|
:animation "asteroid-blink 1s steps(5, start) infinite")
|
||||||
|
|
||||||
(:keyframes asteroid-pulse
|
(:keyframes asteroid-blink
|
||||||
(0% :opacity 1)
|
(to :visibility "hidden"))
|
||||||
(50% :opacity 0.3)
|
|
||||||
(100% :opacity 1))
|
|
||||||
) ;; End of let block
|
) ;; End of let block
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,6 @@
|
||||||
<!-- 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,27 +26,6 @@
|
||||||
<!-- 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,27 +20,6 @@
|
||||||
<!-- 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