diff --git a/asteroid.asd b/asteroid.asd index 718182f..80f5868 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -41,7 +41,8 @@ (:file "conditions") (:file "database") (:file "template-utils") - (:file "spectrum-analyzer") + (:module :parenscript + :components ((:file "spectrum-analyzer"))) (:file "stream-media") (:file "user-management") (:file "playlist-management") diff --git a/docs/API-ENDPOINTS.org b/docs/API-ENDPOINTS.org index 1e463ef..76146f9 100644 --- a/docs/API-ENDPOINTS.org +++ b/docs/API-ENDPOINTS.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - API Endpoints Reference #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/API-REFERENCE.org b/docs/API-REFERENCE.org index 21fbea0..191b2ff 100644 --- a/docs/API-REFERENCE.org +++ b/docs/API-REFERENCE.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - API Reference #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Current Interfaces diff --git a/docs/DEVELOPMENT.org b/docs/DEVELOPMENT.org index e607683..e246fc1 100644 --- a/docs/DEVELOPMENT.org +++ b/docs/DEVELOPMENT.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - Development Guide #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Development Setup diff --git a/docs/DOCKER-STREAMING.org b/docs/DOCKER-STREAMING.org index 75592ed..0cf812b 100644 --- a/docs/DOCKER-STREAMING.org +++ b/docs/DOCKER-STREAMING.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - Docker Streaming Setup #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Docker Streaming Overview diff --git a/docs/INSTALLATION.org b/docs/INSTALLATION.org index 2bf289d..3d4ee53 100644 --- a/docs/INSTALLATION.org +++ b/docs/INSTALLATION.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - Installation Guide #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Installation Overview diff --git a/docs/PLAYLIST-SYSTEM.org b/docs/PLAYLIST-SYSTEM.org index 3835687..1595e18 100644 --- a/docs/PLAYLIST-SYSTEM.org +++ b/docs/PLAYLIST-SYSTEM.org @@ -1,6 +1,6 @@ #+TITLE: Playlist System - Complete (MVP) #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/POSTGRESQL-SETUP.org b/docs/POSTGRESQL-SETUP.org index 03d4ccc..8026f1c 100644 --- a/docs/POSTGRESQL-SETUP.org +++ b/docs/POSTGRESQL-SETUP.org @@ -1,6 +1,6 @@ #+TITLE: PostgreSQL Setup for Asteroid Radio #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/PROJECT-HISTORY.org b/docs/PROJECT-HISTORY.org index 6fdb7aa..f34308c 100644 --- a/docs/PROJECT-HISTORY.org +++ b/docs/PROJECT-HISTORY.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - Project Development History #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 #+DESCRIPTION: Comprehensive history of the Asteroid Radio project from inception to present * Project Overview @@ -11,7 +11,8 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea - *Backend*: Common Lisp (SBCL), Radiance web framework - *Streaming*: Icecast2, Liquidsoap - *Database*: PostgreSQL (configured, ready for migration) -- *Frontend*: HTML5, JavaScript, CLIP templating, LASS (CSS in Lisp) +- *Frontend*: HTML5, JavaScript, Parenscript, CLIP templating, LASS (CSS in Lisp) +- *Audio Visualization*: Web Audio API, Canvas - *Infrastructure*: Docker, Docker Compose * Project Timeline @@ -231,19 +232,43 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea - Synchronized with upstream/main - Prepared comprehensive documentation PR +** Phase 9: Visual Audio Features (December 2025) + +*** 2025-12-06: Real-Time Spectrum Analyzer +- *Lead*: Brian O'Reilly (Fade), Glenn Thompson +- Implemented spectrum analyzer using Parenscript +- Web Audio API integration for real-time visualization +- Dynamic JavaScript generation via API endpoint +- Canvas-based frequency display +- Works across all player modes (inline, pop-out, frameset) +- Lisp-to-JavaScript compilation for maintainability + * Development Statistics ** Contributors (by commit count) -1. Glenn Thompson (glenneth/Glenneth) - 135+ commits -2. Brian O'Reilly (Fade) - 55+ commits -3. Luis Pereira (easilok) - 23+ commits +1. Glenn Thompson (glenneth/Glenneth) - 236 commits +2. Brian O'Reilly (Fade) - 109 commits +3. Luis Pereira (easilok) - 63 commits -** Total Commits: 213+ commits +** Total Commits: 408 commits + +** Code Statistics +- *Total Lines of Code*: ~9,300 lines +- *Common Lisp*: 2,753 lines (.lisp, .asd) +- *JavaScript*: 2,315 lines (.js) +- *Templates*: 1,505 lines (.ctml) +- *Other*: 2,720 lines (CSS, Shell, Python, etc.) +- *Source Files*: 50 files + +** Release Information +- *Current Version*: Development (pre-1.0) +- *Tagged Releases*: None (continuous development) +- *Deployment Status*: Production-ready ** Active Development Period - Start: August 12, 2025 -- Current: November 1, 2025 -- Duration: ~2.75 months of active development +- Current: December 6, 2025 +- Duration: ~4 months of active development * Major Features Implemented @@ -266,6 +291,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea - ✅ Multiple quality options (AAC 96k, MP3 128k, MP3 64k) - ✅ ReplayGain volume normalization - ✅ Live now-playing information +- ✅ Real-time spectrum analyzer visualization - ✅ Icecast integration - ✅ Liquidsoap DJ controls - ✅ Stream queue management @@ -325,7 +351,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea - Parallel music scanning - Client-side caching -* Current State (November 2025) +* Current State (December 2025) ** Production Ready Features - Full music streaming platform @@ -333,6 +359,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea - Admin control panel - DJ controls - Multiple player modes +- Real-time spectrum analyzer - Complete Docker deployment (streams + application) - Multi-environment support with dynamic URLs - Comprehensive documentation @@ -392,9 +419,9 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea * Conclusion -Asteroid Radio has evolved from a simple concept into a full-featured internet radio platform in just 2.75 months of active development. The project demonstrates the power of Common Lisp for web development and the collaborative nature of open-source development. +Asteroid Radio has evolved from a simple concept into a full-featured internet radio platform in just 4 months of active development. The project demonstrates the power of Common Lisp for web development and the collaborative nature of open-source development. -With complete Docker deployment, comprehensive documentation, and a growing feature set, Asteroid Radio is ready for production use while continuing to evolve with regular improvements, bug fixes, and new features based on user needs and technical requirements. +With complete Docker deployment, real-time audio visualization, comprehensive documentation, and a growing feature set, Asteroid Radio is ready for production use while continuing to evolve with regular improvements, bug fixes, and new features based on user needs and technical requirements. ** Project Links - Repository: https://github.com/fade/asteroid @@ -403,4 +430,4 @@ With complete Docker deployment, comprehensive documentation, and a growing feat --- -*Last Updated: 2025-11-01* +*Last Updated: 2025-12-06* diff --git a/docs/PROJECT-OVERVIEW.org b/docs/PROJECT-OVERVIEW.org index 685d43d..0cf3dbe 100644 --- a/docs/PROJECT-OVERVIEW.org +++ b/docs/PROJECT-OVERVIEW.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio - Project Overview #+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade) -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * 🎯 Mission @@ -45,6 +45,8 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack - **HTML5** with semantic templates - **CSS3** with dark hacker theme - **JavaScript** for interactive features +- **Parenscript** - Lisp-to-JavaScript compiler for spectrum analyzer +- **Web Audio API** - Real-time audio visualization - **VT323 Font** for retro terminal aesthetic **Streaming:** @@ -81,6 +83,7 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack - ✅ **Music Library** - Track management with pagination, search, and filtering - ✅ **User Playlists** - Create, manage, and play personal music collections - ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players +- ✅ **Real-Time Spectrum Analyzer** - Visual audio frequency display using Web Audio API and Parenscript - ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based) - ✅ **REST API** - Comprehensive JSON API with 15+ endpoints - ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3) diff --git a/docs/README.org b/docs/README.org index 329643e..0cdf4d3 100644 --- a/docs/README.org +++ b/docs/README.org @@ -65,6 +65,7 @@ Pagination system for efficient browsing of large music libraries. - **Music Library**: Track management with pagination, search, and filtering - **Playlists**: User playlists with creation and playback - **Multiple Player Modes**: Inline, pop-out, and persistent frameset players +- **Real-Time Spectrum Analyzer**: Visual audio frequency display using Web Audio API - **Stream Queue Control**: Admin control over broadcast stream queue - **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers - **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3 @@ -147,5 +148,5 @@ For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Proje --- -*Last Updated: 2025-10-26* -*Documentation Version: 3.0* +*Last Updated: 2025-12-06* +*Documentation Version: 3.1* diff --git a/docs/STREAM-CONTROL.org b/docs/STREAM-CONTROL.org index a49a497..017d684 100644 --- a/docs/STREAM-CONTROL.org +++ b/docs/STREAM-CONTROL.org @@ -1,6 +1,6 @@ #+TITLE: Stream Queue Control System #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/TESTING.org b/docs/TESTING.org index 2ae88cc..74f74a7 100644 --- a/docs/TESTING.org +++ b/docs/TESTING.org @@ -1,6 +1,6 @@ #+TITLE: Asteroid Radio Testing Guide #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/TRACK-PAGINATION-SYSTEM.org b/docs/TRACK-PAGINATION-SYSTEM.org index 10bf150..574e3e7 100644 --- a/docs/TRACK-PAGINATION-SYSTEM.org +++ b/docs/TRACK-PAGINATION-SYSTEM.org @@ -1,6 +1,6 @@ #+TITLE: Track Pagination System - Complete #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/docs/USER-MANAGEMENT-SYSTEM.org b/docs/USER-MANAGEMENT-SYSTEM.org index f182ce2..8704326 100644 --- a/docs/USER-MANAGEMENT-SYSTEM.org +++ b/docs/USER-MANAGEMENT-SYSTEM.org @@ -1,6 +1,6 @@ #+TITLE: User Management System - Complete #+AUTHOR: Asteroid Radio Development Team -#+DATE: 2025-10-26 +#+DATE: 2025-12-06 * Overview diff --git a/spectrum-analyzer.lisp b/parenscript/spectrum-analyzer.lisp similarity index 100% rename from spectrum-analyzer.lisp rename to parenscript/spectrum-analyzer.lisp diff --git a/static/js/front-page.js b/static/js/front-page.js index af2fb68..b719ce8 100644 --- a/static/js/front-page.js +++ b/static/js/front-page.js @@ -113,6 +113,8 @@ window.addEventListener('DOMContentLoaded', function() { if (audioElement) { // Track pause timestamp to detect long pauses and reconnect let pauseTimestamp = null; + let isReconnecting = false; + let needsReconnect = false; const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds audioElement.addEventListener('pause', function() { @@ -121,9 +123,24 @@ window.addEventListener('DOMContentLoaded', function() { }); audioElement.addEventListener('play', function() { - if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + // Check if we need to reconnect after long pause + if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + needsReconnect = true; + console.log('Long pause detected, will reconnect when playing starts...'); + } + pauseTimestamp = null; + }); + + // Intercept the playing event to stop stale audio + audioElement.addEventListener('playing', function() { + if (needsReconnect && !isReconnecting) { + isReconnecting = true; + needsReconnect = false; console.log('Reconnecting stream after long pause to clear stale buffers...'); + // Stop the stale audio immediately + audioElement.pause(); + // Reset spectrum analyzer before reconnect if (typeof resetSpectrumAnalyzer === 'function') { resetSpectrumAnalyzer(); @@ -139,9 +156,10 @@ window.addEventListener('DOMContentLoaded', function() { initSpectrumAnalyzer(); console.log('Spectrum analyzer reinitialized after reconnect'); } + + isReconnecting = false; }, 500); } - pauseTimestamp = null; }); audioElement.addEventListener('error', function(e) { diff --git a/static/js/player.js b/static/js/player.js index cb794eb..8d8dae4 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -29,6 +29,8 @@ document.addEventListener('DOMContentLoaded', function() { // Track pause timestamp to detect long pauses and reconnect let pauseTimestamp = null; + let isReconnecting = false; + let needsReconnect = false; const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds liveAudio.addEventListener('pause', function() { @@ -37,9 +39,24 @@ document.addEventListener('DOMContentLoaded', function() { }); liveAudio.addEventListener('play', function() { - if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + // Check if we need to reconnect after long pause + if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + needsReconnect = true; + console.log('Long pause detected, will reconnect when playing starts...'); + } + pauseTimestamp = null; + }); + + // Intercept the playing event to stop stale audio + liveAudio.addEventListener('playing', function() { + if (needsReconnect && !isReconnecting) { + isReconnecting = true; + needsReconnect = false; console.log('Reconnecting live stream after long pause to clear stale buffers...'); + // Stop the stale audio immediately + liveAudio.pause(); + // Reset spectrum analyzer before reconnect if (typeof resetSpectrumAnalyzer === 'function') { resetSpectrumAnalyzer(); @@ -55,9 +72,10 @@ document.addEventListener('DOMContentLoaded', function() { initSpectrumAnalyzer(); console.log('Spectrum analyzer reinitialized after reconnect'); } + + isReconnecting = false; }, 500); } - pauseTimestamp = null; }); } // Restore user quality preference diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml index b9398c9..ea6f7b3 100644 --- a/template/audio-player-frame.ctml +++ b/template/audio-player-frame.ctml @@ -49,6 +49,8 @@ // Track pause timestamp to detect long pauses and reconnect let pauseTimestamp = null; + let isReconnecting = false; + let needsReconnect = false; const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds audioElement.addEventListener('pause', function() { @@ -56,23 +58,25 @@ console.log('Frame player stream paused at:', pauseTimestamp); }); - // Add event listeners for debugging - audioElement.addEventListener('waiting', function() { - console.log('Audio buffering...'); - }); - - audioElement.addEventListener('playing', function() { - console.log('Audio playing'); - }); - - audioElement.addEventListener('error', function(e) { - console.error('Audio error:', e); - }); - audioElement.addEventListener('play', function() { - if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + // Check if we need to reconnect after long pause + if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + needsReconnect = true; + console.log('Long pause detected, will reconnect when playing starts...'); + } + pauseTimestamp = null; + }); + + // Intercept the playing event to stop stale audio + audioElement.addEventListener('playing', function() { + if (needsReconnect && !isReconnecting) { + isReconnecting = true; + needsReconnect = false; console.log('Reconnecting frame player stream after long pause to clear stale buffers...'); + // Stop the stale audio immediately + audioElement.pause(); + // Reset spectrum analyzer before reconnect if (typeof resetSpectrumAnalyzer === 'function') { resetSpectrumAnalyzer(); @@ -88,9 +92,21 @@ initSpectrumAnalyzer(); console.log('Spectrum analyzer reinitialized after reconnect'); } + + isReconnecting = false; }, 500); + } else { + console.log('Audio playing'); } - pauseTimestamp = null; + }); + + // Add event listeners for debugging + audioElement.addEventListener('waiting', function() { + console.log('Audio buffering...'); + }); + + audioElement.addEventListener('error', function(e) { + console.error('Audio error:', e); }); const selector = document.getElementById('stream-quality'); diff --git a/template/popout-player.ctml b/template/popout-player.ctml index 7582eaa..136addf 100644 --- a/template/popout-player.ctml +++ b/template/popout-player.ctml @@ -136,6 +136,8 @@ // Track pause timestamp to detect long pauses and reconnect let pauseTimestamp = null; + let isReconnecting = false; + let needsReconnect = false; const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds audioElement.addEventListener('pause', function() { @@ -144,9 +146,24 @@ }); audioElement.addEventListener('play', function() { - if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + // Check if we need to reconnect after long pause + if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) { + needsReconnect = true; + console.log('Long pause detected, will reconnect when playing starts...'); + } + pauseTimestamp = null; + }); + + // Intercept the playing event to stop stale audio + audioElement.addEventListener('playing', function() { + if (needsReconnect && !isReconnecting) { + isReconnecting = true; + needsReconnect = false; console.log('Reconnecting popout stream after long pause to clear stale buffers...'); + // Stop the stale audio immediately + audioElement.pause(); + // Reset spectrum analyzer before reconnect if (typeof resetSpectrumAnalyzer === 'function') { resetSpectrumAnalyzer(); @@ -162,9 +179,10 @@ initSpectrumAnalyzer(); console.log('Spectrum analyzer reinitialized after reconnect'); } + + isReconnecting = false; }, 500); } - pauseTimestamp = null; }); audioElement.addEventListener('error', function(e) {