M1-06: Background recording and audio session hardening #6
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#6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Record with the phone locked, and stop losing recordings to audio-route and
interruption events. This issue is the single biggest reliability complaint about v2.
Wes, on what M1 has to be: "It handles microphone grabbing beautifully. It doesn't crash
like the other one, or lock up, like the other one."
The three known failure modes, all observed and documented
1 — The app holds a stale microphone instead of probing the current default. From the
2026-07-30 memo: recorded on AirPods, backgrounded the app, used the phone for an hour
without AirPods, reconnected them, started a new recording. Wes's read: "The app is not
probing to see what the current default settings are... maybe it has like a previous grab on
the mic and speakers from the AirPods."
2 — Swapping to wired headphones produced a recording of nothing. Same day: AirPods put
away, wired headphones plugged in, hit record. The recording toast appeared, nothing was
captured, a warning about failing to grab the mic appeared, and on hitting stop the whole
app froze on "saving" — for what was four seconds of silence, one or two kilobytes.
3 — A declined phone call ended the recording. 2026-08-06: "this stops recording, if I
get a phone call... I didn't answer it. I declined it. But the recording had already
stopped." This one forced the morning's scoping memo to be split into two files.
Scope
AVAudioSessioncategory and options.AVAudioSession.routeChangeNotificationand follow thecurrent default input rather than caching one. Ideally the switch is seamless mid-recording,
the way the system phone app hands off; if that is not achievable, it must at minimum not
silently record nothing.
AVAudioSession.interruptionNotification. An interruption thatends without the user taking the call must resume the recording, not terminate it.
Done when — Wes validates all four on the device
Milestone 1 of the Stash v3 rebuild, scoped 2026-08-06. M1 is a functioning iOS app on Wes's
phone with no sync at all — the entire goal is recording voice memos reliably and getting
them off the device by hand. Sync arrives in M3.
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.
Branch
m1-06-background-audio, one commit, installed on your phone. No PR — this isneeds-hardwareand three of the four done-when boxes need you and real hardware.The route-change fix is structural, not a handler
Failure mode 2 — swap headphones, record nothing — was a design fault. The writer used to be configured from whatever format the input node reported at start, which pins the whole recording to the first microphone connected. A new route arrives at a different sample rate, every buffer is rejected, and you get a file of silence.
AudioFileSinknow writes one fixed format, 48 kHz mono, and converts everything into it. A route change costs a newAVAudioConverterand 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 now 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 you started deliberately is not other audio.Timestamps changed too, and this one is easy to miss in review. They now count frames written rather than reading
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.A trap worth knowing about
UIBackgroundModescould not be set the way it looked like it could.INFOPLIST_KEY_UIBackgroundModeslands in the pbxproj and Xcode never writes it into the Info.plist — so background audio read as configured everywhere except in the one file that decides it. I caught it by checking the built plist rather than the build setting.project.ymlnow generates a realInfo.plist. This is inAGENTS.md: checkplutil -p …/Stash.app/Info.plist, not the setting.What I validated
UIBackgroundModes: [audio]confirmed present in the built plist, for both the simulator and the device build.make lintclean, builds, installed and confirmed running onwes-iphone.What I did NOT validate
Three of your four done-when boxes. I cannot swap AirPods, take a call, or lock your phone:
Two more gaps worth naming:
finishWriting()is awaited rather than blocked on, so the main thread is not held, but I have not reproduced v2's freeze to confirm this is the same code path that caused it.Worth doing the long walk test before this merges, since that is the one M1 exists for.
Device pass, from Wes on
wes-iphone: started on the built-in microphone, plugged in wired headphones mid-recording, backgrounded the app, reopened it — all worked, and the microphone display tracked the route correctly throughout.That covers two of the four boxes:
PR is open on that basis. The call path remains the one piece of this issue with no evidence behind it: it is reasoned from the documented
.began/.endedorder, and nothing here has taken a phone call.Also still open, and worth repeating because a short test cannot show it: drift is only measured where the converter is an identity. A 40-minute recording over Bluetooth could come out short without anything looking wrong along the way.