Add AI disclaimer, 4 emulator screenshots, expanded feature sections (Android Auto, SomaFM, per-station quality, tabs, Media3 notification, now-playing logging, blurred art background), build.sh documentation, inline architecture diagram, and tech stack table. Made-with: Cursor
150 lines
6.1 KiB
Markdown
150 lines
6.1 KiB
Markdown
# 24/7 Radio
|
||
|
||
Android app for 24/7 internet radio streaming with minimum latency, aggressive reconnection, and full Icecast/Shoutcast metadata support.
|
||
|
||
> [!IMPORTANT]
|
||
> This project was built with the assistance of [Cursor](https://cursor.com) (Claude Opus/Sonnet 4.6).
|
||
|
||
---
|
||
|
||
## Screenshots
|
||
|
||
| Station List | Now Playing | Mini-Player | Station Art Fallback |
|
||
|:---:|:---:|:---:|:---:|
|
||
|  |  |  |  |
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
### Playback
|
||
- **Custom raw audio pipeline** — OkHttp → IcyParser → Mp3FrameSync → MediaCodec → AudioTrack for minimum latency (~26ms per MP3 frame)
|
||
- **Stay Connected mode** — aggressive reconnection with exponential backoff; never gives up until you say stop
|
||
- **Configurable buffer** — 0–500ms slider; 0ms keeps you as live as possible, higher values smooth out spotty connections
|
||
- **Icecast/Shoutcast metadata** — ICY protocol track title and artist extraction
|
||
- **Album art** — MusicBrainz/Cover Art Archive lookup with a fallback chain (ICY stream URL → station default art → station logo → placeholder)
|
||
|
||
### Station Management
|
||
- **SomaFM built-in** — full SomaFM catalog pre-loaded, sorted by listener count
|
||
- **Per-station stream quality** — choose 128/256 kbps and SSL/non-SSL per station, or set a global preference order
|
||
- **PLS/M3U import/export** — with `#EXTIMG` support for station artwork URLs
|
||
- **Tabs (playlists)** — pin/unpin, rename, and drag-to-reorder tabs and stations within them
|
||
- **Starring/favoriting** — starred stations sort to the top of their tab
|
||
|
||
### Now Playing Screen
|
||
- **Blurred album art background** — art fills the screen behind the player controls
|
||
- **Session timer** — total elapsed time since you hit play, not reset on reconnect
|
||
- **Connection timer** — elapsed time for the current TCP connection, resets on each reconnect
|
||
- **Latency indicator** — estimated stream-to-speaker latency in ms
|
||
- **Track history** — every track change is logged to the database with station and timestamp
|
||
- **Now Playing file logging** — optionally write track history to CSV, JSON Lines, or plain text
|
||
|
||
### System Integration
|
||
- **Media3 notification** — system media notification with album art, transport controls (play/stop + seek-to-live), lockscreen and Bluetooth headset support
|
||
- **Android Auto** — app registers as a media source; browse your playlists and stations from the car display
|
||
|
||
### Build Tooling
|
||
- **`build.sh`** — interactive menu: release APK (signed), debug APK, keystore management, and clean
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
- Android 9.0+ (API 28)
|
||
- Internet connection
|
||
|
||
---
|
||
|
||
## Build
|
||
|
||
The recommended path is the interactive build script:
|
||
|
||
```bash
|
||
./build.sh
|
||
```
|
||
|
||
Menu options:
|
||
1. **Build Release APK** — signed, ready to sideload
|
||
2. **Build Debug APK** — quick, for testing
|
||
3. **Manage Keystore** — create or inspect the signing key
|
||
4. **Clean** — remove build artifacts
|
||
|
||
On first run, choose option 3 to create a keystore before building a release APK. Output lands in `dist/`.
|
||
|
||
Alternatively, build directly with Gradle:
|
||
|
||
```bash
|
||
./gradlew assembleDebug # debug
|
||
./gradlew assembleRelease # release (requires keystore configured in build.gradle.kts)
|
||
```
|
||
|
||
---
|
||
|
||
## Importing Stations
|
||
|
||
1. Open the app
|
||
2. Tap the settings gear → Import
|
||
3. Select a `.m3u` or `.pls` file
|
||
4. Stations are added to your library
|
||
|
||
Exported files include `#EXTIMG` tags for stations that have a default artwork URL set.
|
||
|
||
---
|
||
|
||
## SomaFM
|
||
|
||
The SomaFM catalog is pre-loaded on first launch — no import needed. Stations are sorted by current listener count (refreshable with the sync button). You can set a global stream quality preference (256 kbps SSL recommended) or override it per station via long-press → Quality.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
Four layers:
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ UI Layer │
|
||
│ Compose screens, ViewModel, StateFlow │
|
||
├─────────────────────────────────────────┤
|
||
│ Service Layer │
|
||
│ RadioPlaybackService (MediaLibrary │
|
||
│ Service), RadioPlayerAdapter (Media3 │
|
||
│ Player facade), wake + wifi locks │
|
||
├─────────────────────────────────────────┤
|
||
│ Audio Engine │
|
||
│ StreamConnection → IcyParser → │
|
||
│ Mp3FrameSync → MediaCodec → AudioTrack │
|
||
├─────────────────────────────────────────┤
|
||
│ Data Layer │
|
||
│ Room DB, PLS/M3U import/export, │
|
||
│ DataStore preferences │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
The audio engine is a standalone component with no Android framework dependencies. The service owns reconnection policy, Android Auto browse tree, and the Media3 session. The UI observes state via `StateFlow` and never touches the engine directly.
|
||
|
||
For full detail see the [design document](docs/plans/2026-03-09-android-247-radio-design.md) and [Media3/Android Auto design](docs/plans/2026-03-18-media3-android-auto-design.md).
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
| Layer | Library |
|
||
|---|---|
|
||
| Language | Kotlin |
|
||
| UI | Jetpack Compose, Material Design 3 |
|
||
| Media session / Auto | AndroidX Media3 (media3-session, media3-common) |
|
||
| HTTP streaming | OkHttp |
|
||
| Audio decoding | MediaCodec (hardware-accelerated) |
|
||
| Audio output | AudioTrack |
|
||
| Database | Room |
|
||
| Preferences | DataStore |
|
||
| Image loading | Coil |
|
||
| Album art lookup | MusicBrainz / Cover Art Archive (no API key required) |
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE)
|