
SUB/WAVE - self-hosted internet radio with an AI DJ, runs on your Navidrome library
Been building this for a few weeks and finally have it stable enough to share. It's a personal radio station that runs on one Linux box and broadcasts a single Icecast stream, everyone tuning in hears the same track at the same moment. No per-listener shuffle, no skip button. Properly like radio.
The DJ is an LLM. It picks the next track from your library, writes the links between songs, reads time/weather, and handles listener song requests in plain language. You can swap the LLM provider at runtime from the admin UI (Ollama, Anthropic, OpenAI, Google, OpenRouter) — same for the voice engine (Piper/Kokoro on-device, or ElevenLabs/OpenAI cloud).
The stack:
- Icecast — single
/stream.mp3mount - Liquidsoap — crossfades, voice ducking, jingles, limiter
- Node controller — the DJ brain, scheduler, TTS, HTTP API
- Next.js PWA — player + admin console, installs to home screen with lock-screen controls
- Navidrome (or anything Subsonic-compatible — Airsonic, Gonic, Funkwhale) for the music
The controller and Liquidsoap talk only through files in a shared state/ directory — no socket, no message queue. Unix-y and easy to debug.
Admin console has: a 24×7 schedule grid you paint shows onto, up to 12 DJ personas with their own voice and talk frequency, toggleable "skills" (weather check, news headline, station ID), and a mood tagger that walks your library and classifies tracks.
Live demo (currently spinning Punjabi music): https://radio.klair.co/listen
Landing: https://radio.klair.co
Repo: https://github.com/perminder-klair/subwave
MIT licensed. There's a npx subwave setup CLI for first boot, and DEPLOY.md covers the production walkthrough (Cloudflare + Caddy + Docker compose).
Happy to answer questions on the architecture or any of the trade-offs, the no-skip thing in particular was the call that shaped most of the design.