# 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.