Files
lockstep-player/DESIGN.md

149 lines
9.8 KiB
Markdown
Raw Normal View History

2026-05-13 21:27:19 +02:00
# Lockstep Android MVP — design
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
Product goals, DSP scope, and repo layout are summarized in [SPECS.md](SPECS.md). This document captures **Android shell architecture**, **decisions made so far**, **libpasada JNI + state machine**, and **open UI questions**.
2026-05-13 20:39:34 +02:00
## UI screens
- Onboarding: explains why notifications are necessary, implements login
- Library: shows the playlists of the user
- Annotation: user presses a button to annotate the beat
- Now Playing: player showing an individual track, with pause/previous/next buttons
- Collection mode: app records sensor data during a run
- Settings: allows to enable modes above, logout button
2026-05-13 21:27:19 +02:00
## Already captured from SPECS.md
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
- **Product**: Pace-aware playlist ordering + real-time playback adaptation via accelerometer, **libpasada** (C++/JNI) + **Oboe**, user-supplied **MP3** via file descriptors, feedback loop for sensor-driven playback.
- **Metadata**: **[jukebox](jukebox/)** submodule — Spotify-backed metadata in **SQLite**, separate from DSP.
- **Native**: libpasada independently buildable/testable; Android side may **stub** JNI until native is ready.
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
## Your decisions (this session)
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
| Topic | Choice |
|-------|--------|
| Session context | **Background / screen off** — needs foreground service, ongoing notification, and media-style controls expectation |
| Playlist assembly | **Manual** local MP3 pick + align/display with jukebox metadata where possible |
| Playback ownership | **Run mode owned by a foreground service** — Oboe, libpasada JNI, and accelerometer feeding live in the service while a run is active; UI binds for display/control only |
| Accel → native path | **`SensorManager` callbacks on a dedicated `HandlerThread` (not main)** — **`onSensorChanged` may call JNI directly**; **no intermediate Java queue** for MVP. libpasada must be safe relative to concurrent Oboe/audio-thread work (locking or lock-free handoff inside native). |
| Jukebox scope (MVP) | **Playlist identity + BPM/metadata in SQLite**; **album/track artwork deferred** — artwork is **intended to live in jukebox later**, not in the app long-term. |
| Spotify credentials | **Refresh is triggered and handled by the host app** (lifecycle, WorkManager, error handling, user re-login). Jukebox remains a **library**: expose/sync APIs that **accept valid credentials or sessions** the app supplies after refresh; avoid owning background refresh loops unless you later consolidate that into jukebox by explicit choice. |
| MP3 ↔ jukebox matching | **1)** Match primarily using **embedded MP3 tags** (ID3 / common frames—title, artist, album; plus **ISRC** when present on both file and jukebox row). **2)** If no confident unique match, **prompt the user to link manually** (pick jukebox track ↔ local file). Persist manual links in app or jukebox-backed **mapping store**. **Ambiguous multi-match** should fall through to manual/disambiguation (exact field weights deferred at implementation). |
| libpasada JNI + states | **State machine** (below); JNI entry points **`init` (per run start)**, **`feedAccel`**, **`play`** (MP3 FD + begin adaptive playback), **`pause`**, **`resume`**, **`stop`**, **`getDiagnostics`**. |
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
## libpasada state machine (native)
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
Oboe behavior is summarized per state:
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
| State | Meaning |
|-------|---------|
| **LOADED** | JNI `.so` loaded (`System.loadLibrary`); no Oboe callbacks registered yet. |
| **INITIALIZED** | Oboe stream active, outputting **silence**; callbacks registered; ready for **`play()`**. |
| **PLAYING** | Oboe outputting **decoded/adapted music** (pace-driven processing active). |
| **PAUSED** | User paused: Oboe still runs but outputs **silence** (distinct from STOPPED). |
| **FINISHED** | Current track ended: Oboe outputs **silence** until the app starts another **`play()`** (or you later add explicit advance rules). |
| **STOPPED** | Oboe **callbacks unregistered** / stream torn down for this run segment; lowest-power idle after **`stop()`**. |
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
**JNI calls (frozen names for stubs)**
2026-05-13 20:39:34 +02:00
2026-05-13 21:27:19 +02:00
- **`init()`** — Called **each time a run is started** (not once per app launch): transition into **INITIALIZED** (register Oboe, run silent audio, allocate buffers, reset pace pipeline as needed). Preconditions: typically **LOADED** or returning from **STOPPED** after prior **`stop()`**.
- **`feedAccel()`** — Submit accelerometer samples to the pace estimator (unchanged intent).
- **`play(...)`** — Accept **open MP3 file descriptor** (and any offset/length/index the native decoder needs), begin decoding and **adaptive playback****PLAYING**. Used when starting or switching tracks from **INITIALIZED** or **FINISHED** (new FD); **not** used to leave **PAUSED** (use **`resume()`**).
- **`pause()`** — **PLAYING → PAUSED** (silent output; keep graph alive).
- **`resume()`** — **PAUSED → PLAYING**: continue the **same** track/decode position and adaptive processing without re-opening the FD.
- **`stop()`** — Tear down to **STOPPED** (unregister Oboe callbacks / release stream for this run).
- **`getDiagnostics()`** — Runtime metrics / last error for logging and UI (unchanged intent).
```mermaid
stateDiagram-v2
direction LR
[*] --> Loaded
Loaded --> Initialized: init on run start
Initialized --> Playing: play fd
Playing --> Paused: pause
Paused --> Playing: resume
Playing --> Finished: track ends native
Finished --> Playing: play next fd
Paused --> Stopped: stop
Playing --> Stopped: stop
Finished --> Stopped: stop
Initialized --> Stopped: stop
Stopped --> Initialized: init next run
```
## Architecture implications (non-negotiable follow-through)
- **Foreground service** + **notification channel** (Android 8+) with **media controls** (Compact/MediaStyle or MediaSession) so runs survive screen-off and fit user expectations.
- **Lifecycle split**: **ForegroundService is the single owner** of the real-time pipeline during a run (Oboe + JNI + `SensorManager` registration with a **`Handler` on a background `HandlerThread`** for listener delivery). Activity/Compose is for setup, library mapping, and reflecting service state—not for owning playback threads. Accelerometer-driven JNI runs on that **sensor HandlerThread**, concurrent with Oboes audio callback path unless libpasada merges them internally. (Separate process for the service remains optional if OEM kills warrant it—defer unless needed.)
- **Permissions & battery**: `POST_NOTIFICATIONS` (13+), possibly **exact alarm** if you schedule anything (likely N/A for MVP); document **ignore battery optimizations** only if you discover OEM kills sensor/audio — question below.
- **Jukebox vs app**: Treat jukebox as **metadata persistence + Spotify fetch helpers**; the **app owns OAuth/token refresh scheduling** and passes renewed tokens (or session handles) into jukebox when syncing. **Reads** should stay useful **offline** from SQLite; **network sync** requires valid credentials (exact failure UX still covered under onboarding/errors below).
- **Library ingestion**: Reading tags runs **off main thread** (e.g. during folder scan or when user adds files); store resolved **local URI/path ↔ jukebox track id** so the foreground service can open FDs without redoing fuzzy matching.
- **ForegroundService ↔ JNI**: Starting a run calls **`init()`**; handing off each local track calls **`play(fd, …)`**; **`pause()`** / notification pause map to **`pause()`**; **`resume()`** when the user resumes from pause; **`stop()`** when the run ends. Sensor **`HandlerThread`** keeps calling **`feedAccel()`** whenever registered.
---
## UI / UX questions (MVP 1)
### Navigation and screens
1. **Minimum screens**: e.g. Run (now playing + pace indicator), Library (local files + jukebox side-by-side?), Settings — which are **must-have** vs deferred?
2. **Run screen layout**: Dominant **Now Playing** + subtle pace vs dominant **pace/BPM match** + compact transport?
3. **Start/stop run**: Explicit **Start run** that arms sensors + service vs auto-start on play?
### System UI integration
4. **Notification**: Full **MediaSession** (lockscreen, BT headsets, Wear) vs minimal ongoing notification only?
5. **Widgets / Wear**: Explicitly **out of scope** for MVP?
### Onboarding and permissions
6. **First-run flow**: Grant notifications, pick folder, sign-in to Spotify (if required by jukebox), battery optimization warning — **single wizard** or inline prompts?
7. **Empty states**: No MP3s mapped, no Spotify login, no BPM in DB — copy and primary actions?
### Visual design
8. **Design system**: Material 3 / dynamic color vs fixed brand palette for sports contexts (high contrast sunlight)?
9. **Accessibility**: Minimum targets for sweaty fingers, **TalkBack** labeling for pace and controls?
### Feedback and failure modes
10. **When pace detection is noisy**: Show confidence, freeze adaptation, or fallback to **nominal BPM**?
11. **Errors**: Missing file, codec failure, JNI assertion — **blocking dialog** vs toast + safe stop?
---
## Suggested order to resolve
```mermaid
flowchart LR
subgraph core [Core commitments]
FS[ForegroundService]
MS[MediaSession optional depth]
JNI[libpasada JNI states]
end
subgraph data [Data layer]
Match[Tags then manual map]
JB[Jukebox SQLite plus app auth]
end
subgraph ui [UI shell]
Nav[Screen map and navigation]
RunUX[Run lifecycle UX]
end
FS --> MS
FS --> JNI
JNI --> Match
Match --> JB
FS --> RunUX
Nav --> RunUX
```
1. **JNI + libpasada states** — frozen (see **libpasada state machine** section).
2. **MP3↔metadata matching****embedded tags first**, then **manual link** — decided.
3. Lock **screen map** + **notification depth**.
4. Fill **edge-case UX** (permissions, empty states, failures).
---
## Optional codebase alignment
Quick pass over existing Gradle modules and any placeholder Activity will turn this questionnaire into file-specific tasks (app module layout, jukebox submodule APIs).