Skip to content

Start meetings on AirPods without crashing or failing when they switch to call mode - #1767

Merged
claude[bot] merged 12 commits into
mainfrom
claude/fix-airpods-start-tap-crash
Sep 23, 2026
Merged

claude[bot] merged 12 commits into
mainfrom
claude/fix-airpods-start-tap-crash

Conversation

@claude

@claude claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Before: starting a meeting with AirPods as the mic could crash the app. Opening the AirPods mic flips them from 48 kHz into their 24 kHz call mode. If that flip lands after we check the mic format but before we attach to the mic, macOS throws an error Swift can't catch, and the app dies. The first version of this PR stopped the crash, but on Justin's Mac the first AirPods start then failed every time with "The microphone route did not become ready". A second start worked because the AirPods were already in call mode.

After: the start checks the mic format again just before creating the mic file. If the AirPods have switched, it throws away the unstarted mic setup and builds a fresh one on the new format, up to twice, then records normally. The check right before attaching to the mic stays as a last safety net and fails cleanly instead of crashing. The mid-meeting mic recovery path has the same safety net.

The crash is also in 1.1.61 (same attach call), so it isn't new in 1.1.62. It was found and retested during Justin's local test run on 2026-09-23 (logs: graph at 48000, guard saw 24000 about 6 ms later).

How: settleMeetingInputGraphFormat re-reads the input node's format, using the same voice-processing state the graph was validated with (now carried on PreparedMeetingInputGraph). On a mismatch it discards the unstarted graph, waits 0.3 s, and calls makeReadyMeetingInputGraph again with the meeting's mic selection kept. startAudioCapture calls it before the mic file is created and adopts the rebuilt graph. ensureMicTapFormatStillMatches still runs under the graph lock right before both installTap calls, so a flip that lands in the last few milliseconds becomes a normal failed attempt.

Why

The app crashed at meeting start with AirPods, and after the first fix every first AirPods start failed. Both were seen on hardware.

Product Impact

  • Affects: meetings
  • Lane: meeting reliability
  • Why this matters: AirPods are a common meeting mic. The first start has to just work.

What changed

  • AudioDeviceRecovery.swift: added MicTapFormatPolicy, ensureMicTapFormatStillMatches (called before the recovery tap install) and settleMeetingInputGraphFormat.
  • AudioFileManager.swift: the start now settles the route before creating the mic file, and checks again before the tap install.
  • Audio.swift: PreparedMeetingInputGraph carries voiceProcessingEnabled.
  • Tests: AudioInitializationTests.testMicTapIsNotInstalledAfterAirPodsSwitchToTheirCallFormat, plus testMeetingStartSettlesTheMicRouteBeforeCreatingTheMicFile, a source-order check (settle, then mic file, then guard, then tap install).

How I checked it

  • scripts/dev/agent-preflight.sh
  • python3 scripts/dev/check-build-source-lists.py
  • bash build.sh --no-open (no Swift toolchain in this session; CI builds it)
  • bash run-tests.sh (CI)
  • bash run-integration-smoke.sh (CI)
  • swift test (CI)
  • Manual check, first version: on Justin's Mac the app stayed up and the start failed cleanly (the flip was seen at 48000 → 24000).
  • Manual check, this version: start a meeting with AirPods while they're not yet in call mode (e.g. play music on them first). It should record on the first try, and the log should show "Mic input format after route settled".

Risk Review

  • Privacy / local-first behavior reviewed (log fields are rates and channel counts only)
  • Storage path or migration impact reviewed (none; the mic file is created after the rebuild, at the settled rate)
  • Public-facing copy stays concrete and matches current product scope
  • Release/update impact reviewed (no version change)
  • Agent PRs link the issue/workpad and stay draft until human review
  • No private transcripts, audio, tokens, personal paths, or customer data are included

Notes

The rebuild runs after system audio capture has started. If the rebuild throws, the existing start-failure path stops everything, same as an engine.start() failure today. It adds 0.3 s to a start only when the format actually moved.

Agent handoff

COORD_DONE: BRIEF | this PR | rebuild the mic graph on the settled route at start + guard before both tap installs | none | none | preflight + source lists; CI pending | Justin retries a first AirPods start

🤖 Generated with Claude Code

https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka

…rmat

AirPods can drop from 48 kHz to their 24 kHz call profile between graph
validation and installTap, because opening their mic triggers the switch.
installTap then raises an Objective-C exception that Swift cannot catch,
so the app crashes at recording start. Re-check the input format under the
graph lock right before both tap installs (start and device recovery) and
turn a mismatch into a normal failed attempt instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
@claude
claude Bot requested a review from r3dbars September 23, 2026 16:25
@claude claude Bot mentioned this pull request Sep 23, 2026
11 of 13 tasks
On hardware the tap-install guard turned the crash into a failed start on
nearly every first AirPods start: the graph validates at 48 kHz, opening
the AirPods mic flips them to their 24 kHz call profile, and the start
then refused to install the tap. Before the mic file is created, re-read
the input format and, if it moved, discard the unstarted graph and build
a fresh one on the settled route (at most twice). The guard before
installTap stays as the last safety net.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
@claude claude Bot changed the title Stop meeting recording from crashing when AirPods switch format at start Start meetings on AirPods without crashing or failing when they switch to call mode Sep 23, 2026
On Justin's Mac a meeting that survived two lid-closes cleanly still put
up the "System audio interrupted" prompt after each wake, and saved as
degraded for device switches. The planned wake reconnect announced itself
exactly like a stall, and each wake counted twice (mic + system) toward
the three-switch degraded rule.

The tap's wake reconnect now publishes a new .systemWake recovery event
(arms the write hold like .deviceSwitch, but is not counted) and no
"reconnecting" message. The user still hears about it if the reconnect
fails or the tap stalls afterwards. The mic restart after wake is also
kept out of the device-switch count. Every sleep is still recorded as a
gap, so the capture quality still notes the interruption.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
An AirPods call-profile flip can also land during a mid-meeting mic
recovery (after a wake or a reconnect). The tap-install guard caught it,
but the recovery then stopped the meeting. Run the same settle-and-rebuild
step on the recovery graph before the recovery segment is sized.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
…ake' into claude/quiet-wake-reconnect

# Conflicts:
#	Sources/TranscriptedCore/Audio/CoreAudioSystemAudioCapture.swift
#	Tests/TranscriptedCoreTests/AudioTests/CoreAudioSystemAudioCaptureTests.swift
Both add a small policy enum at the same spot in AudioDeviceRecovery.swift;
keep both. Lets #1767 merge cleanly after #1771.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
With AirPods as the output, a tap and aggregate kept attached across sleep
came back delivering only digital zeros while a video played, and AirPods
playback stayed garbled until they were reconnected. Buffers kept arriving,
so the stall check never fired and nothing reconnected.

- Sleep now releases the tap and aggregate (keeping the queued tail), and the
  wake reconnect builds a fresh tap on the output the Mac woke with. If no
  wake notice comes, it rebuilds after 30s of awake time.
- For 5 minutes after a wake, a tap delivering only zeros for 3s while
  another process is running audio output is rebuilt, up to 3 times. Real
  signal ends the watch; zeros on a quiet Mac never trigger it.
- Every reconnect, the sleep release and capture failures are now logged, so
  the next hardware run shows what happened.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
- Wake: leave the meeting mic alone when it is still delivering. Every
  rebuild creates a fresh engine that first binds to the macOS default
  input; with AirPods as the default that flipped them into call mode
  and garbled playback after wake (confirmed on hardware).
- A callback landing after the tap's format listener fired no longer
  latches an overflow, and drain checks format change first, so a rate
  change (e.g. a call app opening the AirPods mic) reconnects instead of
  ending system audio. An overflow while the tap already reports a new
  format also reconnects.
- A failed tap rebuild after wake or a route change retries (1, 2, 4,
  8 s) instead of ending system audio on the first error.
- A lid closed again before the last wake's recovery ran: that stale
  recovery is skipped, the tap stays released, and the new sleep keeps
  its mic hold. The mic sleep hold now ends after 30 s of awake time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
- Call-audio tap (#1762/#1771): per-recording reconnect counts by cause
  (wake, format change, silent after wake, stall), rebuild retries,
  sleeps, silent-after-wake give-ups and why system audio ended. Bucketed
  on meeting events and allowlisted as Sentry tags.
- #1767: mic_format_rebuilds_bucket when AirPods flip format at start.
- #1768: meeting_system_audio_prompt_answered (outcome, tcc_status),
  plus mic_only_by_choice and system_permission_check on
  meeting_recording_started.
- #1773: launch_models_warmed once per launch; models_warm on
  meeting_recording_started.
- #1774: headset_mic_switched on dictation events.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
Justin chose to leave the AirPods dictation change out of 1.1.62
(decision card, 2026-09-23 19:45Z). Reverses #1774's diff
(main..d469c0e) on this branch only and drops its headset_mic_switched
tracking from the dictation events, allowlists, test, and docs.
#1767/#1771, #1768 and #1773 stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
@claude
claude Bot marked this pull request as ready for review September 23, 2026 20:00
@claude
claude Bot merged commit 7519672 into main Sep 23, 2026
7 checks passed
@claude
claude Bot deleted the claude/fix-airpods-start-tap-crash branch September 23, 2026 20:00
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
Keeps the in-place mic restart ahead of #1767's settled-format rebuild
in device recovery, and gives the in-place graph the new
voiceProcessingEnabled field (always false there).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZkqPnF73GpVNv9N5LYX9b
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