Skip to content

fix(audio): give Bluetooth headsets time to settle during microphone recovery - #129

Closed
trsdn wants to merge 1 commit into
mainfrom
fix/bluetooth-recovery-settling
Closed

trsdn wants to merge 1 commit into
mainfrom
fix/bluetooth-recovery-settling

Conversation

@trsdn

@trsdn trsdn commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

With AirPods Max set as the System Default input, dictation failed on 2026-10-13. The device summary added in 1.7.3 made the cause visible in the log:

capture generation 3 failed: ... The audio engine stopped repeatedly during microphone recovery. [selection=system-default access=authorized inputs=3 default=true transport=bluetooth virtual=false channels=1 rate=24000 alive=yes busy=no]

Generation 3 and 4 each ran three recovery attempts about 0.9 s apart, then gave up. A Bluetooth headset switches to its microphone profile when capture starts, which can change the format several times over a few seconds. The recovery gave up after three attempts with a fixed 250 ms wait, and the attempt count was never reset during a recording.

Changes:

  • New MicrophoneRecoveryPolicy: up to 6 attempts, with the wait growing 250 ms → 1.5 s (about 5.25 s of settling in total, instead of about 0.75 s).
  • Attempts are counted per burst: once a recovered engine runs for 3 s without another change, the count starts again.
  • Every configuration decision is now logged at notice level (persisted) with the engine state, attempt count, and the device's transport, channels, and sample rate; never the name or UID.

Related issue

Maintainer report: "AirPods Max auf System Default und es geht nicht mehr". Follows #119 and #114 (microphone errors), which the 1.7.3 diagnostics were added for. Does not close them.

Prepared with an AI assistant using Copilot SDK in VS Code.

Validation

  • swift build -c release -Xswiftc -warnings-as-errors, swiftlint lint --strict, and swift test succeed locally
  • For audio, hotkey, paste, overlay, or update changes: I ran the built app and checked the affected flow

How the change was verified (the tests cover pure logic only, so say what you tried by hand for UI, audio, hotkey, or update changes):

  • swift build -c release -Xswiftc -warnings-as-errors: passed. swiftlint lint --strict: 0 violations. swift test: 104 tests passed, 6 new for the policy (delay growth and cap, total settling time at least 5 s, attempt limit, a stable engine starts a fresh burst, an unstable one keeps its count) and for the device format summary (never contains a name or UID).
  • I could not reproduce the failure. With the AirPods Max as default input, the app's real AudioEngine captured correctly in five scenarios run through a temporary test (deleted afterwards): cold start, three rounds with 5 s gaps, a playback stream running at 1 % volume, and start/stop chimes at the moment of capture. None triggered a configuration-change loop. The root cause of the loop is therefore not proven; this change addresses the observed symptom (too little settling time) and adds the logging needed to find the cause if it persists.
  • Whether this fixes the failure on the maintainer's machine is not yet confirmed.

Impact

  • User or operational risk: a failing microphone can now take up to about 5 s longer to report an error, because recovery keeps trying. Dictation on devices that settle within that time now succeeds instead of failing. No change for devices that do not trigger configuration changes.
  • Security or privacy impact (permissions, network destinations, stored or transmitted data): none. The new log lines contain only engine state and device transport, channel count, and sample rate.
  • Compatibility or migration impact (macOS versions, saved preferences, update path): none.

Documentation

  • README, docs, and changelog are updated where the change is user-visible (changelog; no README change needed)

…recovery

AirPods Max switch profile when capture starts and can change format several times. Recovery gave up after three attempts 0.9 s apart. Allow six attempts with growing waits, count them per burst of changes, and log each configuration decision with the device transport and sample rate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 09:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The debounce period can incorrectly reset the recovery count and bypass the six-attempt burst limit.

2 open findings
What changed in this PR

Improves Bluetooth microphone recovery by extending retries, adding burst-based accounting, and expanding privacy-safe diagnostics.

Changes:

  • Adds configurable recovery backoff and stability policy.
  • Logs device format and recovery state.
  • Adds policy tests and release notes.
File Description
Sources/​OpenWritr/​AudioEngine.swift Integrates recovery policy and diagnostics.
Sources/​OpenWritr/​MicrophoneRecoveryPolicy.swift Defines retry and stability behavior.
Sources/​OpenWritr/​AudioDiagnostics.swift Adds privacy-safe format summaries.
Tests/​OpenWritrTests/​MicrophoneRecoveryPolicyTests.swift Tests policy and diagnostic output.
CHANGELOG.md Documents user-visible changes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

return
}

let attempts = countedRecoveryAttemptsOnQueue()
Comment thread CHANGELOG.md
## [1.7.4] — 2026-10-13

### Fixed
- Dictation with Bluetooth headsets such as AirPods Max no longer fails after the audio engine restarts a few times while the headset switches to its microphone profile. The microphone recovery now waits longer between attempts, allows more of them, and counts them per burst of changes instead of per recording

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No high-confidence actionable HIG defects were found.

The PR diff changes only audio recovery logic (AudioEngine.swift, AudioDiagnostics.swift, new MicrophoneRecoveryPolicy.swift). It touches no SwiftUI/AppKit views, colors, text styles, motion, or accessibility metadata. The new log line records only transport, channel count and sample rate, and never the device name or UID, so I found no privacy defect.

Rendered surfaces and variants reviewed:

  • Settings: light, dark, light with larger accessibility text
  • About: light, dark, dark with larger accessibility text
  • Overlay, light and dark for each state: listening, transcribing, enhancing, done, copied, clipboard, enhanced-clipboard, error

I found no visible defect in these renders that this PR introduces or exposes.

Generated by Apple HIG visual review for #129 · copilot · auto · 16.8 AIC · ⌖ 0.496 AIC · ⊞ 7.8K

@trsdn

trsdn commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #130 (released in v1.7.4), which replaces the AVAudioEngine capture instead of waiting longer for it to recover. The branch is left in place.

@trsdn trsdn closed this Oct 13, 2026
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.

2 participants