Skip to content

Fix alarm sound session activation failures in background#596

Open
bjorkert wants to merge 4 commits intodevfrom
fix/alarm-audio-session-activation
Open

Fix alarm sound session activation failures in background#596
bjorkert wants to merge 4 commits intodevfrom
fix/alarm-audio-session-activation

Conversation

@bjorkert
Copy link
Copy Markdown
Member

@bjorkert bjorkert commented Apr 9, 2026

Fixes #590Unable to play alarm: Session activation failed.

In background without Silent Tune holding the audio session alive, iOS denies AVAudioSession.setActive(true) with the legacy non-mixable options: [] (cannotInterruptOthers, 560557684). The alarm goes silent.

Result

AlarmSound now picks its session options from a fallback ladder and logs each attempt:

  1. [] — non-mixable, dominates other audio (preferred)
  2. .duckOthers — mixable, ducks other audio
  3. .mixWithOthers — mixable, no ducking (last resort)

When the app is in background without Silent Tune, step 1 is skipped (iOS will always deny it there) and the ladder starts at .duckOthers.

Each session activation logs its outcome with the iOS error code, so future regressions or unexpected denials are diagnosable from a user's log.

Use .duckOthers instead of empty options when configuring the audio
session for alarm playback. The empty options created a non-mixable
session that conflicted with the background silent audio player
(which uses .mixWithOthers), causing setActive(true) to fail with
"Session activation failed" when the app was in the background.
@bjorkert bjorkert marked this pull request as draft April 26, 2026 08:39
@bjorkert
Copy link
Copy Markdown
Member Author

Changed it to draft, needs more testing.

Limit the .duckOthers option to the only state where legacy options: []
fails: background without Silent Tune holding a mixable session alive.
In foreground or with Silent Tune, restore options: [] so the alarm
continues to dominate other audio with no behavioral change for those
users.

In that same fail-prone state, plumb the alarm's soundFile through
AlarmManager.sendNotification so the system-delivered notification
carries the user's configured alarm sound as an audible fallback. In
other states the notification keeps .default to avoid an echo with the
in-app AVAudioPlayer loop.
@bjorkert bjorkert marked this pull request as ready for review April 29, 2026 14:22
@marionbarker
Copy link
Copy Markdown
Collaborator

Test

✅ successful test

Demonstrate the problem

Build using dev: 6.0.9, c9f74f4

  • configured LoopFollow phone with a RileyLink for background refresh
  • enable high and low alerts configured so I should get an alert and lock the phone
  • wait after CGM update - no alert sound
  • unlock phone, check alert status - see an alert but did not hear one
  • wait while app is open - alarm sounds

Demonstrate this PR fixes the problem

Build using fix/alarm-audio-session-activation, 54c2a5d

  • enable high and low alerts configured so I should get an alert and lock the phone
  • wait after CGM update - ✅ alert sounds even though phone is locked.

Copy link
Copy Markdown
Collaborator

@marionbarker marionbarker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approve by test

Copy link
Copy Markdown
Collaborator

@dnzxy dnzxy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .duckOthers gating is a reasonable fix for cannotInterruptOthers, but I don’t think the notification path is safe as written. Since the notification with custom sound is scheduled before in-app playback is attempted, it can double-play when .duckOthers succeeds.

I'd suggest to please either make the notification sound truly conditional on playback failure, or intentionally make the background/no-Silent-Tune path notification-only.

We could also consider using applicationState == .background instead of != .active, and centralize the duplicated policy logic.

Comment thread LoopFollow/Alarm/Alarm.swift Outdated
}()

AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze")
// When backgrounded without Silent Tune holding a session alive, the in-app
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't something like potentially safer to avoid unintended duplicate plays?

let playbackStarted = AlarmSound.play(...)

if !playbackStarted && inBackgroundWithoutSilentTune {
    AlarmManager.shared.sendNotification(
        title: type.rawValue,
        actionTitle: ...,
        soundFile: soundFile
    )
} else {
    AlarmManager.shared.sendNotification(
        title: type.rawValue,
        actionTitle: ...,
        soundFile: nil
    )
}

Replace the static .duckOthers/[] choice with a fallback ladder that tries
options in order [] → .duckOthers → .mixWithOthers and stops at the first
that activates. In background without Silent Tune the [] candidate is
skipped, since iOS denies it there (cannotInterruptOthers, 560557684).
Each attempt is logged with the iOS error code so failures are visible
in the field.

Revert the notification soundFile path; notifications stay on .default
and the in-app AVAudioPlayer remains the only source of the alarm tone.
Also drops the redundant enableAudio() call from play() — the do-block
already activates the session.
@bjorkert
Copy link
Copy Markdown
Member Author

Thanks for testing and feedback. Code and PR description updated. Tested successfully with RileyLink and SilentTune.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants