DEV Community

SleepTrace
SleepTrace

Posted on

Field notes: why background audio on iOS fails silently at 3am

Field notes: why background audio on iOS fails silently at 3am

If you have ever wondered why your overnight audio app "just stops" after three hours, you have met the iOS background execution budget. The phone does not kill rogue audio apps with an alert. It quietly throttles the process until the audio queue starves and your ring buffer fills with zeros. By morning, your detection looks like a flat line that never happened.

Here is what the documentation does not emphasize and the simulator hides.

The assertion that actually holds it together

AVAudioSession.sharedInstance().setCategory(.record, mode: .measurement, options: [.duckOthers, .interruptSpokenAudioAndSpeechAndReading) is the opening move, not the whole story. You also need a background task assertion that is renewed before it expires. A single beginBackgroundTask request at launch covers ~30 seconds. Overnight, you must renew it inside the expiry handler or the OS reclaims the time budget and your audio queue stalls.

The queue that survives a budget reclaim

Most audio apps use AVAudioEngine with a tap on the input node. That is simple and correct until iOS decides to throttle. The more robust pattern — the one that survives a budget squeeze — is an AVAudioInputNode pull rendering tap combined with a manually-managed ring buffer. Pulling (rather than pushing into a delegate) means you only allocate when you are actually permitted to run. When the OS tightens the budget, the pull simply returns silence until the next allowed window. Your buffer stays valid, and you can detect the "silent gap" as a data-quality flag rather than a crash.

Clock drift kills the timestamps, not the audio

The quiet killer in overnight field work is not a crash. It is clock drift between the audio timestamp (CMTime from the engine) and wall-clock time (Date). Eight hours of 20ms windows means the timestamps diverge by enough that correlating snoring events with "what happened at 01:12" silently fails. The fix is to anchor the audio clock to CACurrentMediaTime at the start of the session and re-anchor after any interruption. We document the exact drift correction used in production, including the code.

The flat-line detector

The most useful production line you can add:

if recentBuffer.maxAmplitude == 0 for more than 180 windows:
    scheduleBufferCheck()   // OS likely throttled the queue
    log("silent stretch", confidence: .low)
Enter fullscreen mode Exit fullscreen mode

It converts a mysterious 3am failure into an observable signal quality problem. The morning report then says "confidence low: possible OS throttling at 02:40–03:12" instead of silently presenting a flat night as "quiet sleep."

The lesson: overnight iOS audio is less about capturing every sample and more about detecting when the capture was denied, then reporting honesty about it.

Top comments (0)