Commit Graph

419 Commits

Author SHA1 Message Date
Glenn Thompson da28c70254 Client-side stream sync with in-flight guards, fix connection exhaustion
Stream sync improvements:
- Server now sends changed_at timestamp + raw remaining seconds
- Client schedules UI updates based on changed_at + measured buffer lag
- Removed server-side delay logic entirely
- Poll interval set to 10s (was 15s, briefly 5s which caused issues)

Connection exhaustion fix:
- Added in-flight guards to update-mini-now-playing and poll-now-playing
- Prevents fetch pileup when server is slow or stalled
- Each poller skips if previous request hasn't completed

Other:
- Include changed_at in now-playing JSON API response
- Replace em dashes with hyphens throughout
- Update cl-streamer submodule (get-metadata-changed-at export)
2026-04-13 09:30:40 +01:00
Glenn Thompson 20ed7ecb02 Tune sync delay to 10s, fix countdown during transitions, add buffer bloat monitor
- Set *browser-buffer-seconds* to 10 (5s=23s early, 23s=14s late)
- Hide countdown timer during title transition window (titles mismatch)
- Add client-side buffer bloat monitor: logs buffer size every 30s,
  auto-reconnects if buffer exceeds 15s to prevent drift
- Reduce buffer monitor log spam (only log when >5s, check every 30s)
- Update cl-streamer submodule ref
2026-04-09 17:12:15 +01:00
Glenn Thompson 10c75f04e1 Stream sync, countdown timer, playback state fix, buffer bloat detection
Multi-pronged attempt to fix cumulative lag between audio playback,
now-playing display, and browser notifications:

== Synchronization (work in progress) ==
- Server-side time-based metadata delay via get-listener-now-playing
  with configurable *browser-buffer-seconds* parameter
- First track after restart syncs perfectly; subsequent tracks drift
  due to browser audio buffer bloat (buffer grows unbounded over time)
- Added client-side buffer bloat detection: monitors audio.buffered
  vs audio.currentTime and auto-reconnects when buffer exceeds 15s
- Added [STREAM-SYNC], [NOTIFY], [BUFFER] diagnostic logging to
  browser console for ongoing diagnosis

== Countdown timer ==
- Server exposes remaining seconds via pipeline-track-remaining
- now-playing JSON API includes 'remaining' field adjusted for
  browser buffer delay
- Player frame shows [mm:ss] countdown next to track title
- Main page now-playing area also shows countdown with its own
  polling (15s interval) and local 1-second ticker
- LASS styles for .track-countdown and .track-countdown-mini

== Playback state fix ==
- Fixed save-playback-state saving one track ahead of what was
  actually playing (was saving the track loading during crossfade)
- Uses *pending-save-file* with one-track delay so the saved state
  reflects the track that was actually heard

== Notifications ==
- All notification conditions working correctly per diagnostic logs
- show-track-notification logs supported/permission/enabled/last/title
- Notifications fire consistently on track changes
2026-04-09 16:32:58 +01:00
Glenn Thompson 76d331248b Replace r-simple-rate monkey-patches with fixed-window-check
The previous approach overrode simple-rate::tax-rate and rate:left
directly, but Radiance reloads r-simple-rate at startup and clobbers
the overrides.  New approach: define-page-with-limit and
define-api-with-limit now call fixed-window-check directly, bypassing
rate:with-limitation entirely.  The fixed-window logic is self-contained
and immune to module reload ordering.
2026-04-09 08:04:26 +01:00
Glenn Thompson 91686cd0cc Fix r-simple-rate sliding-window bug + normalize polling intervals
Rate limiter fix (limiter.lisp):
- Override simple-rate::tax-rate with fixed-window implementation
  The upstream version updates the timestamp on every request, which
  prevents the window from ever resetting while polling is active.
  This override only updates the timestamp when the window expires and
  the counter resets.
- Override rate:left to correctly report expired windows as full budget,
  so with-limitation does not block on stale tracking entries.
- These are monkey-patches on r-simple-rate; the upstream library is
  not modified.

Polling normalization:
- front-page.lisp: channel-name polling 10s → 15s (matches stream-player.lisp)

Context: In frameset mode, front-page.js and stream-player.js both poll
the channel-name and now-playing endpoints. The sliding-window bug meant
the rate limit counter never reset as long as requests kept arriving
within the 60s timeout, eventually exhausting the budget and producing
429 errors for all API endpoints.
2026-04-09 06:59:42 +01:00
Glenn Thompson 9042e78ae8 Merge branch 'main' into glenneth/cl-streamer-standalone 2026-04-07 09:14:44 +01:00
Brian O'Reilly b3790bcb25 fix: prevent debugger accumulation from vulnerability scanner probes
Bogus requests (e.g. /wp-login.php, /.env) from external scanners were
signalling FILE-TO-SERVE-DOES-NOT-EXIST and REQUEST-NOT-FOUND conditions
that dropped into the debugger when Swank/Slynk was connected. Enough
accumulated sessions would lock up the runtime.

Three defence-in-depth changes:
- Static file handler now probe-files before calling serve-file
- start-server reads ASTEROID_DEBUG env var to set radiance:*debugger*
- Override radiance:render-error-page for proper 404/403/500 responses

🅯 Brian O'Reilly <fade@deepsky.com>, 2026
2026-04-06 12:25:41 -04:00
Glenn Thompson 17882bb82c Normalize channel-name polling to 15s (was 10s) 2026-03-14 06:57:25 +00:00
Brian O'Reilly 4f7b63e1b9 docker port maps leak to external interface...
Unless they are explicitly bound to loopback, which I thought was the
default, but it is not. likely related to the interface between
bridges and ip tables in the Linux kernel, but anyhow, get literal
about the portmap interface address to prevent exposing the database
to the entire internet. With thanks to the friendly heads up email
from the German Federal Republic via Hetzner.
2026-03-14 06:52:09 +00:00
Brian O'Reilly 0a2ac0c409 docker port maps leak to external interface...
Unless they are explicitly bound to loopback, which I thought was the
default, but it is not. likely related to the interface between
bridges and ip tables in the Linux kernel, but anyhow, get literal
about the portmap interface address to prevent exposing the database
to the entire internet. With thanks to the friendly heads up email
from the German Federal Republic via Hetzner.
2026-03-13 17:21:22 -04:00
Glenn Thompson 5d7606fb3b Bump now-playing-json rate limit 30→120/min + update cl-streamer submodule
- frontend-partials.lisp: increase rate limit on now-playing-json from
  30 to 120 requests/minute to prevent 429 errors from concurrent
  frontend pollers (player + now-playing + popout)
- cl-streamer: catch serious-condition in play-list for native code
  crash recovery (23b5ee1)
2026-03-09 09:25:20 +00:00
Glenn Thompson 2a713d7386 Fix SBCL bracket-escaping in shuffle library scanner
stream-harmony.lisp:
- scan-music-library-files: use native-namestring instead of namestring
  when collecting file paths, producing real OS paths without SBCL's
  backslash escaping of brackets (e.g. [FLAC] not \[FLAC])
- Use parse-native-namestring for root directory entry point so dirs
  with brackets are not treated as wildcard patterns

cl-streamer submodule updated to d57a268:
- play-file, read-audio-metadata, play-list retry: same native-namestring
  fix to prevent double-escaping through parse-native-namestring

Fixes FLAC FILE slot unbound errors and SIGSEGV memory fault that
crashed the shuffle pipeline after consecutive failures on bracket paths.
2026-03-08 22:02:46 +03:00
Glenn Thompson 04a874ceb5 Update CL-STREAMER-STANDALONE.org: mark all phases done, add DSL rationale
- Phases 1-4 marked DONE with commit refs
- Step 1 (eliminate *server* global) and Step 2 (shuffle pipeline) documented
- Checklist items marked [X] for completed work
- Added design rationale: why keyword args over quoted-plist DSL
  (idiomatic, compile-time checking, no parser, composability, extensibility)
- Updated architecture diagram to show current state
- Remaining: integration test, source protocol, VPS deployment
2026-03-08 13:41:35 +03:00
Glenn Thompson 5efa49321e Add shuffle stream as second pipeline
stream-harmony.lisp:
- scan-music-library-files: recursive directory scanner for supported formats
- Cached shuffle library pool (2797 tracks, 1hr TTL refresh)
- shuffle-random-batch: picks N random tracks from pool
- refill-shuffle-queue: track-change hook keeps queue topped up
- on-shuffle-track-change: updates recently-played, refills queue
- shuffle-now-playing: now-playing stats for shuffle mounts
- start-shuffle-streaming: creates pipeline sharing curated server,
  registers hooks, seeds queue, starts play-list with crossfade
- stop-shuffle-streaming: stops pipeline without stopping shared server

asteroid.lisp:
- Start shuffle pipeline after curated pipeline on startup
- Stop/restart shuffle in stream restart endpoint
- Fix stale cl-streamer:get-listener-count call (needs server arg)

frontend-partials.lisp:
- Route shuffle mounts to shuffle-now-playing
- Fix stale cl-streamer:get-listener-count (use pipeline-listener-count)

listener-stats.lisp:
- Poll shuffle mounts (/shuffle.mp3, /shuffle.aac) for listener stats

parenscript/stream-player.lisp:
- get-stream-config returns /shuffle.* mounts when channel is shuffle

parenscript/front-page.lisp, parenscript/player.lisp:
- Normalize now-playing polling to 15s (was 5s/10s, caused 429s)

Build verified, runtime tested: both pipelines play simultaneously,
channel selector switches streams correctly.
2026-03-08 13:32:51 +03:00
Glenn Thompson 3e6b496340 Eliminate *server* global: update dj-session + stream-harmony to protocol generics
dj-session.lisp:
- Replace pipeline-harmony-server + harmony:*server* binding with
  pipeline-play-voice / pipeline-stop-voice protocol generics
- Replace volume-ramp with pipeline-volume-ramp
- Replace read-audio-metadata / format-display-title with
  pipeline-read-metadata / pipeline-format-title
- Replace update-all-mounts-metadata with pipeline-update-metadata
- pipeline-stop-all-voices now a protocol generic (was defun)

stream-harmony.lisp:
- Replace cl-streamer:get-listener-count (global-dependent) with
  cl-streamer:pipeline-listener-count (pipeline-scoped)

Build verified.
2026-03-08 12:47:05 +03:00
Glenn Thompson 8d9d2b33b1 Phase 4: Extract cl-streamer to standalone repository
Replace in-tree cl-streamer/ with git submodule pointing to
glenneth1/cl-streamer (https://github.com/glenneth1/cl-streamer).

ASDF discovers cl-streamer via source-registry :tree scan — no
config changes needed. Submodule tracks master branch.

Build verified: all cl-streamer systems load from submodule.
2026-03-08 12:08:09 +03:00
Glenn Thompson 2e86dc4a88 Phase 3: Refactor stream-server.lisp to iolib
- Replace usocket with iolib for socket I/O
- iolib:make-socket with :connect :passive for listener
- iolib:accept-connection for client connections
- Add SO_KEEPALIVE for stale connection detection
- Add TCP_NODELAY for low-latency streaming
- Add SO_SNDTIMEO (30s) write timeout for stale client detection
- Handle iolib:socket-connection-reset-error and isys:ewouldblock
- Update cl-streamer.asd: usocket→iolib, drop chunga/trivial-gray-streams
- Fix now-playing poll interval 5s→15s to eliminate 429 rate limit errors

Runtime verified: audio streams, metadata displays, clients connect.
2026-03-08 11:33:55 +03:00
Glenn Thompson b7266e3ac2 Phase 2: Clean package boundaries — declarative make-pipeline DSL
- New make-pipeline function: single declarative call creates server,
  mounts, encoders, and pipeline wiring from an output spec
- Pipeline owns encoder lifecycle (pipeline-encoders slot, auto-cleanup)
- Pipeline owns server when it creates one (pipeline-owns-server-p)
- Hook system wired: pipeline-add-hook fires on track-change and
  playlist-change via pipeline-fire-hook
- stream-harmony.lisp slimmed: start is 1 make-pipeline + 2 hooks,
  stop is 1 pipeline-stop call (cleanup automatic)
- Removed global encoder variables from Asteroid glue layer
- Backward-compatible: dj-session.lisp unchanged, cl-streamer:*server*
  still set for legacy callers

Runtime verified: audio streams, metadata displays, crossfades work.
2026-03-08 11:23:40 +03:00
Glenn Thompson c79cebcfa7 Phase 1: Define CLOS protocol layer for cl-streamer
- New cl-streamer/protocol.lisp with generic functions for server,
  pipeline, encoder, and hook protocols
- harmony-backend.lisp: convert defuns to defmethod on protocol generics,
  import/re-export protocol symbols, add hook system, backward-compat aliases
- encoder.lisp, aac-encoder.lisp: add encoder-encode/encoder-close methods
- package.lisp: export all protocol symbols
- cl-streamer.asd: add protocol.lisp to components

Runtime verified: audio streams, metadata displays, crossfades work.
2026-03-08 11:09:50 +03:00
Glenn Thompson 37a3b761db Fix API rate limits causing 429 errors on polling endpoints
- asteroid/stats/current: add explicit :limit 120 :timeout 60 (was default 60/60s)
- now-playing, now-playing-inline, now-playing-json: change from :limit 10 :timeout 1
  to :limit 30 :timeout 60 — the 1-second window was too aggressive and likely
  triggering r-simple-rate's negative-amount corruption bug

These endpoints are polled every 5-30s by the player frame, admin dashboard, and
popout player. With multiple tabs/frames sharing a session, the old limits were
easily exceeded, producing 429 responses that cascaded into audio error events.
2026-03-08 10:37:44 +03:00
Glenn Thompson 2aab912b5d Add cl-streamer standalone library refactor plan
Org document detailing the phased approach to extracting cl-streamer
into a standalone, reusable CL audio streaming library:

- Phase 1: Define CLOS protocol (generic functions, protocol classes)
- Phase 2: Clean package boundaries, declarative pipeline DSL
- Phase 3: Refactor stream-server to iolib (per Fade's recommendation)
- Phase 4: Optional separate repository extraction

Aligns with Fade's architecture vision: logically separated stream
daemon with well-defined protocol between application and streamer.
2026-03-07 18:58:21 +03:00
Glenn Thompson f2e60b5648 Fix stream-player.js reconnect loop (play/pause race, stall backoff)
- Keep *is-reconnecting* true until 'playing' event fires, preventing
  pause/stalled handlers from triggering new reconnect cycles mid-flight
- Add exponential backoff for stall retries (5s, 10s, 20s... max 60s)
- Give up auto-reconnect after 10 stall attempts, show manual retry
- Add *stall-count* tracking, reset on successful playback
- Add *user-paused* guard to muted-tab pause handler
- Increase play() delay from 200ms to 500ms after load() for reliability

Fixes: AbortError from play()/pause() race, 429 Too Many Requests from
aggressive reconnect hammering, infinite reconnect loop on muted tabs.
2026-03-07 10:34:43 +03:00
Glenn Thompson bcfda2ebb6 int16 pack encoding + fix playlist resume across schedule changes
- Replace default float packer with int16 packer (mixed:make-packer :encoding :int16)
  cl-mixed now handles float→s16 conversion in optimized C code instead of
  per-sample Lisp loop. Halves pack buffer memory (2 vs 4 bytes/sample).
- Remove float-to-s16 helper (no longer needed)
- Fix resume-from-saved-state: when saved playlist differs from currently
  scheduled playlist, use the scheduled one from the beginning instead of
  continuing the old playlist. Prevents stale playlist playing after restart.
2026-03-06 18:56:10 +03:00
Glenn Thompson df9d939a2f Revert int16 pack encoding (caused static), keep float-to-s16 drain
Setting (mixed:encoding pack) :int16 after server creation did not
change the pack's internal buffer format — data was still written as
float but read as int16, producing garbage audio.

Added TODO comment to investigate correct API for setting pack encoding
at creation time. The float→s16 conversion in Lisp works correctly.
2026-03-06 11:30:53 +03:00
Glenn Thompson f594daabf8 Fix DJ Console: 400 POST error, library search SQL, auto-playlist pause, now-playing override
- Fix api-post: skip Content-Type header on empty-body POSTs (Hunchentoot 400)
- Fix api-post/api-get: unwrap Radiance data wrapper, add try/catch + console logging
- Fix search-library-tracks: use raw SQL with parameterized ILIKE (S-SQL :offset broken)
- Fix search-library-tracks: quote file-path column name for Radiance hyphenated columns
- Add pipeline-stop-all-voices: immediately silence all Harmony voices on mixer
- Fix pause-auto-playlist: clear queue + skip + stop all voices (no more overlap)
- Override get-now-playing-stats during DJ session to show active deck info
2026-03-06 08:06:02 +03:00
Glenn Thompson e712009d79 Implement DJ Console Phase 1 — dual-deck library mixing
New files:
- dj-session.lisp: DJ session state, deck management, crossfader
  (constant-power curve), auto-playlist pause/resume, ICY metadata
  auto-detect, library search, watchdog timer
- parenscript/dj-console.lisp: UI polling (500ms), deck controls,
  crossfader, library search with load-to-deck, session management
- template/dj-console.ctml: Dark hacker-themed dual-deck interface
  with progress bars, transport controls, crossfader slider, metadata
  override, and library search table

Modified files:
- asteroid.lisp: 14 DJ API endpoints (session, deck, crossfader,
  library search), define-page dj-console, dj-console.js serving
- asteroid.asd: Add dj-session and dj-console components
- cl-streamer/harmony-backend.lisp: Export update-all-mounts-metadata,
  volume-ramp, pipeline-harmony-server for DJ deck control
- navbar-admin.ctml: DJ Console link (role-gated to :dj/:admin)

API endpoints all require :dj role. Session lifecycle:
  GO LIVE -> pause auto-playlist -> mix -> END SESSION -> resume

External audio input stubbed for Phase 2.
2026-03-05 21:22:09 +03:00
Glenn Thompson 3ddd86f8ab Add DJ Console design document
Comprehensive design doc for the live DJ mixing feature:
- Dual-deck library mixing with crossfader (constant-power curve)
- External audio input: local sound card (ALSA/Pulse/JACK) and
  network audio (Icecast source protocol for remote DJs)
- Session lifecycle with auto-playlist pause/resume and watchdog
- API endpoints, backend classes, frontend layout
- Phased implementation plan
- Open questions for team discussion
2026-03-05 20:40:42 +03:00
Glenn Thompson ef3e1eab47 Rewrite README.org for current Radiance/Harmony/CL-Streamer architecture
- Document CL-Streamer as our own in-process streaming server
- Remove Icecast/Liquidsoap sections (no longer used)
- Add architecture diagram showing single-process audio pipeline
- Document playlist scheduling, playback state persistence, resume
- Add FDK-AAC shim build instructions
- Update dependencies (Harmony, cl-mixed, CFFI, LAME, FDK-AAC, etc.)
- Add resource usage stats
- Update troubleshooting for common SBCL pathname issues
- Docker now only used for PostgreSQL
2026-03-05 20:22:52 +03:00
Glenn Thompson 770e565027 Fix broken file paths in all 5 scheduler playlists
- underworld-and-friends.m3u: Fix Drexciya (Bubble Chamber mp3, Aqua Worm Hole mp3),
  Model 500 (correct filename 08-model_500-digital_solutions.flac)
- afternoon-orbit.m3u: Fix BoC (dash + mp3), The Orb (mp3 + track numbers),
  Drexciya, Model 500, Vector Lovers (track 11 mp3), Underworld STITI (subdir)
- midnight-ambient.m3u: Fix Bark Psychosis (mp3), Tape Loop Orchestra (NBSP in filename)
- morning-drift.m3u: Fix BoC Kid for Today (dash + mp3)
- evening-descent.m3u: Fix Johann Johannsson (capitalization),
  Tape Loop Orchestra (disc subdir), Brian Eno (curly quote in filename)
2026-03-05 20:13:35 +03:00
Glenn Thompson f39abeb8f8 Fix playlist resume and SIMPLE-ARRAY pathname errors
- Add *resumed-from-saved-state* flag to prevent scheduler's db:connected
  trigger from overwriting resumed playlist position with full playlist
- Use sb-ext:parse-native-namestring in play-file to prevent SBCL from
  interpreting brackets in directory names (e.g. [WEB FLAC]) as wildcard
  patterns, which caused non-simple-string pathname components that broke
  cl-flac's CFFI calls
2026-03-05 18:21:32 +03:00
Glenn Thompson 1807e58971 Remove Icecast/Liquidsoap, migrate fully to Harmony/CL-Streamer
- Delete all Icecast/Liquidsoap config files, Dockerfiles, and .liq scripts
- Remove icecast/liquidsoap services from docker-compose.yml (keep postgres)
- Remove liquidsoap-command, parse-liquidsoap-metadata, format-remaining-time
- Remove liquidsoap-command-succeeded-p, liquidsoap-reload-and-skip
- Remove icecast-now-playing, check-icecast-status, check-liquidsoap-status
- Remove icecast XML polling from listener-stats.lisp
- Replace asteroid/liquidsoap/* APIs with asteroid/stream/* APIs
- Simplify all if-harmony-else-liquidsoap branches to Harmony-only
- Update admin template: single Stream Status card, pipeline controls
- Update about.ctml credits: Harmony + CL-Mixed replace Icecast + Liquidsoap
- Update status.ctml server name to CL-Streamer / Harmony
- Update parenscript/admin.lisp: stream-* functions, new API endpoints
- Export pipeline-running-p from cl-streamer/harmony package
2026-03-05 17:24:12 +03:00
Glenn Thompson 47e6c5da46 Fix scheduler timezone and taglib type errors
- cl-cron uses local time, not UTC: add utc-hour-to-local-hour
  conversion so schedule hours fire at correct UTC times
  (e.g. 12:00 UTC now fires at 15:00 local on UTC+3)

- Wrap each taglib field read in safe-tag with per-field error
  handling so a type error in one field (e.g. album with non-simple
  string) doesn't crash play-file or skip the track

- Use (coerce ... 'simple-string) instead of copy-seq for
  guaranteed simple-string output from ensure-simple-string
2026-03-05 15:16:21 +03:00
Glenn Thompson 9ae7546466 Fix playlist stalls, FLAC/taglib errors, and playback state resume
- Prevent play-list thread death on scheduler playlist change:
  drain-queue-into-remaining drains full scheduler queue at once,
  updates loop-queue reference so repeat replays correct playlist,
  top-level handler-case prevents thread from dying silently

- Fix taglib SIMPLE-ARRAY CHARACTER type errors:
  ensure-simple-string coerces metadata strings and trims whitespace

- Fix FLAC::FILE slot unbound errors:
  retry once with 200ms delay for transient init failures

- Improve playback state persistence:
  save playlist path alongside track file so restart loads the
  correct playlist instead of always falling back to stream-queue.m3u

- Startup now uses resume-from-saved-state to resolve saved playlist
  and track position, falls back to stream-queue.m3u only if no state
2026-03-05 13:25:15 +03:00
Glenn Thompson 16da880822 Fix playlist repeat: loop playlist when tracks exhaust, scheduler takes priority
- Enable loop-queue in play-list: repeats current playlist when tracks run out
- next-entry checks queue first so scheduler-queued playlists override repeat cycle
- Prevents silent stream / client reconnect loops between scheduled playlist changes
2026-03-04 10:20:57 +03:00
Glenn Thompson d66d4fe46c Crossfade timing, scheduler fix, playback resume, auth noise cleanup
- Delay metadata/track-change notification by 1s after crossfade completes
- Log 'Loading next:' instead of 'Now playing:' during crossfade prep
- Add diagnostic logging: track duration check, crossfade trigger time
- harmony-load-playlist defaults to skip=nil: scheduler queues tracks
  without interrupting current playback
- Save current track to .playback-state.lisp on each track change,
  resume from saved position on restart
- Replace ~50 format-t debug statements in auth with log:info/log:warn
- Remove password hash logging for security
- Add .playback-state.lisp to .gitignore
2026-03-04 00:23:25 +03:00
Glenn Thompson 6e23efe1e4 Fix listener count and recently-played rendering
- Use total listener count across all mounts instead of per-mount
  (asteroid.lisp icecast-status API, stream-harmony.lisp now-playing)
- Fix recently-played.lisp: equal -> = for ParenScript string comparison
- Remove non-existent asteroid-shuffle.mp3 mount from recently-played JS
- Map all mount references to existing asteroid.mp3/asteroid.aac
2026-03-03 23:25:23 +03:00
Glenn Thompson fd1bc504a5 cl-streamer integration fixes: CORS, reconnect, stream config
Server-side fixes (stream-server.lisp):
- Add CORS preflight (OPTIONS) request handler for browser crossorigin audio
- AAC clients start from current buffer position instead of burst to avoid
  ADTS frame alignment issues that caused browser decode errors
- Upgrade client stream error logging from debug to warn for diagnostics
- Add send-cors-preflight function with proper Access-Control headers

Frontend fixes (stream-player.lisp):
- Rewrite reconnect-stream to reuse existing audio element instead of
  creating a new one, preserving browser user gesture context and preventing
  NotAllowedError on autoplay after reconnect
- Unify stream config: both curated and shuffle channels use same mount
  points (asteroid.mp3/asteroid.aac) since cl-streamer has a single pipeline
- Remove non-existent /asteroid-shuffle.mp3 mount reference that caused
  404s and broken pipe cascade when switching to shuffle channel
- Map :low quality to same MP3 mount (asteroid-low.mp3 not yet available)

Note: Channel selector preserved for future multi-stream support.
Recently-played API works correctly; frontend rendering to investigate separately.
2026-03-03 23:17:01 +03:00
Glenn Thompson 77458467c4 Fix integration: CORS, auto-start, mount names, Icecast bypass
CORS fix (icy-protocol.lisp):
- Add Access-Control-Allow-Origin: * to stream response headers
- Browser audio player can now connect cross-origin (port 8080 -> 8000)

Auto-start (asteroid.lisp -main):
- Start cl-streamer pipeline automatically on boot
- Load stream-queue.m3u and begin playback immediately
- Wrapped in handler-case so streaming failure doesn't block web server

Mount names (stream-harmony.lisp):
- Renamed /stream.mp3 -> /asteroid.mp3, /stream.aac -> /asteroid.aac
- Matches existing frontend URLs, zero template changes needed

Icecast bypass (asteroid.lisp, listener-stats.lisp):
- Front page uses get-now-playing-stats instead of icecast-now-playing
- check-icecast-status returns cl-streamer status when pipeline is active
- check-liquidsoap-status returns N/A when using cl-streamer
- asteroid/icecast-status API returns cl-streamer data directly
- poll-and-store-stats uses cl-streamer listener counts directly
- Eliminates hanging HTTP requests to port 8000 for Icecast XML

Tested: full browser streaming working end-to-end
2026-03-03 22:29:21 +03:00
Glenn Thompson dad1418bf8 Integrate cl-streamer into Asteroid Radio (replaces Icecast + Liquidsoap)
New files:
- stream-harmony.lisp: Bridge between cl-streamer pipeline and Asteroid app
  - start-harmony-streaming / stop-harmony-streaming lifecycle
  - on-harmony-track-change callback: feeds recently-played, DB track lookup
  - harmony-now-playing: returns same alist format as icecast-now-playing
  - harmony-load-playlist: loads M3U, converts Docker paths, feeds queue
  - harmony-skip-track / harmony-get-status

Pipeline control (harmony-backend.lisp):
- Add pipeline-current-track, pipeline-on-track-change callback
- Add pipeline-skip, pipeline-queue-files, pipeline-get-queue, pipeline-clear-queue
- play-list now supports skip flag, queue consumption, loop-queue mode
- notify-track-change fires callback after crossfade completes

Graceful fallback - all touch points check *harmony-pipeline*:
- frontend-partials.lisp: now-playing endpoints try Harmony first, fall back to Icecast
- asteroid.lisp: admin APIs (status/skip/reload/restart) try Harmony first
- playlist-scheduler.lisp: load-scheduled-playlist tries Harmony first
- asteroid.asd: added cl-streamer subsystem dependencies

Docker scripts updated:
- start.sh / stop.sh: only start/stop postgres (cl-streamer replaces streaming)
2026-03-03 21:27:29 +03:00
Glenn Thompson edf9326007 Taglib metadata reading, crossfade metadata timing fix
- Add taglib dependency to cl-streamer/harmony system
- Add read-audio-metadata: reads artist/title/album from FLAC/MP3 tags
- Add format-display-title: builds 'Artist - Title' from tags, falls back to filename
- Add update-all-mounts-metadata: updates ICY metadata on all mount points
- Defer metadata update during crossfade until fade completes (listeners hear correct track)
- Fix play-list wait loop: was nested inside crossfade conditional, first track never waited
- Remove filename-derived :title from test playlist (taglib reads real tags now)
2026-03-03 21:10:44 +03:00
Glenn Thompson 2649a8169a AAC encoding fixes, crossfade, buffer tuning, README rewrite
- Fix FDK-AAC C shim: use proper OUT_BITSTREAM_DATA=3 constant and INT types
- Add frame-aligned PCM accumulation buffer to AAC encoder for clean output
- Add fdkaac_encode and fdkaac_close to C shim (all FDK-AAC calls in C)
- Implement crossfade between tracks (3s overlap, 2s fade-in/out)
- Tune buffer: 90% drain sleep for encoding headroom, 64KB burst-on-connect
- Add FFI bindings for new shim functions
- Rewrite README.org with full architecture docs and integration plan
2026-03-03 20:20:43 +03:00
Glenn Thompson fcda723577 feat: Broadcast buffer, sequential playlist, ICY metadata
- Broadcast buffer: single-producer multi-consumer ring buffer with
  per-client read cursors. 32KB burst-on-connect for fast playback.
  Never blocks producer (overwrites old data for slow clients).
- Sequential playlist: play-list runs tracks one at a time using
  Harmony's on-end callback + condition variable for completion.
- ICY metadata: set-now-playing called on each track change.
- Fixed string vs pathname bug in harmony:play (etypecase mismatch).
- Debug logging for client disconnect diagnosis.

Verified: browser plays shuffled FLAC playlist via 128kbps MP3 stream.
2026-03-03 18:15:31 +03:00
Glenn Thompson a9e8276e9a feat: End-to-end streaming working! Custom streaming-drain + fixes
Major changes:
- streaming-drain: Custom drain that captures PCM from Harmony's pack
  buffer (raw IEEE 754 floats in unsigned-byte-8 array), converts to
  signed-16 PCM via CFFI, encodes to MP3 via LAME, and writes to
  stream server's ring buffer
- Fixed ring buffer deadlock: buffer-read/buffer-write held lock then
  called buffer-available which tried to acquire same lock. Created
  internal %buffer-available/%buffer-free-space without locking.
- Fixed ring buffer zero-length guard for unbound variable in finally
- Fixed sleep duration in drain: was dividing raw byte count by
  samplerate, now correctly converts to frames first
- Added flexi-streams wrapper for bivalent HTTP socket I/O
- Exported all public API symbols from cl-streamer package
- Added test-stream.lisp end-to-end test script

Verified: Amon Tobin FLAC -> 128kbps MP3 stream at localhost:8000
  file reports: MPEG ADTS, layer III, v1, 128kbps, 44.1kHz, JntStereo
2026-03-03 17:46:55 +03:00
Glenn Thompson 9d5166c562 feat: Add Harmony backend with dummy drain for streaming pipeline
- harmony-backend.lisp: Audio pipeline connecting Harmony to encoders
- Uses :dummy drain to capture PCM without audio output
- Reads from packer bip-buffer via request-read/finish-read API
- Encodes PCM to MP3 via LAME and feeds to stream server

All three systems compile cleanly:
  cl-streamer, cl-streamer/encoder, cl-streamer/harmony
2026-03-03 16:56:24 +03:00
Glenn Thompson 6c15441a08 feat: Add MP3 and AAC encoder FFI bindings
- LAME FFI (lame-ffi.lisp) for MP3 encoding
- FDK-AAC FFI (fdkaac-ffi.lisp) for AAC encoding
- High-level encoder wrappers (encoder.lisp, aac-encoder.lisp)
- Fix compilation warnings in buffer.lisp and icy-protocol.lisp

Both encoders tested and loading successfully:
- LAME version: 3.101
- FDK-AAC: libfdk-aac.so.2

Next steps: Harmony integration for audio pipeline
2026-03-03 16:46:29 +03:00
Glenn Thompson e1be88a54a feat: Initial cl-streamer skeleton for CL-native streaming
- Architecture document outlining Icecast/Liquidsoap replacement
- Core streaming server with ICY metadata protocol support
- Thread-safe ring buffer for audio data
- Mount point abstraction with metadata updates
- Multi-client connection handling

This is experimental groundwork for integrating with Harmony/cl-mixed
and playlisp/parsector for a pure CL streaming solution.
2026-03-03 16:30:49 +03:00
Luis Pereira 5f9dc80ac8 fix: add parenscript utils file 2026-03-02 17:51:26 -05:00
Luis Pereira 8700724f81 feat: add media session API on now-playing update 2026-03-02 17:51:26 -05:00
Luis Pereira 61266647a9 feat: frontpage now playing default fill 2026-03-02 17:51:26 -05:00
Luis Pereira 6fd8071a05 fix: add favourite count to icecast stats 2026-03-02 17:51:26 -05:00