Compare commits

..

38 Commits

Author SHA1 Message Date
Glenn Thompson 3c44bd5f37 refactor: Move spectrum-analyzer.lisp to parenscript/ directory
Organize all Parenscript-generated JavaScript files together in the
parenscript/ module for better code organization.
2025-12-04 04:04:20 +03:00
Glenn Thompson 53658fb023 Merge main into parenscript-conversion to sync with spectrum analyzer feature 2025-12-04 03:48:09 +03:00
Glenn Thompson 6817e27483 Merge main into parenscript-conversion 2025-11-24 07:42:25 +03:00
Glenn Thompson 7493885e4e Add phase metadata to stream-queue.m3u 2025-11-24 07:35:14 +03:00
Glenn Thompson 4bfc31a3c3 Merge upstream/main into main 2025-11-24 07:34:26 +03:00
Glenn Thompson 61570fe2e6 fix: Remove extra closing paren in frontend-partials.lisp 2025-11-22 10:53:52 +03:00
Glenn Thompson 070e8d8dac feat: Update ParenScript users.lisp with API fixes and audio player color
- Fix user management API endpoints to match backend changes
- Add user-id to form data for role updates and activation/deactivation
- Change endpoints from /users/{id}/action to /user/action format
- Handle Radiance API data wrapping properly
- Add confirmation prompt for user activation
- Update audio player background color to match app panels (#1a2332)

Incorporates changes from main branch users.js updates
2025-11-22 10:52:05 +03:00
Glenn Thompson ed13589202 Merge main into parenscript-conversion
- Resolve conflicts in frontend-partials.lisp, asteroid.lass, and templates
- Keep improved recently-played styling from parenscript-conversion branch
- Incorporate user management updates from main
2025-11-22 10:50:42 +03:00
Glenn Thompson 30ff73a3e2 Merge remote-tracking branch 'upstream/main' 2025-11-22 10:22:19 +03:00
Glenn Thompson f691a1edc8 style: Update recently played track styling (ParenScript)
- Change track link colors: cyan default, green hover, blue visited
- Add equal left/right padding (12px) to track-list for centered separators
- Increase track-item top/bottom padding from 6px to 10px for better spacing
- Move track-list and track-item inside recently-played-list block for proper CSS nesting
- Matches styling changes from recently-played-tracks branch
2025-11-21 14:34:36 +03:00
Glenn Thompson 27f362f954 refactor: Make track name clickable with external link icon (ParenScript)
- Update ParenScript to make track name a clickable link
- Add external link icon next to track name
- Remove separate MusicBrainz link to reduce clutter
- Fix LASS hover syntax using :and combinator at correct nesting level
- Update CSS styling for track-link with proper hover effects
- Improves UX by making the primary action (clicking track) more intuitive
2025-11-21 09:49:45 +03:00
Glenn Thompson e3e3a144d4 Merge remote-tracking branch 'upstream/main' 2025-11-21 08:35:30 +03:00
Glenn Thompson 578306f06f feat: Add recently-played tracks feature with ParenScript
- Convert recently-played.js to ParenScript in parenscript/recently-played.lisp
- Add API endpoint /api/asteroid/recently-played
- Add track monitoring in icecast-now-playing to populate recently-played list
- Add recently-played panel to front-page.ctml and front-page-content.ctml
- Add LASS styling for recently-played section
- Fix ParenScript issues: use ps:ps instead of ps:ps* with quote, use aref for innerHTML
- Display last 3 tracks with time ago formatting and MusicBrainz search links
2025-11-20 11:26:26 +03:00
Glenn Thompson 6bbc3d0b6a fix: Correct parenthesis mismatches in player.lisp and frontend-partials.lisp
- Fixed missing closing paren in save-queue-as-playlist function
- Fixed extra closing paren in icecast-now-playing function
- Updated player.lisp with upstream changes from player.js:
  * Removed array indexing for track properties
  * Added RADIANCE API wrapper handling
  * Complete save-queue-as-playlist implementation
- Build and server startup now working correctly
2025-11-20 07:36:10 +03:00
Glenn Thompson a08e42f752 chore: Add favicon images and clean up patch file 2025-11-20 07:16:11 +03:00
Glenn Thompson 24e6859aa0 fix: Admin login and authentication issues
- Fix undefined uri-path function - use radiance:path instead
- Fix redirect paths for subdomain routing (remove /asteroid prefix)
- Add error handling and debug logging to admin page
- Fix login redirect to use correct paths for asteroid.localhost
- Add debug output to track authentication flow
2025-11-20 07:16:03 +03:00
Glenn Thompson d540c87cfc fix: Use uri-path instead of :external representation for API detection
- Replace uri-to-url with :representation :external with uri-path
- Fixes issue where full URLs like http://asteroid.radio.localhost were generated
- .localhost domains resolve to 127.0.0.1 which breaks on remote servers
- Path-only approach works for both local and remote deployments
- Follows Radiance best practices: :external is only for redirect URLs
2025-11-20 07:14:23 +03:00
Glenn Thompson 16d81e8ccc Document frontend-partials.lisp changes in ParenScript experiment
- Added details about listener count aggregation across all mount points
- Documented stray ^ character fix
- Documented error handler additions
- Documented debug logging additions
- Cross-referenced error variable removal to Challenge 3
2025-11-20 07:13:34 +03:00
Glenn Thompson 5f77b4cd4f Complete ParenScript migration: player.js and admin.js converted
- Converted player.js to parenscript/player.lisp
- Converted admin.js to parenscript/admin.lisp
- Fixed ParenScript compilation errors (push macro, != operator, error handlers)
- Fixed now-playing display with proper Icecast stats parsing
- Aggregated listener counts across all three stream mount points (mp3, aac, low)
- Updated documentation with all lessons learned and ParenScript patterns
- All JavaScript files now successfully converted to ParenScript
- Application maintains 100% original functionality
2025-11-20 07:13:32 +03:00
Glenn Thompson d0e40cccad fix: Comment out Quicklisp check in build script and update ParenScript docs
- Allow building when Quicklisp is already loaded
- Update ParenScript resources with correct GitLab repository URL
2025-11-20 07:07:57 +03:00
glenneth 263dc8a800 docs: Update ParenScript experiment with profile.js and users.js lessons
Added documentation for the two latest conversions:
- Marked profile.js and users.js as complete
- Documented modulo operator issue (use 'rem' not '%')
- Documented property access with hyphens (use ps:getprop)
- Documented HTML generation patterns
- Documented conditional attributes in templates
- Added comprehensive summary of 10 key ParenScript patterns

Status: 4 of 6 JavaScript files successfully converted
Remaining: admin.js, player.js (complex, 610 lines)
2025-11-20 07:07:27 +03:00
glenneth 022b1d8b96 feat: Convert users.js to ParenScript
Successfully converted users.js with all functionality:
- User stats display (total, active, admin, DJ counts)
- Load users list with table display
- Change user role (UI working, backend may need fixes)
- Activate/deactivate users
- Create new user form
- Auto-refresh stats every 30 seconds

Generated JavaScript working correctly.

Files:
- parenscript/users.lisp - ParenScript source
- asteroid.asd - Added users to parenscript module
- asteroid.lisp - Added users.js to static route interception
- static/js/users.js - Removed from git (backed up as .original)

Four files successfully converted to ParenScript!
Remaining: admin.js, player.js
2025-11-20 07:07:27 +03:00
glenneth cc79ba7330 feat: Convert profile.js to ParenScript
Successfully converted profile.js with all functionality:
- Profile data loading (username, role, join date, last active)
- Listening statistics display
- Recent tracks display
- Top artists display
- Password change form
- Export listening data
- Clear listening history
- Toast notifications

Generated JavaScript working correctly after fixing modulo operator.

Key learning: Use 'rem' instead of '%' for modulo in ParenScript.

Files:
- parenscript/profile.lisp - ParenScript source
- asteroid.asd - Added profile to parenscript module
- asteroid.lisp - Added profile.js to static route interception
- static/js/profile.js - Removed from git (backed up as .original)
- static/js/player.js - Restored (skipped for now, too complex)

Three files successfully converted to ParenScript\!
2025-11-20 07:07:27 +03:00
glenneth 3d7b08119a docs: Update ParenScript experiment documentation
Moved PARENSCRIPT-EXPERIMENT.org to docs/ directory.

Updates:
- Marked front-page.js as complete in Phase 2
- Removed duplicate front-page.js from Phase 3
- Added conversion progress section with both files
- Documented front-page.js specific patterns:
  * Global variables with defvar
  * String concatenation with +
  * Conditional logic with cond
  * Object property access with ps:getprop
- Listed all tested features for front-page.js

Status: 2 of 6 JavaScript files converted successfully
2025-11-20 07:07:27 +03:00
glenneth c35ae5a1f0 feat: Convert front-page.js to ParenScript
Successfully converted front-page.js with all functionality:
- Stream quality configuration and switching
- Now playing updates (every 10 seconds)
- Pop-out player functionality
- Frameset mode toggle
- Auto-reconnect on stream errors

Generated JavaScript: 6900 characters
No browser errors, all features working

Files:
- parenscript/front-page.lisp - ParenScript source
- asteroid.asd - Added front-page to parenscript module
- asteroid.lisp - Added front-page.js to static route interception
- static/js/front-page.js - Removed from git (backed up as .original)

Two files successfully converted to ParenScript!
2025-11-20 07:07:27 +03:00
glenneth 0d50f01a07 fix: Replace async/await with promise chains in ParenScript
ParenScript doesn't support async/await syntax properly. Changed to use
promise chains with .then() which compiles correctly.

Result:
- No JavaScript errors
- Auth UI working correctly
- Generated JS: 1386 characters
- First successful ParenScript replacement complete\!

Next: Can convert more JS files (profile.js, users.js, etc.)
2025-11-20 07:07:27 +03:00
glenneth 3c2ddf84c0 fix: ParenScript compilation working - intercept static route
The issue was route ordering. Since Radiance matches routes in load order,
we couldn't override the static file route. Solution: intercept the static
route and check if path is 'js/auth-ui.js', then serve ParenScript-compiled
JavaScript instead.

Changes:
- Compile ParenScript to string at load time (stored in *auth-ui-js*)
- Intercept static route to serve ParenScript for auth-ui.js
- JavaScript successfully generated (1290 chars)
- Ready for browser testing
2025-11-20 07:07:27 +03:00
glenneth b12e366d2c fix: Move ParenScript route before static file route
The ParenScript route must come before the catch-all static route
to properly override /static/js/auth-ui.js with dynamically compiled
JavaScript. Routes are matched in order, so specific routes must
precede general patterns.
2025-11-20 07:07:27 +03:00
glenneth 3c76436e81 fix: Correct parenthesis count in auth-ui ParenScript
Removed extra closing parenthesis that was causing compilation error.
Build now succeeds with ParenScript version of auth-ui.js
2025-11-20 07:07:27 +03:00
glenneth 8b839daf0a experiment: Replace auth-ui.js with ParenScript version
- Removed static/js/auth-ui.js (backed up as .original)
- Templates still reference /asteroid/static/js/auth-ui.js
- Route now serves dynamically compiled ParenScript
- Ready to test ParenScript replacement
2025-11-20 07:07:27 +03:00
glenneth ec00843a90 experiment: Convert auth-ui.js to ParenScript
- Created parenscript/auth-ui.lisp with ParenScript version
- Added route to serve compiled JavaScript at /static/js/auth-ui.js
- Updated asteroid.asd to include parenscript module
- First conversion: auth-ui.js (authentication UI state management)

The ParenScript code compiles to equivalent JavaScript and is served
dynamically. This allows us to write client-side code in Lisp.
2025-11-20 07:07:27 +03:00
glenneth aa4ed06d7f experiment: Add ParenScript setup and utilities
- Add parenscript dependency to asteroid.asd
- Create parenscript-utils.lisp with helper functions and macros
- Add PARENSCRIPT-EXPERIMENT.org documenting the conversion plan
- Goal: Replace all JavaScript with ParenScript for full-stack Lisp
2025-11-20 07:07:24 +03:00
Glenn Thompson cec3763403 Merge remote-tracking branch 'upstream/main' 2025-11-20 07:04:29 +03:00
Glenn Thompson c198775083 Merge remote-tracking branch 'upstream/main' 2025-11-18 07:15:32 +03:00
Glenn Thompson d187a01641 Merge remote-tracking branch 'upstream/main' 2025-11-17 06:34:49 +03:00
Glenn Thompson f498008d2a Merge remote-tracking branch 'upstream/main' 2025-11-15 18:43:48 +03:00
Glenn Thompson 96a3ce2b64 Merge remote-tracking branch 'upstream/main' 2025-11-14 09:08:31 +03:00
Glenn Thompson 63d606b39b fix: Use sequential mode in liquidsoap to play through entire playlist
The playlist was stuck on the first track because mode='normal' stops
after playing once. Changed to mode='sequential' which plays through
the entire playlist in order and then loops.

Also improved reload mechanism:
- Use reload_mode='watch' for efficient file change detection
- Increased reload interval to 5 minutes (less disruptive)
2025-11-13 07:08:25 +03:00
11 changed files with 52 additions and 324 deletions

View File

@ -558,9 +558,11 @@
(cond
;; Serve ParenScript-compiled auth-ui.js
((string= path "js/auth-ui.js")
(format t "~%=== SERVING PARENSCRIPT auth-ui.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(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"))
(error (e)
(format t "ERROR generating auth-ui.js: ~a~%" e)
@ -568,9 +570,11 @@
;; Serve ParenScript-compiled front-page.js
((string= path "js/front-page.js")
(format t "~%=== SERVING PARENSCRIPT front-page.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(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"))
(error (e)
(format t "ERROR generating front-page.js: ~a~%" e)
@ -578,9 +582,11 @@
;; Serve ParenScript-compiled profile.js
((string= path "js/profile.js")
(format t "~%=== SERVING PARENSCRIPT profile.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-profile-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating profile.js: ~a~%" e)
@ -588,9 +594,11 @@
;; Serve ParenScript-compiled users.js
((string= path "js/users.js")
(format t "~%=== SERVING PARENSCRIPT users.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-users-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating users.js: ~a~%" e)
@ -598,9 +606,11 @@
;; Serve ParenScript-compiled admin.js
((string= path "js/admin.js")
(format t "~%=== SERVING PARENSCRIPT admin.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-admin-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating admin.js: ~a~%" e)
@ -608,9 +618,11 @@
;; Serve ParenScript-compiled player.js
((string= path "js/player.js")
(format t "~%=== SERVING PARENSCRIPT player.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-player-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating player.js: ~a~%" e)
@ -618,9 +630,11 @@
;; Serve ParenScript-compiled recently-played.js
((string= path "js/recently-played.js")
(format t "~%=== SERVING PARENSCRIPT recently-played.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(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"))
(error (e)
(format t "ERROR generating recently-played.js: ~a~%" e)

View File

@ -10,6 +10,7 @@
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
(format t "DEBUG: Fetching Icecast stats from ~a~%" icecast-url)
(when response
(let ((xml-string (if (stringp response)
response
@ -33,6 +34,7 @@
(aref groups 0)
"Unknown")))
"Unknown")))
(format t "DEBUG: Parsed title=~a, total-listeners=~a~%" title total-listeners)
;; Track recently played if title changed
(when (and title

View File

@ -367,11 +367,6 @@
;; Live stream info update
(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
(fetch "/api/asteroid/partial/now-playing-inline")
(then (lambda (response)

View File

@ -188,56 +188,7 @@
(ps:chain audio-element (load))
(ps:chain (ps:chain audio-element (play))
(catch (lambda (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))))))))
(ps:chain console (log "Reload failed:" err))))))))))
;; Check frameset preference
(let ((path (ps:@ window location pathname))

View File

@ -34,62 +34,11 @@
(update-player-display)
(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"))))
(when live-audio
;; Reduce buffer to minimize delay
(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)))))
)))
(setf (ps:@ live-audio preload) "none")))
;; Restore user quality preference
(let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))

View File

@ -12,30 +12,6 @@
(defvar *canvas* nil)
(defvar *canvas-ctx* 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 ()
"Initialize the spectrum analyzer"
@ -61,35 +37,27 @@
(:catch (e)
(ps:chain console (log "Cross-frame access error:" e)))))
(when (and audio-element canvas-element)
;; Store current audio element
(setf *current-audio-element* audio-element)
(when (and audio-element canvas-element (not *audio-context*))
;; Create Audio Context
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
(ps:@ window |webkitAudioContext|))))
;; Only create audio context and media source once
(when (not *audio-context*)
;; Create Audio Context
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
(ps:@ window |webkitAudioContext|))))
;; Create Analyser Node
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
(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")))
;; Create Analyser Node
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
(setf (ps:@ *analyser* |fftSize|) 256)
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
;; Connect audio source to analyser
(let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
(ps:chain source (connect *analyser*))
(ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
;; Setup canvas
(setf *canvas* canvas-element)
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
;; Start visualization if not already running
(when (not *animation-id*)
(draw-spectrum)))))
;; Start visualization
(draw-spectrum))))
(defun draw-spectrum ()
"Draw the spectrum analyzer visualization"
@ -101,8 +69,7 @@
(height (ps:@ *canvas* height))
(bar-width (/ width buffer-length))
(bar-height 0)
(x 0)
(is-muted (and *current-audio-element* (ps:@ *current-audio-element* muted))))
(x 0))
(ps:chain *analyser* (get-byte-frequency-data data-array))
@ -110,68 +77,21 @@
(setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(0, 0, 0, 0.2)")
(ps:chain *canvas-ctx* (fill-rect 0 0 width height))
;; Get current theme colors
(let ((theme (ps:getprop *themes* *current-theme*)))
(cond
;; 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
(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))
;; Draw bars
(dotimes (i buffer-length)
(setf bar-height (/ (* (aref data-array i) height) 256))
;; Create gradient for each bar
(let ((gradient (ps:chain *canvas-ctx*
(create-linear-gradient 0 (- height bar-height) 0 height))))
(ps:chain gradient (add-color-stop 0 "#00ff00"))
(ps:chain gradient (add-color-stop 0.5 "#00aa00"))
(ps:chain gradient (add-color-stop 1 "#005500"))
(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)))
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
(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)))))
(incf x bar-width)))))
(defun stop-spectrum-analyzer ()
"Stop the spectrum analyzer"
@ -179,47 +99,9 @@
(cancel-animation-frame *animation-id*)
(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
(ps:chain document (add-event-listener "DOMContentLoaded"
(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"))
(ps:chain document (get-element-by-id "persistent-audio")))))

View File

@ -1161,12 +1161,10 @@
:opacity 1
:background-color #(color-accented-black)))
;; Live stream pulse animation (like old MacBook sleep indicator)
;; Live stream blink animation
(.live-stream-indicator
:animation "asteroid-pulse 2s ease-in-out infinite")
:animation "asteroid-blink 1s steps(5, start) infinite")
(:keyframes asteroid-pulse
(0% :opacity 1)
(50% :opacity 0.3)
(100% :opacity 1))
(:keyframes asteroid-blink
(to :visibility "hidden"))
) ;; End of let block

View File

@ -26,27 +26,6 @@
<!-- Spectrum Analyzer Canvas -->
<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>
<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>
<nav class="nav">

View File

@ -26,27 +26,6 @@
<!-- Spectrum Analyzer Canvas -->
<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>
<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>
<nav class="nav">

View File

@ -20,27 +20,6 @@
<!-- Spectrum Analyzer Canvas -->
<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>
<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 class="nav">