Skip to content

feat(apple): make SpokenInstructionObserver's audio session management opt-out - #960

Open
meme-te wants to merge 1 commit into
stadiamaps:mainfrom
meme-te:feat/optional-audio-session
Open

feat(apple): make SpokenInstructionObserver's audio session management opt-out#960
meme-te wants to merge 1 commit into
stadiamaps:mainfrom
meme-te:feat/optional-audio-session

Conversation

@meme-te

@meme-te meme-te commented Sep 9, 2026

Copy link
Copy Markdown

Problem

SpokenInstructionObserver always takes over the shared AVAudioSession around each spoken
instruction. That is the right default for apps using the built-in AVSpeechSynthesizer, but it
permanently strands other apps' audio for apps that inject a custom SpeechSynthesizer playing
through their own session.

The mechanism:

  • requestAudioFocus() sets .duckOthers + .interruptSpokenAudioAndMixWithOthers and
    mode = .voicePrompt, then activates the session.
  • releaseAudioFocus() clears hasAudioFocus only after setActive(false) succeeds.
  • setActive(false) fails while the session still has active audio I/O.

An app that keeps a microphone tap open (wake-word standby) or plays its own guidance audio
therefore never releases focus: hasAudioFocus stays true, every later requestAudioFocus()
early-returns on guard !hasAudioFocus, and other apps stay ducked for the rest of the session.

We measured this on a device with a Bluetooth headset: background music dropped from 0.80 to
0.30 system output volume about one second after navigation started, and never recovered.

Recovering from the host app does not work either. Re-applying setCategory on an already-active
session interrupts other apps' playback, so "take the session back" trades a stuck duck for
repeated audio dropouts (we tried it, and it was worse).

There is also no way to opt out today: FerrostarCore takes spokenInstructionObserver as a
non-optional concrete SpokenInstructionObserver, and the class is public but not open, so it
can be neither replaced nor subclassed from another module.

Change

Adds managesAudioSession to SpokenInstructionObserver, defaulting to true so existing
behavior is unchanged
. Apps that own their audio session can pass false:

SpokenInstructionObserver(synthesizer: mySynthesizer, isMuted: false, managesAudioSession: false)

One file, guarding the three audioManager calls. No change to instruction de-duplication or any
other logic.

Testing

Built and run on device (iPhone, Bluetooth A2DP headset) with managesAudioSession: false:
background music keeps playing across app launch and repeated microphone on/off cycles, and the
app's own spoken guidance still plays through its session.

…t opt-out

`SpokenInstructionObserver` currently always takes over the shared `AVAudioSession`
around each spoken instruction. That is the right default for apps using the built-in
`AVSpeechSynthesizer`, but it strands other apps' audio for apps that inject a custom
`SpeechSynthesizer` playing through their own session.

Why it strands:

  * `requestAudioFocus()` sets `.duckOthers` + `.interruptSpokenAudioAndMixWithOthers`
    and `mode = .voicePrompt`, then activates the session.
  * `releaseAudioFocus()` clears `hasAudioFocus` only *after* `setActive(false)` succeeds.
  * `setActive(false)` fails while the session still has active audio I/O.

An app that keeps a microphone tap open (wake-word standby) or plays its own guidance
audio therefore never releases focus: `hasAudioFocus` stays `true`, subsequent
`requestAudioFocus()` calls early-return, and other apps stay ducked for the rest of
the session. We measured this on-device: background music dropped from 0.80 to 0.30
system output volume ~1s after navigation started, and never recovered.

Recovering from the host app is not a workable fix either. Re-applying `setCategory`
on an already-active session interrupts other apps' playback, so "take the session
back" trades a stuck duck for repeated audio dropouts.

This adds `managesAudioSession` (default `true`, so existing behavior is unchanged) so
that a host app which owns its audio session can opt out.
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.

1 participant