Open-source project
perminder-klair/subwave avatar
perminder-klair/subwave

SUB/WAVE: a self-hosted internet radio station with an AI DJ

Personal internet radio: Agentic AI DJ

1,359 stars121 forksTypeScriptMIT

At a glance

What is it?
SUB/WAVE turns a Navidrome library into a single Icecast broadcast where an LLM picks tracks and talks between them. It is a radio station, not a playlist, and the setup cost is real.
Who is it for?
Adopt SUB/WAVE if you already run Navidrome, have a machine that can hold an Icecast stream and an LLM, and want one shared broadcast rather than per-listener playlists. Do not adopt it if you need a skip button, per-user queues, or if you have no model to point it at: the hosted DJ Brain starts at £5/month and is the intended answer for that case.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem SUB/WAVE solves: one broadcast, not a queue per listener

Most self-hosted music setups optimise for the individual. Navidrome, Plex and Jellyfin all assume each listener picks their own track and can skip. SUB/WAVE inverts that. The README states it plainly: "It's radio, not a playlist. No per-listener shuffle, no skip button, no 'up next for you.'" Every listener hears the same Icecast stream at the same moment.

That constraint is the product. It makes the DJ possible, because the DJ only has to decide one thing at a time for one audience. It also makes the station a social object: two people listening at once are genuinely listening together, and a plain-language request like "play something more upbeat" changes what everyone hears.

The audience is narrow on purpose. You need an existing music library served by Navidrome over the Subsonic API, because SUB/WAVE has no external catalogue. You need a machine that can run Docker Compose services plus an LLM endpoint. And you need to want a station rather than a player.

How the DJ, the library and the stream fit together

The repository layout shows the split. A controller service holds the logic and the state. A broadcast service handles the audio. Caddy is the only service bound to a host port, with Cloudflare terminating TLS in front of it, and the compose file gates the edge on controller and broadcast health to avoid cold-boot 502s. A web service serves the Next.js player and admin console. Liquidsoap sits in its own top-level directory for the stream itself.

Tracks come from Navidrome over the Subsonic API. The DJ tags what it finds: the Library Observatory at /observatory is described as a map of every track the DJ has tagged, placed by genre and lit by energy, with a per-track dossier showing BPM, key, mood, embeddings and nearest neighbours. Those embeddings are what make a vague request resolvable.

Speech is separate from music. Six TTS engines are listed: Piper and Kokoro run in-process for fast local speech, a tts-heavy sidecar adds Chatterbox and PocketTTS, and Cloud (OpenAI or ElevenLabs) and a Remote engine cover the rest. You can pick a different engine per kind of speech, so a station ident and a weather read can sound different.

The LLM provider is swappable from the admin UI with no redeploy: Ollama, Anthropic, OpenAI, Google, DeepSeek, OpenRouter, Requesty, Vercel AI Gateway, or any OpenAI-compatible server. A daily token budget can cap hosted-model spend, and past the cap the README says the music keeps playing without the chatter. That is the right failure mode for a radio station, and it is worth knowing before you pick a provider.

Installing SUB/WAVE and getting a first stream up

The repository ships an install.sh at the top level, and package.json exposes a CLI through the subwave binary. Node 20 or later is required by the engines field. The documented entry point for a fresh install is the installer script.

bash
./install.sh

The stack is orchestrated by Docker Compose. The production compose file is image-first: services pull baked GHCR images, and the build blocks exist so a checkout can rebuild locally. Only Caddy binds a host port.

bash
docker compose up -d

Just three environment variables are required to boot, according to .env.example. Everything else is collected by the first-run wizard, which writes to state/settings.json as the runtime source of truth.

bash
ADMIN_USER=admin
ADMIN_PASS=            # generate one: openssl rand -hex 16
SITE_URL=https://radio.example.com

Once the stack is up, the wizard lives at /onboarding on your host. That is where Navidrome, the LLM provider, the TTS engine and the DJ persona get configured. The .env.example notes that the optional overrides are kept for 12-factor, CI and GitOps style deploys where environment beats the UI, so you can skip the wizard for those fields if you prefer.

A development path also exists. npm run dev:docker brings up docker-compose.dev.yml, and npm run rebuild recreates it with a build. npm run logs follows the output. For the web front end alone, npm run dev:web runs the Next.js dev server.

One operational detail from the compose file is easy to miss. State persists in a state directory that is a bind mount, so docker compose down -v will not touch it, but the comment warns to keep it clear of git clean -dffx. The stem cache can be relocated with STEMS_DIR, and the file is explicit that moving it does not migrate what is already cached: you have to move the old contents across yourself or the old copy sits there unreferenced.

Where SUB/WAVE breaks down

The single-broadcast model is the main limitation, and it is not a bug you can configure away. If two listeners want different things, one of them loses. There is no skip. A bad DJ pick is heard by everyone until it ends.

Resource cost is the second constraint. You are running Caddy, a controller, a broadcast service, a web server, and possibly a tts-heavy sidecar, plus an LLM. Piper and Kokoro run in-process, which the README frames as fast local speech, but the heavier engines are a separate profile you have to start deliberately with docker compose --profile tts-heavy up -d. If you point the DJ at a hosted model without a token budget, cost scales with how much the DJ talks, and the admin Stats page exists precisely because that usage is worth watching.

The library dependency is absolute. SUB/WAVE pulls from Navidrome over the Subsonic API and has no external catalogue. If your music is not in a Subsonic-compatible server, this is the wrong tool and no amount of configuration fixes it.

Finally, the README does not document rollback for the stem cache relocation, and it states outright that moving STEMS_DIR does not migrate existing cache contents. Treat that path change as a manual operation, not a setting toggle.

SUB/WAVE against AzuraCast and plain Navidrome

The obvious comparison is AzuraCast, which also produces a shared Icecast stream from a self-hosted library, and which has been the default answer to this problem for years. The difference is who chooses the music. AzuraCast schedules playlists: you build rotations, assign them to hours, and the automation plays what the rotation says. It is deterministic and predictable. SUB/WAVE puts a language model in the scheduler seat instead, so the sequence is generated rather than authored, and the station talks between tracks with idents, time checks and weather. AzuraCast gives you a programme log you can reason about in advance; SUB/WAVE gives you a DJ that responds to a request typed in plain language.

Against Navidrome alone, the difference is even sharper. Navidrome is a library server with per-user playback, and SUB/WAVE consumes it as a source rather than replacing it. You keep Navidrome, point SUB/WAVE at it, and get a broadcast on top. If what you actually want is on-demand listening with skips, Navidrome already does that and SUB/WAVE would only add a second, less controllable way to hear the same files.

The honest framing: choose AzuraCast when you want to author the schedule and predict the output, and SUB/WAVE when you want the schedule generated and are willing to accept the variance that comes with it.

Licence, maintenance and what an upgrade actually costs

SUB/WAVE is MIT licensed, and package.json confirms the license field. The README is explicit that the software stays complete and free even though the hosted DJ Brain and the hosted station exist to fund the project. MIT means you can run, modify and redistribute it; it does not grant you rights to the hosted services, and the README does not describe the terms of those. That is a question for the hosted offering, not the repository.

The repository is not archived, and the last push was on 2026-09-17. Releases are frequent: v1.14.0 on 2026-09-09, v1.15.0 on 2026-09-10, v1.16.0 on 2026-09-14. Release Please config files sit at the top level, so versioning and changelog generation are automated, and CHANGELOG.md is maintained in the repository.

Upgrade cost is mostly image pulls. The production compose file is image-first and pins images to SUBWAVE_VERSION with a latest fallback, so you control which version you run. State lives outside the containers in a bind mount, which means a container replacement does not wipe your station settings or library tags. The real upgrade risk is the settings file: state/settings.json is described as the runtime source of truth, so a version that changes the settings schema would touch it. The repository does not document a migration path for it, so back that directory up before bumping SUBWAVE_VERSION.

Editorial conclusion

Adopt SUB/WAVE if you already run Navidrome, have a machine that can hold an Icecast stream and an LLM, and want one shared broadcast rather than per-listener playlists. Do not adopt it if you need a skip button, per-user queues, or if you have no model to point it at: the hosted DJ Brain starts at £5/month and is the intended answer for that case. Before committing, verify three things in your own environment: that the controller boots with ADMIN_USER and ADMIN_PASS set under NODE_ENV=production, that your chosen LLM provider is reachable from the container, and that your state directory sits somewhere git clean -dffx cannot reach.

Frequently asked questions

What is SUB/WAVE?

It is a personal internet radio station. It serves one shared Icecast stream so every listener hears the same broadcast at the same time, and an AI DJ picks the tracks and talks between them with station idents, time checks and weather.

Where does SUB/WAVE get its music?

From your own library, pulled from Navidrome over the Subsonic API. The README states there is no external catalogue, so a Subsonic-compatible server is a hard requirement.

Can listeners skip tracks on SUB/WAVE?

No. The README describes it as radio rather than a playlist, with no per-listener shuffle and no skip button. Listeners can make plain-language requests, and the DJ works out what was meant and slots it in for everyone.

Do I need to run my own language model for SUB/WAVE?

Not necessarily. The LLM provider is swappable between Ollama, Anthropic, OpenAI, Google, DeepSeek, OpenRouter, Requesty, Vercel AI Gateway or any OpenAI-compatible server, and the hosted SUB/WAVE DJ Brain is offered as a metered alternative starting at £5/month.

Official sources

  1. License: MIT
  2. perminder-klair/subwave on GitHub
  3. Project website
  4. README
  5. Releases
Community notes

Community notes