Android M1: meeting recording via foreground service

- core/recording: MeetingRecorder (AudioRecord at the device's native
  rate, WAV on disk + 60 s rolling window resampled to 16 kHz), streaming
  WavWriter, thread-safe RollingWindow, linear resampler; WavReader moved
  here from the app
- app: RecorderService (foreground, type microphone) with ongoing
  notification and StateFlow state; Record/Stop UI with timer and level
  meter; runtime permission flow; finished recordings auto-load for
  transcription
- launcher icon (mic + waveform, matching the desktop brand) and
  notification glyph
- fix real M0 bug caught by the new unit tests: WavReader parsed 16-bit
  fmt fields (audioFormat/channels/bitsPerSample) with 32-bit reads
- JVM tests: WAV round trip, native-rate header, resampler, rolling
  window — 5/5 green; assembleDebug and aapt2 APK checks pass
- validated on-device on a Fairphone 6 (Android 16): model download,
  engine load, recording and transcription all working
This commit is contained in:
2026-09-07 11:31:29 +02:00
parent e5ec95a5fb
commit 8e079467fa
19 changed files with 803 additions and 45 deletions
+17 -10
View File
@@ -4,10 +4,10 @@ Native Android app with the same functionality as the desktop meetrec:
record meetings and transcribe them on-device with Whisper. Nothing leaves
the phone — no cloud, no telemetry.
Status: **M0 (engine proof)** — the app can download a GGML model, load it
via JNI (whisper.cpp, CPU/NEON) and transcribe a picked WAV file. Recording,
live transcript, and output files arrive in later milestones (see the
milestone plan in the project docs).
Status: **M1 (recording)** — record a meeting with a foreground service
(native sample rate, timer, level meter), then transcribe it on device.
Model download + engine load are from M0; the live rolling transcript
arrives with M2 (see the milestone plan in the project docs).
## Requirements
@@ -28,15 +28,15 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
`third_party/whisper.cpp` is gitignored — the fetch script pins the exact
release tag so the JNI layer never breaks on upstream churn.
## Test on device (M0)
## Test on device
1. Launch **MeetRec**, pick a model (`tiny` is fine for a first test) and tap
**Download** (model comes from Hugging Face; tiny is ~75 MB).
2. Tap **Load engine**.
3. Tap **Pick WAV file** and choose a 16 kHz mono WAV for best results
(other PCM WAVs are resampled automatically).
4. Tap **Transcribe** — segments with timestamps appear; share via the
share sheet.
3. Tap **Record**, speak, then tap **Stop** — the recording is saved as WAV
(native device rate) and auto-loaded for transcription.
4. Tap **Transcribe** — segments with timestamps appear; share via the share
sheet. You can also pick any PCM WAV file instead of recording.
Expect roughly realtime transcription with `tiny`/`base` on the
Fairphone 6's CPU; `small` is noticeably slower — use it for final passes
@@ -51,12 +51,19 @@ only (the live/final split comes with the recorder milestones).
## Module layout
```
app/ Compose UI (model download, WAV picker, transcript view)
app/ Compose UI + RecorderService (foreground mic recording)
core/recording/ MeetingRecorder, WavWriter/WavReader, RollingWindow,
linear resampler — pure Kotlin, unit tested on the JVM
core/whisper/ whisper.cpp JNI wrapper: CMake build + LibWhisper.kt +
WhisperEngine.kt (the on-device transcription API)
tools/ fetch-whisper.sh — vendor the pinned whisper.cpp release
```
Recording mirrors the desktop app: audio is captured at the input's native
rate (WAV on disk keeps it), while the 60 s rolling window is resampled to
Whisper's 16 kHz for the upcoming live pass. Recording continues while the
app is backgrounded via the microphone foreground service.
Models live in the app's private storage (`filesDir/models`), shared files
with the desktop app's `~/.cache/meetrec/whisper-cpp` naming
(ggml-tiny.bin … ggml-large-v3.bin).