Files
Android-247-Radio/README.md
cottongin a09c50c302 docs: refresh README with screenshots, full feature list, and build guide
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
2026-03-18 14:06:00 -04:00

150 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
|:---:|:---:|:---:|:---:|
| ![Station list showing SomaFM tab](docs/screenshots/01-station-list.jpg) | ![Now Playing with album art](docs/screenshots/02-now-playing-art.jpg) | ![Station list with mini-player bar](docs/screenshots/03-station-list-miniplayer.jpg) | ![Now Playing with station art fallback](docs/screenshots/04-now-playing-station-art.jpg) |
---
## 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** — 0500ms 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)