From 2c404234364fe8c86c1ac4de34c3fd1d6453c66d Mon Sep 17 00:00:00 2001 From: Caine Date: Thu, 26 Mar 2026 22:53:50 +0000 Subject: Add README with full architecture docs --- README.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8303b2 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# Radio Susan 📻 + +Personal internet radio station. 24/7 automated playback from a music library with smart DJ logic, energy-based scheduling, TTS announcements, and jingles. + +**Stream:** `https://radio.jihakuz.xyz/stream` (MP3 192kbps) + +## Stack + +| Component | Role | +|-----------|------| +| **Icecast2** | Streaming server | +| **Liquidsoap** | Radio automation — reads playlist, inserts jingles, outputs to Icecast | +| **generate_daily_playlist.py** | Builds a full 24h playlist with baked-in TTS announcements | +| **generate_site.py** | Generates a static HTML "now playing" site from the playlist | +| **radio_dj.py** | Fallback per-track DJ when no daily playlist exists | +| **dj_core.py** | Shared logic: track loading, energy filtering, cooldowns, modes | +| **analyse_tracks.py** | Essentia audio feature extraction (energy, BPM) | +| **build_genre_cache.py** | Genre cache from beets DB | +| **generate_jingles.py** | Generates jingle MP3s from TTS + SFX | +| **edge-tts** | Microsoft Edge TTS engine for announcements | + +## How It Works + +### Daily Pipeline + +Every midnight, `generate_daily_playlist.py` builds a complete 24-hour playlist: + +1. **Loads all tracks** from beets DB + genre cache + Essentia audio features +2. **For each hour**, looks up target energy from an energy curve (ambient at night, high energy in the evening) +3. **Picks a mode** (weighted random): shuffle 40%, deep cuts 20%, new to library 15%, old favourites 15%, full album 10% +4. **Selects a track** using contrast maximisation — prefers energy distance from recent picks +5. **Every 3 tracks**, generates a TTS announcement via edge-tts +6. **Outputs** `playlist.json` + `playlist.m3u` for Liquidsoap + +Liquidsoap picks up the new playlist automatically via file watching. + +### Fallback Chain + +If the daily playlist is empty or missing: +1. `radio_dj.py` — same smart DJ logic, picks one track at a time +2. `all.m3u` — pure random shuffle of the entire library (last resort) + +### Energy Schedule + +The DJ schedules by **energy level**, not genre. Whatever genre fits the energy gets played. + +| Show | Hours | Energy | Vibe | +|------|-------|--------|------| +| Night Drift | 00–07 | 1 | Ambient, drone | +| Morning Calm | 07–09 | 2–3 | Downtempo, chill | +| Late Morning | 09–12 | 4–5 | Indie, jazz, trip-hop | +| Lunch Break | 12–14 | 4 | Soul, dub | +| Afternoon Session | 14–17 | 5–6 | Electronic, funk | +| Drive Time | 17–19 | 5–6 | Mixed | +| Evening Heat | 19–22 | 7–8 | Techno, house, DnB | +| Wind Down | 22–00 | 2–4 | Comedown, ambient | + +### Smart Modes + +| Mode | Weight | Strategy | +|------|--------|----------| +| shuffle | 40% | Random from energy-matched pool | +| deep_cuts | 20% | Tracks with 0 plays | +| new_to_library | 15% | Added in last 14 days | +| old_favourites | 15% | Highest play count | +| full_album | 10% | Plays entire album in order | + +### Cooldowns + +- **Track:** 7 days +- **Artist:** 3 hours +- **Album:** 24 hours +- **Genre:** no back-to-back same genre + +### Announcements vs Jingles + +- **Announcements** are baked into the playlist at generation time — pre-rendered TTS clips inserted into the M3U every 3 tracks. +- **Jingles** are dynamic in Liquidsoap — randomly rotated from `/jingles/`, roughly 1 per 4 tracks. + +## Audio Analysis + +`analyse_tracks.py` uses Essentia to extract per-track features: + +- **Energy** — normalised via percentile breakpoints to a 1–10 scale +- **BPM** — via RhythmExtractor2013 + +Results in `audio_features.db` (SQLite). Incremental — only analyses new files. + +**Known issue:** BPM detection is unreliable for fast genres (hardcore, breakcore, DnB) — often reads half or a third of the real tempo. + +## Static Website + +`generate_site.py` builds a single-page dark-themed site: + +- Now Playing banner (polls track state) +- Up Next preview +- Stats bar +- Collapsible show cards with full tracklists +- Inline listen button + +## Setup + +```bash +# Copy secrets template +cp secrets.liq.example secrets.liq +# Edit with your Icecast password +nano secrets.liq + +# Install Python deps +python3 -m venv venv +source venv/bin/activate +pip install edge-tts essentia + +# Crons (add to crontab) +0 0 * * * sg mediaserver "python3 generate_daily_playlist.py" +5 0 * * * sg mediaserver "python3 generate_site.py" +0 7 * * * python3 build_genre_cache.py +30 7 * * * venv/bin/python3 analyse_tracks.py +``` + +## Data Sources + +| Source | Used for | +|--------|----------| +| Beets DB | Track metadata, genres, added date | +| Navidrome DB | Play counts, starred tracks | +| audio_features.db | Essentia energy + BPM | +| genre_cache.json | Genre-to-energy mapping | + +## SFX Samples + +The `sfx/` directory contains sound effects used by `generate_jingles.py`: + +`airhorn` · `cashregister` · `countdown` · `crash` · `demon` · `meow` · `pirate_arr` · `pirate_plank` · `pirate_yaargh` · `reverb_hit` · `scream` · `static` · `sweep` · `synth_horn` · `text_tone` + +## Resource Usage + +Minimal. Icecast ~10MB RAM, Liquidsoap ~50–70MB RAM. Playlist generation ~2.5 minutes (mostly TTS). Analysis ~1s/track. -- cgit v1.2.3