Files
lockstep-player/DESIGN.md

148 lines
9.7 KiB
Markdown
Raw 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.
# Lockstep Android MVP — design
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**.
## UI screens
- 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
## Already captured from SPECS.md
- **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.
## Your decisions (this session)
| 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`**. |
## libpasada state machine (native)
Oboe behavior is summarized per state:
| 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()`**. |
**JNI calls (frozen names for stubs)**
- **`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).