Skip to content

Latest commit

Β 

History

1,350 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

GenWave β€” Broadcast Audio Streaming Service

CI Release NuGet License Demo on-air

A self-hosted internet radio station: one shared broadcast stream, equal-power crossfades, and loudness level-matching so quiet and loud tracks play back at a consistent volume. It never emits dead air. Deployed entirely via Docker.

No hand-built audio engine. A C# / .NET 10 control plane orchestrates Liquidsoap (real-time mix, crossfade, encode) and Icecast (fan-out). Selection is criteria-based β€” the feeder pulls through INextItemProvider over a media library catalog; there is no ordered playlist table.

This is GenWave Home, the AGPL edition β€” see License.

🎧 Hear it live: demo.genwaveradio.com β€” the public demo station, running the reference appliance topology: hear & see what's on the air, tune in, even request a song!

πŸŽ™οΈ On the air fast: download.genwaveradio.com launches the setup wizard β€” no sudo, anywhere in the path. Measured cold start: on air in 1:03 on a Raspberry Pi 5 (4 GB, 9,000-track NFS library) β€” 1:36 to the first note. 1:10 on a fresh VM (CCX23 same as demo.genwaveradio.com). (2026-08-19, wizard-measured.)

Why GenWave

Twenty-plus years ago, in my first job as a software engineer, we pulled a lot of all-nighters, fueled by music that IT graciously let us keep on one of their servers (the Exchange server, if memory serves). We'd bring in CDs, IT would rip them, and when the last non-engineer left for the night, someone would fire up a playlist and start the stream.

It was great, until it wasn't. The problem with playlists is that they're dumb: same music, same order, every time. Being engineers, we solved it! Sort of. I wrote a playlist generator that jumbled the order on every run. Problem solved! Except now we were hearing B-sides and artists nobody recognized, with no good way to find out what was playing. It felt more like real radio, and it left us with a thought that stuck, with me at least: wouldn't it be cool if we had DJs to announce the music? There was no way to build that back then, so we coerced our QA lead into recording a few sound bites, played one every X tracks, and christened the result FLAP Radio (because reasons).

The itch never went away. Over the years I built a home version of FLAP Radio. Still, honestly, a playlist randomizer, and I was never truly happy with it. Then, a couple of years ago, LLMs and TTS started making a serious splash, and the itch came back in earnest: the technology had finally caught up with the idea. This time GenWave was born for real: a station that never goes silent, knows what it's playing, and has a DJ who tells you about it.

Itch scratched. πŸ“»

Quickstart

You need Docker (with Compose v2.24+) and a music library of .mp3/.flac files β€” see HARDWARE.md for what GenWave runs on, how to size a box, and (its "LLM model floor" section) which DJ model to run β€” every model benched from 3B to 12B passed the truth gate, so size the model to your box, not to your trust.

Published images are multi-arch (amd64 + arm64), and a 4GB Raspberry Pi 5 is a verified deployment β€” playout plus on-box TTS, no LLM. See HARDWARE.md's Raspberry Pi section for the prep that topology needs.

./setup.sh          # the first-run wizard (v5.3.0): four questions β€” prebuilt images or build
                    # from source, where the music lives, which topology, admin on/off β€” then
                    # it generates every secret, writes .env atomically, and launches

Re-running ./setup.sh on a box that already has a .env never rewrites it: it verifies the existing install against the machine (drift report), and --repair fixes what it can. The same wizard is what download.genwaveradio.com fetches.

Or, by hand β€” the dev flow the wizard wraps:

cp .env.example .env
# edit .env: set POSTGRES_PASSWORD, LIBRARY_DB_PASSWORD, STATION_DB_PASSWORD,
#            ICECAST_SOURCE_PASSWORD, ICECAST_ADMIN_PASSWORD,
#            MEDIA_DIR (absolute path to your library),
#            and ADMIN_PASSWORD (admin UI login; empty = the admin plane is
#            locked entirely β€” fail-closed, the stream still runs)

./build.sh
./launch.sh

Both scripts preflight the machine before touching anything (Docker running, compose plugin, .NET SDK, .env secrets) and every failure exit says how to proceed; on the dev flow, a launch that fails part-way rolls the stack back down rather than leaving half of it running. --pinned deliberately does not roll back β€” whatever is still broadcasting keeps broadcasting, and the failure report says how to converge (see DEPLOYMENT.md). SKIP_PREFLIGHT=1 bypasses the checks on unusual setups.

Seven services start: db, icecast, engine, api, kokoro (TTS synthesizer), admin_ui (operator console β€” rides the admin compose profile, on by default via .env.example), and dockerproxy (a read-only, allowlisted docker-stats sidecar feeding the admin Health page β€” internal network only, no ports). Three more services ride opt-in compose profiles: piper, a CPU-only fallback TTS (fallback β€” off by default since v5.1.0's voice contract; see Resilience below), a Cloudflare tunnel with health/metrics observability (tunnel), and a Grafana Alloy log shipper (logging) β€” ./launch.sh --with fallback,logging,tunnel activates any of them; see DEPLOYMENT.md and observability/.

launch.sh has three other presets worth knowing: --pinned (run published GHCR images instead of building β€” the appliance/upgrade path), --piper-only (drop kokoro and the ollama pair for a 4GB-class box, run Piper as the station's primary TTS engine, and halve enrichment concurrency), and --dry-run (print the exact command plan, touch nothing). After a successful launch the file stack is recorded as COMPOSE_FILE in .env, so a plain docker compose down/ps/logs targets the same stack you launched.

  • Stream: http://localhost:8000/stream β€” open it in any audio player
  • Admin UI: http://localhost:3000 β€” log in with the password set in ADMIN_PASSWORD
  • API: http://localhost:8080 β€” anonymous hot path (GET /media/random, GET /media/{id}, GET /health) plus the cookie-auth admin surface under /api/* (and, on /api/announcements only, a scoped announce token for automations β€” see below)
  • Spectator page: http://localhost:8081 β€” the station's read-only public face (now playing, history, stats, an optional anonymous song-request line with free-text wishes plus genre/mood pickers, and β€” opt-in via the live Station:Thumbs:Enabled, off by default β€” listener thumbs on the current track). Off by default: flip the live Station:SpectatorMode setting to enable it; DEPLOYMENT.md covers the four operating modes and the public topology. Metadata-aware players also get per-track album art via ICY StreamUrl once Station:PublicBaseUrl is set.

On first boot the library scans MEDIA_DIR, enriches each file (loudness + cue + energy + BPM + tags, plus a high-confidence MusicBrainz release-year lookup when the tags carry none β€” disable-able live via Library:YearLookup:Enabled), and the feeder begins pulling ready tracks. Until the first tracks are ready, the engine plays the safe-rotation source β€” a curated library scope (Station:SafeScope:LibraryIds) pulled via GET /internal/safe-track. On a fresh deploy, a one-shot boot seed creates a safe library, renders a branded TTS announcement ("Please Stand By"), and points SafeScope at it β€” so drains air the announcement, not a random track; an operator-set SafeScope is never overwritten. If the scope resolves empty, mksafe emits silence as a logged degraded mode. The Orchestrator interleaves TTS patter (station IDs, lead-ins, back-announces β€” and, opt-in, top-of-hour time checks plus weather and this-day-in-history segments) with music once Kokoro is up. When an Llm:Endpoint is configured (Settings page β€” live, no restart), lead-ins and back-announces become LLM-authored copy, optionally in an operator-authored DJ persona's voice (Personas page) β€” or hire a ready-made DJ from the community Community Catalog (CC0 persona cards, browsed and adopted one-click from the Admin UI after a full-card review); with no LLM configured the template patter airs unchanged; Llm:ReasoningEffort (live, default none) is what makes thinking models β€” gemma4, qwen3, deepseek-r1, magistral β€” usable, and omit keeps a third-party OpenAI-compatible backend that rejects the field working. A weekly format clock (Schedule page β€” a drag-paint 7Γ—48 grid) decides who's on the air when, with audible DJ-to-DJ handoffs at the boundaries; since v3.4.0 the hours have names too: shows (a name, a tagline, and a flavor line that colors the DJ's patter) assign to whole runs in one click, sign-ons welcome you to the named show, top-of-hour idents brand it, dated specials shadow the weekly grid for one-off broadcasts, and the spectator card tells listeners what's on now and up next. Since v5.1.0 the DJs can even talk to each other: crosstalk β€” short two-voice banter exchanges written and rendered ahead of air, dropped in at a mid-show break seam, each aired exactly once. Opt-in per show via Crosstalk:Shows (empty = off; nothing changes on upgrade until you enable it). Station identity (STATION_NAME, voice, scope) defaults to GWAV 108.8 / af_heart / library 1 β€” override via env if needed.

Since v5.4.0 the station speaks for the house. Owner announcements (Announcements page, or POST /api/announcements): type a line and the on-air DJ works it into the next break in character β€” or reads it exactly as written (verbatim), which is also what happens whenever the in-character copy fails the truth gate, so your words always air. Announcements are a durable unit of content with a visible lifecycle (pending β†’ claimed β†’ aired / expired / declined), capped at 280 characters, six a minute, and a queue depth of 12, with duplicates collapsed; a dedicated reveal-once announce token lets automations submit without the admin cookie. A public station never carries the house's events: while Station:SpectatorMode is on, announcements are refused. The companion genwave-homeassistant integration (HACS, MIT) turns that into a genwave.announce service, a notify entity, a now-playing sensor, and a starter blueprint gallery β€” dinner bell, laundry done, morning ramp β€” so Home Assistant can have your DJ say "the laundry's done".

The station's look is themeable too (v3.0.0–v3.2.0). A theme is one JSON manifest β€” colour tokens for light and dark plus curated fonts β€” composed live into CSS for both the Admin UI and the spectator page; pick one via Settings or the switcher on either surface (a cookie-remembered visitor choice outranks the station default). The Community Catalog generalized from a persona-only shelf into a multi-kind one β€” personas, themes, shows (v3.4.0), Dean-curated font packs, and since v5.2.0 avatar packs and icon packs β€” each kind on its own shelf tab, all adopted through the same one-click review flow. Installed packs list on the Wardrobe page (fonts v3.1.0; avatars and icons v5.2.0), which is also where a pack is uninstalled β€” font packs refused while a saved theme still references one of their faces, avatar packs freely (worn faces are copies and survive); the theme editor (/editor, v3.2.0) mixes any theme's palette with a vendored-or-installed face and saves the remix as your own station theme.

And since v5.2.0 the station has faces (gh-#206, gh-#297, gh-#15). Every DJ can wear an avatar: install an avatar pack from the catalog and apply faces per persona (suggested matches offered, bulk-apply behind one confirm β€” nothing auto-writes), or upload your own image per DJ (server-side normalized to a clean 512Γ—512 PNG, metadata stripped structurally). Faces show on the admin Personas page, on the spectator page's DJ card, and β€” for metadata-aware players β€” as the stream artwork while that DJ talks, under a strict right face or no face rule: at show boundaries the station shows a placeholder rather than ever pairing a name with the wrong face (two-voice crosstalk credits the station, not one DJ). Icon packs swap the admin console's entire icon set live via Station:IconPack β€” a pack is one JSON document whose schema structurally cannot express scripts, links, or literal colors, rendered with per-name fallback to the built-in set so no pack ever breaks a newer page. And a single uploaded station image replaces the shipped logo everywhere it appears: stream art for idents, the spectator logo and favicon, and the authed admin tab icon β€” delete it and the shipped look returns byte-identically.

Since v5.5.0 the library tends itself. The Gardener runs five rot passes on a timer β€” dead files, near-duplicates, stale metadata, shelf dust (unaired past its days), and rows no schedule envelope admits β€” surfacing findings on a Gardener page an operator works through and dismisses, not a silent auto-fixer; since v5.5.1 the queue is worked through five badged kind tabs with server-side paging that never splits a duplicate cluster. A Gardener tile and a rotation-health block (playable / never-aired / aired-once counts) sit on the dashboard. Listeners can thumb the current track up or down from the spectator page (Station:Thumbs:Enabled β€” live, off by default) alongside the existing operator thumbs; both feed one decayed rotation nudge into persona selection, so what actually gets played shapes what plays next. Deep Cuts shows can pin a rotation predicate that favors the under-played, with a relax ladder so a thin catalog never starves the boundary. File actions β€” retag, rename, move, never delete β€” are the one Gardener capability that writes to your media at all: jailed to MediaRoot, a .gwbak backup behind every retag, and armed only when both knobs agree (Gardener:FileActions:Enabled and the compose.fileactions.yaml overlay that widens the api's media mount to :rw); see DEPLOYMENT.md.

Resilience & operator tools

The broadcast never depends on a sick dependency. LLM failure is a mode, not an error: consecutive failures walk the station Normal β†’ Soft (one real LLM attempt per cooldown window, template copy otherwise) β†’ Hard (zero LLM calls); background health probes plus a cooldown walk it back up, and an operator can pin any mode live (Llm:DegradationPin). The DJ is fact-checked before it speaks (v5.4.0's truth gate): every LLM-authored line is checked against the facts it was given and the station clock β€” an invented number, a wrong weekday, a "tonight" at noon β€” re-asked exactly once, and template-floored if the model lies twice. A DJ never speaks in someone else's voice (the v5.1.0 voice contract): if a DJ's own voice can't be produced, the break simply doesn't air β€” the music continues uninterrupted, and the Voice tile on the health surface tells you the engine is down versus the DJ having nothing to say. Voice failover is opt-in: enable the fallback compose profile to run the Piper sidecar and set Tts:Fallback:Endpoint (live, no restart) to accept a substitute voice instead of silence β€” the previous always-on behavior, now a choice (DEPLOYMENT.md has both halves). Every spoken line passes one normalization chokepoint (reasoning-block scrub, markdown strip, operator pronunciation corrections β€” editable with live preview under Settings β†’ TTS, e.g. MacLeod β†’ Muh-cloud). The Booth log page answers "what did the DJ do and say at 9:14" as a persistent narrative feed (track starts, patter, mode changes, 14-day retention), with an LLM call inspector tab showing the last ~50 calls (prompt, response, persona, timing, mode, and a typed cause β€” Timeout / OverLength / TruthGateReject / EmptyCompletion / … β€” in-memory, never persisted); a red LLM tile names its dominant cause and model instead of just being red. The Health page is a container-level view of the running stack β€” state, CPU, memory per service β€” fed by a read-only, endpoint-allowlisted docker-stats sidecar (the API never touches the docker socket). Station Imaging (the operator-authored always-airable segments: liners, station IDs, jingles, promos) is authored in the Admin UI and rendered through the same TTS + loudness pipeline as everything else. Dependency health probes debounce β€” a verdict flips unhealthy only after consecutive probe failures (a slow TTS render is not an outage), with interval, timeout, and threshold all live-editable settings. MusicBrainz lookups are throttled to 1 req/s with a version-stamped User-Agent, and misses are stamped so they're never re-asked.

Repository layout

.
β”œβ”€ compose.yaml            # core topology: db, icecast, engine, api, kokoro, dockerproxy
β”‚                          #   (+ profiles: admin_ui [admin, default-on], piper [fallback],
β”‚                          #      cloudflared [tunnel], alloy [logging])
β”œβ”€ .env.example            # secrets template β†’ copy to .env
β”œβ”€ engine/
β”‚  └─ genwave.liq          # Liquidsoap playout script
β”œβ”€ db/
β”‚  β”œβ”€ 01-library.sh        # library schema + library_svc role (canonical fresh install)
β”‚  └─ 02..41-*-migration.sh # idempotent in-place upgrades, one per shipped feature β€”
β”‚                          #   each header says what it adds; ./migrate.sh applies them all
β”œβ”€ icecast/
β”‚  β”œβ”€ Dockerfile           # self-owned Icecast2 image
β”‚  β”œβ”€ entrypoint.sh        # renders passwords from env, runs Icecast
β”‚  └─ icecast.xml.tmpl     # hardened single-mount config
β”œβ”€ admin-ui/               # Next.js (App Router) operator console (`:3000`)
β”œβ”€ observability/          # the observability contract: Alloy config, label conventions, Grafana dashboards as code (F78)
β”œβ”€ tools/
β”‚  β”œβ”€ find_smoke_candidates.cs   # picks a divergent-gain track pair for the smoke test
β”‚  β”œβ”€ smoke_test.sh              # manual pre-release regression gate (no human listening required)
β”‚  β”œβ”€ onair_gate.sh              # Β§0 on-air acceptance gate (live engine)
β”‚  β”œβ”€ test-pronunciation.sh      # hear how TTS says a name; iterate spellings, then add a speech correction (gh-#37)
β”‚  β”œβ”€ preflight.sh               # shared machine/env checks sourced by build.sh + launch.sh (gh-#19)
β”‚  β”œβ”€ check-compose-publish.sh   # CI guard: 0.0.0.0 host publishes allowed only for the front proxy (F67.1)
β”‚  β”œβ”€ check-compose-socket.sh    # CI guard: docker.sock read-only + alloy-only, every profile combo (F78.2)
β”‚  β”œβ”€ check-doc-drift.sh         # CI guard: DEPLOYMENT.md/HARDWARE.md values match the compose files (gh-#77)
β”‚  β”œβ”€ check-seam-index.sh        # CI guard: SEAMS.md matches a fresh generation byte-for-byte (F105.6)
β”‚  β”œβ”€ soak-check.sh              # the soak-runbook checkpoint script (HARDWARE.md)
β”‚  β”œβ”€ demo_hour_gate.sql         # the demo-hour acceptance query (booth log, one hour of air)
β”‚  β”œβ”€ IconPackAuthor/            # authoring tool for catalog icon packs (v5.2.0)
β”‚  └─ SeamIndexGenerator/        # writes SEAMS.md from the live DI registrations β€” never hand-edit the map
β”œβ”€ SEAMS.md                # generated seam index: port β†’ adapter β†’ binding site (see CONTRIBUTING before adding a seam)
└─ src/                    # C# solution (.NET 10)
   β”œβ”€ GenWave.Abstractions/  #   the SDK contract surface: selection, catalog read, events, TTS seams
   β”œβ”€ GenWave.Core/          #   domain + engine-facing abstractions; zero I/O
   β”œβ”€ GenWave.Context/       #   external-context providers (weather, this-day-in-history): one pipeline,
   β”‚                         #   fetch-once-per-slot, fact sanitizer β€” skip, never silence
   β”œβ”€ GenWave.MediaLibrary/  #   scan, enrich, catalog (Postgres)
   β”œβ”€ GenWave.Loudness/      #   Ffmpeg{Loudness,Cue,Energy}Analyzer + AubioBpmAnalyzer; shared by MediaLibrary + Tts
   β”œβ”€ GenWave.Tts/           #   Kokoro client, LLM copy writer (ISegmentCopyWriter), renderβ†’measureβ†’cache
   β”œβ”€ GenWave.Orchestration/ #   Orchestrator (INextItemProvider): music + TTS patter interleave
   └─ GenWave.Host/          #   composition root, API (controllers + minimal API), engine control, feeder,
                             #   theme composition (Theming/ β€” manifests β†’ served CSS, both surfaces)

Tests

# Core, Orchestration, Tts unit tests (no Docker needed):
dotnet test GenWave.sln --filter "Category!=Integration"

# Full suite including library + Kokoro integration tests (need Docker + ffmpeg):
dotnet test GenWave.sln

# Β§0 on-air acceptance gate (live engine required):
./tools/onair_gate.sh

# Admin UI (from admin-ui/): type-check (app, then the spec suite's own project), unit tests,
# production build β€” what CI runs:
npx tsc --noEmit && npm run typecheck:specs && npm test && npm run build

Seven test projects: Core.Tests, Context.Tests, Host.Tests, MediaLibrary.Tests, Orchestration.Tests, Tts.Tests, and Architecture.Tests β€” the last enforces the architecture laws (dependency direction, Postgres/HttpClient confinement, contract immutability, the Host graduation tripwire) as ordinary red-green tests; the laws themselves are summarized front-and-center in CONTRIBUTING.md. The full suite plus the on-air gate are required before anything merges to main.

Versions

GenWave releases follow a semantic versioning as follows:

<major_version>.<minor_version>.<bugfix_version>

Where:

  • major_version is bumped when there are major changes, i.e. major implementation change etc. Versions with different major versions are incompatible
  • minor_version is bumped when there are minor changes, i.e. new features, renaming, new modules etc. Versions with different minor versions may be incompatible
  • bugfix_version is bumped when a new bugfix version is published. Versions with only bugfix version changes should be compatible

Optional β€” prove the audio spine with the smoke test

Validates the riskiest third-party behavior (annotation format, Icecast password, crossfade overlap) with none of your own configuration in the way. Needs ffmpeg/ffprobe, jq, and the .NET 10 SDK on the host.

# Load MEDIA_DIR from .env into the shell (paths must resolve under the engine's /media mount)
set -a; . ./.env; set +a

# 1. Pick the most divergent (quiet vs. loud) track pair from your library
cd tools
dotnet run find_smoke_candidates.cs -- "$MEDIA_DIR"
cd ..

# 2. Run the automated smoke test. Brings up db+engine+icecast, pushes the pair, records the
#    stream, asserts output LUFS β‰ˆ target for both with no silent gap at the crossfade.
#    Exits non-zero on failure. (SMOKE_DOWN=1 to tear down after.)
cp tools/smoke-candidates.json .
./tools/smoke_test.sh

⚠️ The smoke test is a manual pre-release gate β€” CI does not run it. It uses the default compose project and pushes test tracks onto whatever engine it targets: run it only against a scratch stack (fresh checkout or isolated -p project), never a live station's deployment.

If level checks fail by a consistent offset, the replay_gain annotation format is wrong (bare number vs "X.XX dB") β€” the test's failure message points at this.

Shipped phases

GenWave's epic-by-epic history β€” from v1 broadcast playout through v5.4.0's "the DJ stops lying, and the house gets a voice" (a truth gate on every LLM-authored line, owner announcements the DJ works into the next break, and the Home Assistant integration that rings them), the v5.4.1 riders (the gate learns the station's name; the now-playing sensor works on a shipped appliance with the admin plane off), v5.5.0's "the library tends itself" (the Gardener's five rot passes, listener thumbs feeding a rotation nudge, Deep Cuts rotation predicates, opt-in file actions), and the v5.5.1 rider (the findings queue at scale: kind tabs + group-safe paging) β€” lives in CHANGELOG.md.

Roadmap

  • Deferred β€” authored-file GC (gh-#3), origin-side Access JWT validation (gh-#75), migration-runner adoption (gh-#12), a purge-tripwire override for legitimate whole-library replacement (gh-#656), scan-side dedupe for directory symlinks inside MediaRoot (gh-#650).
  • Beat-matching + set-level sequencing β€” BPM/beat-aware transitions and energy-curve scheduling beyond per-pair crossfade duration. Deferred as YAGNI.

Operational notes

  • The Liquidsoap control port (1234) is unauthenticated and never published. To inspect it: docker compose exec engine bash then connect to localhost:1234 from inside the container.
  • Icecast /admin and /status share port 8000 β€” password-protected but reachable on the LAN. Never publish 8000 on a public box: the reference public topology fronts everything with Caddy and un-publishes it, and CI enforces the posture via tools/check-compose-publish.sh (0.0.0.0 publishes allowed only for the proxy).
  • Upgrading an existing deployment: git pull && ./launch.sh --pinned on an appliance/demo box, ./launch.sh on the dev stack β€” both run ./migrate.sh themselves (every db/*-migration.sh, idempotent, before anything restarts onto the new images). Call ./migrate.sh by hand only on a box that doesn't use launch.sh at all; a raw docker compose up does not migrate. See DEPLOYMENT.md.
  • A library row the scan can no longer verify is quarantined, never fed to the engine (v5.4.0): rows discovered under a previous MEDIA_DIR root are marked unavailable after the scan's miss grace, and a push checks the file exists first β€” a moved library degrades to fewer tracks, not to silent safe-loops.
  • Secrets live only in .env (gitignored). Promote to Docker secrets before anything public.
  • If you change duration= in engine/genwave.liq, pass the matching CROSSFADE= to smoke_test.sh so its analysis windows line up.
  • The crossfade operator behavior and output.icecast.metadata on-air signal are specific to Liquidsoap 2.4.x. The engine image is pinned to v2.4.5 in engine/Dockerfile (FROM savonet/liquidsoap:v2.4.5) β€” compose.yaml only echoes the pin in a comment β€” do not change it without re-running the smoke test.

Built with AI assistance

GenWave is developed openly with AI as a force multiplier for the people (me) building it β€” not a replacement for them. Design decisions, reviews, and sign-offs are human; the .claude/ toolkit in this repository is part of how the project is built and you're welcome to use it. The same deal applies to contributions β€” see CONTRIBUTING.md.

If you want the workflows/skills I use in GenWave for your own projects, you can find them here, along with a lot of other awesome AI resources. Hats off to Rob Conery for his awesome Claude Code Toolkit!

Standing on

GenWave is a control plane, not a from-scratch audio engine β€” it's wired around real infrastructure that did the hard parts first:

  • Liquidsoap β€” the real-time mixing, crossfade, and encode engine underneath the whole broadcast.
  • Icecast β€” fans the mixed stream out to every listener.
  • Kokoro β€” the primary DJ voice, an open-weight TTS model rendered locally.
  • Piper β€” the CPU-only fallback voice that keeps a DJ talking on modest hardware.
  • Ollama β€” runs the local LLM that writes lead-ins and back-announces when you point GenWave at one β€” the station airs template patter without it.
  • ffmpeg β€” loudness, cue points, energy analysis: the numbers every crossfade decision rests on.
  • aubio β€” the BPM analysis behind every track's tempo.
  • PostgreSQL β€” the catalog of record for every track, measurement, and scheduled show.

πŸ™ to the maintainers of all eight β€” none of this plays a note without them.

Contributing

Contributions are welcome β€” see CONTRIBUTING.md. External contributions require a one-time, lightweight CLA so the Home/Business dual-license model stays viable. Please also read the Code of Conduct and, for anything security-shaped, SECURITY.md.

License

GenWave ships in two editions:

  • GenWave Home β€” this repository. Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). GenWave Home is AGPL and always will be.
  • GenWave Business β€” a commercial edition built on the same core, licensed separately. Development of Home is funded by GenWave Business.

One deliberate exception: the module contract surface in src/GenWave.Abstractions/ (published as the GenWave.Abstractions nuget package β€” 5.5.0 at v5.5.0, additive across 5.x) is MIT-licensed β€” see its LICENSE β€” so any module, open or commercial, can link the contracts freely. Everything else in this repository is AGPL-3.0-only.

About

GenWave is an AI-enhanced broadcast platform capable of streaming curated audio content 24/7 without human intervention, combining AI-generated content, persona-based DJ "announcers", and local music library management to create a complete audio experience that can be configured for different genres & schedules.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages