M1-06: Background recording and audio session hardening #18
No reviewers
Labels
No labels
area/agent
area/capture
area/editor
area/geo
area/security
area/sync
area/transcription
area/ui
area/vault
area/voice-memos
blocked
milestone/M1
milestone/M2
milestone/M3
milestone/M4
needs-decision
needs-hardware
p0
p1
p2
type/bug
type/enhancement
type/feature
type/idea
type/infrastructure
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Stash/stash-ios!18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "m1-06-background-audio"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue #6. Background recording, route-change handling, and interruption resume.
The route-change fix is structural, not a handler
The writer used to be configured from whatever format the input node reported at start, which pins the recording to the first microphone connected. A new route arrives at a different sample rate, every buffer is rejected, and the result is a file of silence — failure mode 2 in the issue, and a design fault rather than a missing observer.
AudioFileSinknow writes one fixed format, 48 kHz mono, and converts everything into it with anAVAudioConverter. A route change costs a new converter and nothing else; the writer never learns anything happened, so one memo stays one file across as many microphones as it likes.Failure mode 1 (stale microphone) falls out of the same change — the input format is read fresh from the current route on every rebuild instead of captured once and trusted.
Failure mode 3 (declined call) —
.began/.endedis always resumed, never treated as a stop..endedis acted on whether or not it carriesshouldResume: that option is advice about other audio politely resuming, and a memo started deliberately is not other audio.Two changes that are easy to miss in review
Timestamps count frames written, not
AVAudioTime.sampleTime. The audio clock does not advance while an interruption holds the engine, so clock-derived timestamps leave a silent gap the length of the call — and restarting the engine can reset it entirely, producing timestamps that run backwards and a file no player will open. Counting frames splices the interruption out and cannot go backwards.UIBackgroundModesneeded a real Info.plist.INFOPLIST_KEY_UIBackgroundModeslands in the pbxproj and Xcode never writes it into the plist, so background audio read as configured while being absent from the only file that decides it. Caught by checking the built plist rather than the build setting.project.ymlnow generatesStash/Info.plistfrominfo.properties, which is gitignored like the project itself.AGENTS.mdrecords the check:plutil -p …/Stash.app/Info.plist.Losing the microphone no longer ends a recording either — the file stays open,
isInterruptedsays so, and the next route change gets another attempt.Also adds the current microphone to the debug view while recording, read from the session's route rather than remembered. v2's worst failure was recording from a microphone nobody knew it still held.
Validated
On device by Wes: started on the built-in microphone, plugged in wired headphones mid-recording, backgrounded the app, reopened. All worked, and the microphone display tracked the route correctly.
In the simulator: backgrounded 15 seconds mid-recording and got 95.04s of continuous audio for ~95s of wall clock — no gap, no drift at that rate.
UIBackgroundModes: [audio]confirmed in both the simulator and device built plists.Resampling, by probe, because the simulator's input is already 48 kHz mono and the converter is an identity there — proving nothing about the case that matters. 16 kHz mono (Bluetooth HFP), 8 kHz, 44.1 kHz mono and stereo, and 48 kHz stereo all convert with amplitude preserved.
make lintclean at--strict.Not validated
Declining a call has no evidence behind it at all. The interruption path is reasoned from the documented notification order; nothing here has taken a phone call. Wes is testing it later today with a second phone, and it gets its own issue if it fails.
finishWriting()is awaited rather than blocked on, so the main thread is not held, but v2's freeze was never reproduced to confirm this is the same path.CI has not run this; the simulator build workflow still has no macOS agent.
Issue #6 stays open after this merges — two done-when boxes are still unticked.