M1-05: Audio capture core — one continuous AAC file #17
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!17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "m1-05-audio-capture"
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 #5. Records one continuous AAC file that survives the app being killed.
The decision the issue asked me to verify first
You flagged
AVAssetWriter.movieFragmentIntervalas asserted from memory. Apple documents it:That paragraph names QuickTime as the example container and this needs
.m4a, so I measured rather than inferred — a throwaway probe writing 20 seconds and never callingfinishWriting():movieFragmentIntervalmovieFragmentInterval = 5sThe shortfall is the tail since the last flush, which is what the interval bounds. Apple's 10-second guidance applies to external storage; this writes to internal flash, so 5 seconds buys a shorter tail. The value cannot be changed once writing starts.
What changed
Stash/Capture/:CaptureNaming.swift—YYYY-MM-DD-HHMMSS-HASH_audio.m4a, carried over byte for byte, stamped with the original capture time rather than the finalise time.en_US_POSIXon the formatter: a non-Gregorian default calendar renders the year as 2569, and nobody testing in English would ever see it.AudioFileSink.swift— AAC mono 48 kbps into the fragmented container. Runs on the audio tap's thread; anNSLockcovers the one overlap with stop, sinceremoveTapdoes not promise an in-flight callback has returned. The locked half offinish()is split out becauseNSLockmay not be held across a suspension point.AudioRecorder.swift— session, permission, engine, lifecycle. The tap is installed from anonisolatedfunction — see below.DebugCaptureView.swift— reachable from Settings. Not the real UI, and says so; the memo list's record button is still the design stub from #3.project.ymlgainsNSMicrophoneUsageDescription.A filename is claimed, not checked — exclusive create, redraw on collision. Check-then-write has a window, and losing it is one recording silently overwriting another.
The crash worth reviewing
The first version crashed instantly on Record with
BUG IN CLIENT OF LIBDISPATCH: Block was expected to execute on queue [com.apple.main-thread].installTaptakes a plain non-Sendableblock, so a closure written inline in a@MainActormethod inherits main-actor isolation — it compiles, then the render thread traps. Installing from anonisolated staticfunction fixes it. Written intoAGENTS.md, because the next audio callback has the same trap waiting and the compiler will not mention it.Validated
On hardware (
wes-iphone), by Wes: recording works, playback is correct, the force-quit test passes, and swapping microphones mid-session is handled cleanly.In the simulator, killing the process with SIGKILL mid-recording: playable, 20.05s recovered, 47 kbps, AAC mono 48 kHz. Projects to ~16 MB for 45 minutes.
make lintclean at--strict, 11 files.Not validated
There are no tests — no test target exists in this repository yet, so everything above was verified by hand exactly once and nothing guards it against regression. Also untested: recordings longer than a minute (the case the design exists for), backgrounding and screen lock (no background audio mode is declared, so capture almost certainly stops when the app leaves the foreground), call interruption, and storage filling mid-recording.
CI has not run this; the simulator build workflow still has no macOS agent.
One thing outside this issue
The issue points at a format spec in
stash-docs. That repository is empty — a README and a LICENSE. I took the naming rules fromCaptureNaming.swiftin the archived v2, which is where they and their reasoning actually live. Worth its own issue; I did not create one.Issue #5 stays open after this merges.