#+TITLE: Asteroid Radio - Internet Radio Streaming Platform #+AUTHOR: Asteroid Radio Development Team #+DATE: 2026-03-05 * Overview Asteroid Radio is a complete internet radio streaming platform built entirely in Common Lisp. It combines the [[https://shirakumo.github.io/radiance/][Radiance]] web framework with an in-process audio streaming pipeline powered by [[https://shirakumo.github.io/harmony/][Harmony]], [[https://shirakumo.github.io/cl-mixed/][cl-mixed]], and a custom streaming server called *CL-Streamer*. Everything runs in a single SBCL process — audio decoding, mixing, encoding, HTTP streaming, metadata, playlist scheduling, and the web frontend. No external streaming services are required. The only external dependency is PostgreSQL for persistent data. ** Project Links - *Repository*: https://github.com/fade/asteroid - *IRC*: #asteroid.music on irc.libera.chat - *Documentation*: See =docs/= directory for comprehensive guides * Key Features ** Live Internet Radio Streaming - Dual-format simultaneous output: 128kbps MP3 + 128kbps AAC - Real-time audio decoding via Harmony (FLAC, MP3 via cl-mixed backends) - Crossfade between tracks (3s overlap, configurable fade-in/fade-out) - ICY metadata protocol for now-playing information in media players - Burst-on-connect for instant playback start (~4s of buffered audio) ** CL-Streamer — The Audio Pipeline CL-Streamer is Asteroid Radio's own native Common Lisp audio streaming server, written from scratch as part of this project. It provides HTTP audio streaming with ICY metadata, dual-format encoding (MP3 + AAC), and real-time audio processing — replacing what previously required separate Icecast and Liquidsoap Docker containers. It lives in the =cl-streamer/= directory and is loaded as an ASDF system within the Asteroid Radio process. See =cl-streamer/README.org= for full technical details. Key components: - *streaming-drain* — custom Harmony drain that intercepts mixed audio and feeds it to encoders instead of a sound card - *MP3 encoder* — LAME via CFFI (=lame-ffi.lisp=) - *AAC encoder* — FDK-AAC via a C shim that avoids SBCL signal handler conflicts (=fdkaac-shim.c=, =fdkaac-ffi.lisp=) - *Broadcast buffer* — single-producer multi-consumer ring buffer per mount point; never blocks the encoder - *ICY metadata* — SHOUTcast-compatible metadata interleaved in the stream - *HTTP server* — usocket-based, serves =/asteroid.mp3= and =/asteroid.aac= directly ** Playlist Scheduling - Cron-based playlist scheduler with configurable time slots (=playlist-scheduler.lisp=) - M3U playlist files with =#EXTINF= metadata (=playlists/= directory) - Playback state persistence — saves current track/playlist position to disk (=.playback-state.lisp=) - Automatic resume from saved state on restart - Sequential playback with crossfade transitions ** Music Library Management - Database-backed track storage with metadata extraction - Support for MP3 and FLAC formats (decoded by cl-mixed backends) - Automatic metadata extraction using [[https://github.com/psilord/taglib][taglib]] (artist, title, album) - Track search, filtering, sorting, and pagination - Recursive directory scanning ** Web Interface - Radiance framework with CLIP templating - Admin dashboard with stream status, listener stats, and library management - Multiple player modes: inline, pop-out, and persistent frameset - Live stream integration with embedded HTML5 audio player - Responsive design for desktop and mobile - Role-based access control (Admin/DJ/Listener) - ParenScript-generated JavaScript (=parenscript/= directory) - LASS-generated CSS (=static/asteroid.lass=) ** Listener Statistics - In-process listener tracking (no external polling needed) - Geo IP lookup with caching (=listener-stats.lisp=) - Per-mount and aggregate listener counts - GDPR-compliant IP hashing and data retention * Architecture #+BEGIN_EXAMPLE ┌──────────────────────────────────────────────────────────────┐ │ SBCL Process │ │ │ │ ┌───────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ Harmony │──▶│ streaming- │──▶│ MP3 Encoder │ │ │ │ (decode, │ │ drain │ │ (LAME FFI) │──▶│ /asteroid.mp3 │ │ crossfade,│ │ (float→s16 │ └───────────────────┘ │ │ │ mix) │ │ conversion) │ ┌───────────────────┐ │ │ └───────────┘ └──────────────┘──▶│ AAC Encoder │ │ │ ▲ │ (FDK-AAC C shim) │──▶│ /asteroid.aac │ │ └───────────────────┘ │ │ ┌───────────┐ ┌───────────────────┐ │ │ │ Playlist │ ICY metadata ────▶│ CL-Streamer │ │ │ │ Scheduler │ Listener stats ◀──│ HTTP Server │ │ │ │ (cl-cron) │ │ (usocket) │ │ │ └───────────┘ └───────────────────┘ │ │ │ │ │ ┌───────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ Radiance │──▶│ Admin │ │ PostgreSQL │ │ │ │ Web Server │ │ Dashboard │ │ (external) │ │ │ │ (port 8080)│ │ + Player UI │ │ │ │ │ └───────────┘ └──────────────┘ └───────────────────┘ │ └──────────────────────────────────────────────────────────────┘ #+END_EXAMPLE ** How Audio Flows 1. *Decode* — Harmony loads a FLAC or MP3 file via cl-mixed-flac or cl-mixed-mpg123 2. *Mix* — during crossfade, two voices play simultaneously through Harmony's mixer 3. *Drain* — the custom =streaming-drain= reads interleaved float samples from the pack buffer 4. *Convert* — floats are converted to signed 16-bit PCM via CFFI 5. *Encode* — PCM is fed to LAME (MP3) and FDK-AAC (AAC) encoders in parallel 6. *Buffer* — encoded bytes are written to per-mount broadcast ring buffers 7. *Serve* — HTTP clients read from their position in the ring buffer with ICY metadata interleaving ** File Structure #+BEGIN_SRC asteroid/ ├── asteroid.lisp # Main server, Radiance routes, API endpoints ├── asteroid.asd # ASDF system definition ├── stream-harmony.lisp # Harmony/CL-Streamer integration, playback state ├── playlist-scheduler.lisp # Cron-based playlist scheduling ├── stream-control.lisp # Stream queue management ├── listener-stats.lisp # Listener statistics and geo IP ├── user-management.lisp # User administration ├── playlist-management.lisp # Playlist operations ├── user-playlists.lisp # User playlist features ├── user-profile.lisp # User profile management ├── track-requests.lisp # Track request system ├── auth-routes.lisp # Authentication routes ├── database.lisp # Database schema and helpers ├── Makefile # Build: compiles SBCL image ├── cl-streamer/ # Audio streaming subsystem │ ├── cl-streamer.asd # ASDF system definition │ ├── harmony-backend.lisp # Harmony integration, crossfade, play-list/play-file │ ├── stream-server.lisp # HTTP streaming server (usocket) │ ├── buffer.lisp # Broadcast ring buffer │ ├── encoder.lisp # MP3 encoder (LAME FFI) │ ├── aac-encoder.lisp # AAC encoder with frame accumulation │ ├── lame-ffi.lisp # CFFI bindings for libmp3lame │ ├── fdkaac-ffi.lisp # CFFI bindings for FDK-AAC (via shim) │ ├── fdkaac-shim.c # C shim for FDK-AAC (avoids SBCL signal conflicts) │ ├── libfdkaac-shim.so # Compiled shim shared library │ ├── icy-protocol.lisp # ICY metadata encoding │ └── README.org # Full CL-Streamer documentation ├── playlists/ # M3U playlist files for the scheduler ├── template/ # CLIP HTML templates ├── parenscript/ # ParenScript JavaScript sources ├── static/ # CSS (LASS) and assets ├── migrations/ # PostgreSQL schema migrations ├── docker/ # Docker config (PostgreSQL only) ├── docs/ # Comprehensive documentation └── music/ # Music library symlink #+END_SRC * Quick Start ** Prerequisites - *SBCL* — Steel Bank Common Lisp - *Quicklisp* — with Ultralisp distribution (for Harmony, cl-mixed) - *PostgreSQL* — running instance (Docker or native) - *System libraries*: =libmp3lame=, =libfdk-aac=, =libflac=, =libmpg123=, =libtagc= ** Building the FDK-AAC Shim CL-Streamer uses a thin C shim to call FDK-AAC, avoiding SBCL signal handler conflicts that cause recursive SIGSEGV when calling FDK-AAC directly via CFFI. The compiled =libfdkaac-shim.so= is included in the repository, but if you need to rebuild it (e.g. after updating =libfdk-aac=): #+BEGIN_SRC bash cd cl-streamer gcc -shared -fPIC -o libfdkaac-shim.so fdkaac-shim.c -lfdk-aac #+END_SRC The shim is loaded automatically at ASDF system load time from the =cl-streamer/= directory — no installation or =LD_LIBRARY_PATH= configuration needed. ** Build and Run #+BEGIN_SRC bash # Clone repository git clone https://github.com/fade/asteroid cd asteroid # Start PostgreSQL (if using Docker) cd docker && docker compose up -d postgres && cd .. # Build the SBCL image make # Run (set ASTEROID_STREAM_URL to the stream server address) ASTEROID_STREAM_URL=http://localhost:8000 ./asteroid #+END_SRC On startup, Asteroid will: 1. Start the Radiance web server on port 8080 2. Start the CL-Streamer HTTP server on port 8000 3. Initialise the Harmony audio pipeline with MP3 + AAC encoders 4. Connect to PostgreSQL and load the playlist schedule 5. Resume playback from saved state (if =.playback-state.lisp= exists) 6. Begin streaming audio on =/asteroid.mp3= and =/asteroid.aac= ** Access Points - *Web Interface*: http://localhost:8080/ - *Admin Panel*: http://localhost:8080/admin - *MP3 Stream*: http://localhost:8000/asteroid.mp3 (128kbps) - *AAC Stream*: http://localhost:8000/asteroid.aac (128kbps) * Playlist Scheduling Playlists are M3U files in the =playlists/= directory. The scheduler (=playlist-scheduler.lisp=) uses =cl-cron= to switch playlists at configured times. ** Schedule Configuration The schedule is stored in the database and loaded on startup. Example schedule: | Time (UTC) | Playlist | Description | |------------+------------------------+--------------------------------| | 00:00 | midnight-ambient.m3u | Deep, dark ambient | | 06:00 | morning-drift.m3u | Lighter, awakening ambient | | 12:00 | afternoon-orbit.m3u | Mid-energy floating ambient | | 13:00 | underworld-and-friends | Techno, electro, progressive | | 18:00 | evening-descent.m3u | Winding down transitional | ** M3U Format Playlists use =#EXTINF= metadata and =/app/music/= prefixed paths (rewritten to local library path at load time): #+BEGIN_SRC #EXTM3U #PLAYLIST:Underworld & Friends #EXTINF:-1,Underworld - Born Slippy (Nuxx) /app/music/Underworld - Second Toughest In The Infants (flac)/Second Toughest In The Infants (CD2)/01 Born Slippy (Nuxx).flac #EXTINF:-1,Orbital - Halcyon + On + On /app/music/Orbital/1993 - Orbital - Orbital 2 (Brown Album - TRUCD2, 828 386.2)/00. Halcyon + On + On.mp3 #+END_SRC ** Playback State Persistence The current track and playlist position are saved to =.playback-state.lisp= periodically. On restart, playback resumes from the saved position rather than restarting the playlist from the beginning. * Music Library Management ** Adding Music 1. Place MP3/FLAC files in the music library directory (symlinked from =music/library/=) 2. Navigate to the admin panel at =http://localhost:8080/admin= 3. Click "Scan Library" to index new tracks 4. Metadata (artist, title, album) is automatically extracted via taglib ** Supported Formats - *FLAC* — decoded by cl-mixed-flac (via libflac) - *MP3* — decoded by cl-mixed-mpg123 (via libmpg123) ** Path Mapping In Docker deployments, music paths use =/app/music/= prefix. In local development, =music/library/= is a symlink to the actual music directory. The M3U playlist loader rewrites paths accordingly. * User Management ** Roles - *Admin*: Full system access, user management, stream control, skip tracks - *DJ*: Content management, playlist creation, library access - *Listener*: Basic playback and personal playlists ** Default Credentials - Username: =admin= - Password: =asteroid123= - ⚠️ Change default password after first login * Player Modes ** Inline Player - Embedded in web pages - HTML5 audio with stream auto-reconnect ** Pop-Out Player - Standalone player window - Independent from main browser window ** Frameset Player - Bottom-frame persistent player - Audio continues during site navigation - Seamless listening experience * API Endpoints ** Status & Streaming - =GET /api/asteroid/status= - Server status - =GET /api/asteroid/icecast-status= - Stream status (now-playing, listeners; name kept for frontend compatibility) - =POST /api/asteroid/stream/skip= - Skip current track (admin) ** Track Management - =GET /api/asteroid/tracks= - List all tracks - =GET /api/asteroid/admin/tracks= - Admin track listing - =POST /api/asteroid/admin/scan-library= - Scan music library ** Playlist Management - =GET /api/asteroid/playlists= - List user playlists - =POST /api/asteroid/playlists/create= - Create playlist - =GET /api/asteroid/playlists/get= - Get playlist details - =POST /api/asteroid/playlists/add-track= - Add track to playlist ** Stream Queue Control (Admin) - =GET /api/asteroid/stream/queue= - Get broadcast queue - =POST /api/asteroid/stream/queue/add= - Add track to queue - =POST /api/asteroid/stream/queue/remove= - Remove from queue - =POST /api/asteroid/stream/queue/clear= - Clear queue See =docs/API-ENDPOINTS.org= for complete API documentation. * Database ** PostgreSQL - Primary database for tracks, users, playlists, sessions, schedule, listener stats - Docker container or native installation - Schema managed via migrations in =migrations/= directory - Connection configured in =config/radiance-postgres.lisp= - See =docs/POSTGRESQL-SETUP.org= for setup details * Dependencies ** Lisp Dependencies - =radiance= - Web framework - =harmony= - Audio framework (decode, mix, effects) - =cl-mixed= / =cl-mixed-flac= / =cl-mixed-mpg123= - Low-level audio mixing and decoding - =r-clip= - CLIP templating - =lass= - CSS preprocessing - =parenscript= - Lisp-to-JavaScript compiler - =cl-json= - JSON handling - =alexandria= - Common Lisp utilities - =bordeaux-threads= - Portable threading - =local-time= - Time handling - =taglib= - Audio metadata extraction - =cl-cron= - Cron-style job scheduling - =cffi= - C foreign function interface - =usocket= - Socket networking - =i-postmodern= - PostgreSQL interface (Radiance) - =ironclad= - Cryptographic hashing (passwords, IP hashing) - =log4cl= - Logging - =slynk= - Sly/SLIME interactive development ** System Libraries - =libmp3lame= - MP3 encoding (LAME) - =libfdk-aac= - AAC encoding (FDK-AAC, accessed via C shim) - =libflac= - FLAC decoding - =libmpg123= - MP3 decoding - =libtagc= - Audio metadata reading (taglib C bindings) ** Infrastructure - *PostgreSQL* - only external service required - *Docker* - optional, for running PostgreSQL * Resource Usage Running on a single SBCL process: - *RAM*: ~320MB RSS (real-time audio decode + dual MP3/AAC encode) - *CPU*: ~8% total (Harmony thread is ~5%) - *Threads*: ~18 * Testing ** Automated Test Suite #+BEGIN_SRC bash # Run comprehensive tests ./test-server.sh # Verbose mode ./test-server.sh -v #+END_SRC ** Test Coverage - 25+ automated tests - API endpoint validation - HTML page rendering - Static file serving - JSON response format - Authentication flows * Contributing ** Development Workflow 1. Fork the repository 2. Create a feature branch 3. Make your changes 4. Run test suite 5. Submit pull request ** Interactive Development Asteroid starts a Slynk server on port 4009. Connect from Emacs/Sly for live REPL access to the running system — inspect streams, modify playlists, debug audio issues without restarting. ** Community - *IRC*: #asteroid.music on irc.libera.chat - *Issues*: GitHub issue tracker - *Discussions*: GitHub discussions ** Core Team - Brian O'Reilly (Fade) - Project founder - Glenn Thompson (glenneth) - Core developer - Luis Pereira - UI/UX * Troubleshooting ** Stream Not Playing - Check that the process is running and CL-Streamer started (look for "CL-Streamer started on port 8000" in logs) - Test stream URLs: =curl -I http://localhost:8000/asteroid.mp3= - Check for audio file errors in the log (missing files, unsupported formats) - Verify music library symlink: =ls -la music/library/= ** SBCL Pathname Issues File paths containing square brackets (e.g. =[WEB FLAC]=) can cause =SIMPLE-ARRAY CHARACTER= errors because SBCL's =pathname= function interprets brackets as wildcard patterns. CL-Streamer uses =sb-ext:parse-native-namestring= to avoid this. ** Database Issues - Check PostgreSQL is running: =docker compose ps= (if using Docker) - Verify connection in =config/radiance-postgres.lisp= - Review application logs for connection errors ** Playback State - Saved to =.playback-state.lisp= in the project root - Delete this file to force a fresh start from the scheduled playlist - Check logs for "Resuming after track..." on startup For detailed troubleshooting, see documentation in =docs/= directory. * Documentation Comprehensive documentation available in the =docs/= directory: - *README.org* - Documentation index - *PROJECT-OVERVIEW.org* - Architecture and features - *PROJECT-HISTORY.org* - Development timeline and milestones - *INSTALLATION.org* - Complete installation guide - *DEVELOPMENT.org* - Developer setup and guidelines - *API-ENDPOINTS.org* - REST API reference - *STREAM-CONTROL.org* - Stream queue management - *USER-MANAGEMENT-SYSTEM.org* - User administration - *PLAYLIST-SYSTEM.org* - Playlist functionality - *TESTING.org* - Automated testing guide - *POSTGRESQL-SETUP.org* - Database setup See also =cl-streamer/README.org= for CL-Streamer technical documentation. * License See LICENSE file for details. * Acknowledgments Built with: - [[https://www.sbcl.org/][SBCL]] (Steel Bank Common Lisp) - [[https://shirakumo.github.io/radiance/][Radiance]] web framework (Shinmera) - [[https://shirakumo.github.io/harmony/][Harmony]] audio framework (Shinmera) - [[https://shirakumo.github.io/cl-mixed/][cl-mixed]] audio mixing library (Shinmera) - [[https://lame.sourceforge.io/][LAME]] MP3 encoder - [[https://github.com/mstorsjo/fdk-aac][FDK-AAC]] AAC encoder Special thanks to Yukari Hafner (Shinmera) for Harmony, cl-mixed, and Radiance, and to the Common Lisp community. * Credits ** Icons - [[https://www.flaticon.com/free-icons/cycle][Cycle icons created by meaicon - Flaticon]] (sync.png reconnect button) --- *Last Updated: 2026-03-05*