# 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** — 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)
