M1-05: Audio capture core — one continuous file that survives a kill #5

Open
opened 2026-08-08 20:03:43 -04:00 by agent · 0 comments
Member

Record audio to a single continuous file that survives the process being killed.

The requirement, unchanged from iOS

Audio is not chunked. Transcripts are chunked so the vault agent can read them mid-walk; audio
is not, because it is not synced mid-walk and there is nothing to gain. Chunking introduces a seam
problem — a click and a missing syllable every sixty seconds — and owes a concatenation step at
exactly the moment that can least afford to fail.

What must hold: a recording killed at minute 40 still yields 40 minutes of audio.

Format

AAC, mono, ~48 kbps, matching iOS. Not lossless — v2 wrote ALAC and put a 45-minute memo in the
hundreds of megabytes, which is fine when nothing syncs and not fine once M3 moves audio over a
cell connection.

The open question, and it is the substance of this issue

iOS solves truncation-tolerance with AVAssetWriter.movieFragmentInterval, which flushes a
playable MP4 index periodically. Android's MediaMuxer has no equivalent worth relying on, and
MediaRecorder writes its index at the end — kill it mid-recording and the .m4a is a file full of
audio that nothing will open. That is exactly the v2 failure this project exists to remove.

Two candidate answers, and Wes's call at the planning checkpoint:

Raw ADTS AAC (.aac). Every frame carries its own header, so the format needs no index at all
and a truncated file is simply a shorter recording. This is structurally more robust than what
iOS does — there is no flush interval to lose, no window at all. The cost is that the file on disk
stops matching iOS's .m4a, which breaks format parity across clients that M3 has to reconcile.

ADTS during capture, remuxed to .m4a at stop. Parity is preserved and the crash artifact is
still complete. The cost is a remux step at stop and a recovery path that finds an orphaned .aac
and finishes it — which is the same shape as the transcript's assembly and recovery, so it is a
pattern the app already has to have.

The second is the recommendation, but it is a real decision with a real cost and it belongs to Wes,
not to the implementation.

Read the current MediaCodec / MediaMuxer / AudioRecord documentation before building on any
of this.
The iOS issue carried the same warning about movieFragmentInterval and it was the right
call — the mechanism is the load-bearing claim in this issue.

Naming

YYYY-MM-DD-HHMMSS-<hash>_audio.<ext>, stamped with the original capture time. Carried over
from v2 deliberately: the vault's CLAUDE.md documents it and inbox processing depends on it. The
format spec lives in stash-docs and both clients must agree — if this issue changes the
extension, the spec changes with it, in the same PR.

Scope

  • AudioRecord feeding MediaCodec, writing to the app's files directory.
  • Start and stop. A debug button only — no real UI; M1-03 owns the UI.

Done when

A recording plays back correctly, and force-stopping the app mid-recording still leaves a playable
file containing everything up to the kill.


Milestone 1 of Stash for Android, mirroring stash-ios M1. M1 is a functioning app on the test
device with no sync at all — record voice memos reliably and get them off the device by hand.
Sync arrives in M3.

Test device is a Pixel 6 (Tensor G1) borrowed from a friend, until something better turns up.
Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from
another manufacturer may not be as forgiving about background work.

Translation of Stash/stash-ios#5. Where the platform forces a different answer this issue says so; where it
does not, the iOS issue is the fuller statement of intent and should be read alongside this one.

Working agreement for every issue in this repo: feature branch, tested before the PR opens
(on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs
update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and
split it.

Record audio to a single continuous file that survives the process being killed. ## The requirement, unchanged from iOS **Audio is not chunked.** Transcripts are chunked so the vault agent can read them mid-walk; audio is not, because it is not synced mid-walk and there is nothing to gain. Chunking introduces a seam problem — a click and a missing syllable every sixty seconds — and owes a concatenation step at exactly the moment that can least afford to fail. What must hold: **a recording killed at minute 40 still yields 40 minutes of audio.** ## Format **AAC, mono, ~48 kbps**, matching iOS. Not lossless — v2 wrote ALAC and put a 45-minute memo in the hundreds of megabytes, which is fine when nothing syncs and not fine once M3 moves audio over a cell connection. ## The open question, and it is the substance of this issue iOS solves truncation-tolerance with `AVAssetWriter.movieFragmentInterval`, which flushes a playable MP4 index periodically. Android's `MediaMuxer` has no equivalent worth relying on, and `MediaRecorder` writes its index at the end — kill it mid-recording and the `.m4a` is a file full of audio that nothing will open. That is exactly the v2 failure this project exists to remove. Two candidate answers, and **Wes's call at the planning checkpoint**: **Raw ADTS AAC (`.aac`).** Every frame carries its own header, so the format needs no index at all and a truncated file is simply a shorter recording. This is *structurally* more robust than what iOS does — there is no flush interval to lose, no window at all. The cost is that the file on disk stops matching iOS's `.m4a`, which breaks format parity across clients that M3 has to reconcile. **ADTS during capture, remuxed to `.m4a` at stop.** Parity is preserved and the crash artifact is still complete. The cost is a remux step at stop and a recovery path that finds an orphaned `.aac` and finishes it — which is the same shape as the transcript's assembly and recovery, so it is a pattern the app already has to have. The second is the recommendation, but it is a real decision with a real cost and it belongs to Wes, not to the implementation. **Read the current `MediaCodec` / `MediaMuxer` / `AudioRecord` documentation before building on any of this.** The iOS issue carried the same warning about `movieFragmentInterval` and it was the right call — the mechanism is the load-bearing claim in this issue. ## Naming `YYYY-MM-DD-HHMMSS-<hash>_audio.<ext>`, stamped with the **original capture time**. Carried over from v2 deliberately: the vault's `CLAUDE.md` documents it and inbox processing depends on it. The format spec lives in `stash-docs` and **both clients must agree** — if this issue changes the extension, the spec changes with it, in the same PR. ## Scope - `AudioRecord` feeding `MediaCodec`, writing to the app's files directory. - Start and stop. A debug button only — no real UI; M1-03 owns the UI. ## Done when A recording plays back correctly, and force-stopping the app mid-recording still leaves a playable file containing everything up to the kill. --- *Milestone 1 of Stash for Android, mirroring `stash-ios` M1. M1 is a functioning app on the test device with **no sync at all** — record voice memos reliably and get them off the device by hand. Sync arrives in M3.* *Test device is a **Pixel 6** (Tensor G1) borrowed from a friend, until something better turns up. Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from another manufacturer may not be as forgiving about background work.* *Translation of `Stash/stash-ios#5`. Where the platform forces a different answer this issue says so; where it does not, the iOS issue is the fuller statement of intent and should be read alongside this one.* *Working agreement for every issue in this repo: feature branch, tested before the PR opens (on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and split it.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-android#5
No description provided.