Skip to content

fix(audio): stabilize microphone capture - #856

Open
altic-dev wants to merge 6 commits into
mainfrom
B/fix-bluetooth-mic-stabilization
Open

fix(audio): stabilize microphone capture#856
altic-dev wants to merge 6 commits into
mainfrom
B/fix-bluetooth-mic-stabilization

Conversation

@altic-dev

Copy link
Copy Markdown
Owner

Description

Stabilizes Direct Core Audio microphone capture in two failure modes:

  • Keeps retrying the same Bluetooth microphone for a bounded five-second window while AirPods and similar devices settle into microphone mode, avoiding premature fallback or unavailable state.
  • Records once-per-second PCM health diagnostics and rebuilds a built-in microphone once when it delivered real signal and then produces sustained digital-zero PCM.

Healthy capture keeps the existing hotkey-to-first-PCM path. This does not reorder microphone priorities or add a recording delay.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Addresses #852. Bluetooth stabilization follows customer AirPods route-switching reports.

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 27.0 (26A5368g)
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran formatter locally: swiftformat --config .swiftformat Sources
  • Ran tests locally

Release validation completed with the private FI Release build: universal app compilation, install and launch, strict FI provenance, code signing, app/DMG notarization and stapling, Gatekeeper assessment, ZIP/DMG validation, and the private FI release gate.

Screenshots / Video

  • No UI/visual changes; screenshots/video are not applicable.

Notes

The silent-PCM recovery is intentionally conservative: built-in Direct Core Audio only, after real signal was observed, after three near-zero one-second windows, and at most once per recording. The artifact is universal; real Intel runtime remains unvalidated without an Intel/Rosetta dictation smoke test.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Greptile Summary

The PR stabilizes direct Core Audio microphone capture by adding bounded same-device Bluetooth retries, deferred route reconciliation, and conservative recovery from sustained digital-zero PCM.

  • Adds a five-second admission window for retrying a settling Bluetooth microphone.
  • Coordinates startup-time route invalidation and deferred reconciliation.
  • Records one-second PCM health windows and allows one internal-microphone recovery per recording.
  • Adds policy-level integration tests for Bluetooth retry, deferred recovery, microphone classification, and silent-PCM handling.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up-review scope.

No blocking failure remains.

Reviews (4): Last reviewed commit: "fix(audio): close bluetooth recovery rac..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 405e0332f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Fluid/Services/ASRService.swift Outdated
Comment on lines +3120 to +3121
self.benchmarkLog("bluetooth_start_route_change_deferred event=\(reason)")
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve reconciliation requests during Bluetooth startup

When a settings restore or input-topology change occurs while a Bluetooth microphone is starting, this unconditional return discards the request, including reconcilesInputSelection and requiresIdlePrewarm. If the existing Bluetooth attempt subsequently delivers first PCM, startup completes without another reconciliation, so the recording can continue on the stale device despite restored priorities or a newly resolved higher-priority input. Defer the hardware rebuild if necessary, but retain and process reconciliation requests after startup settles.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 77f3573. Ordinary Bluetooth profile churn remains suppressed, but explicit prewarm/reconciliation work is now coalesced and replayed after capture startup settles. Added regression coverage for preservation, merged flags, one-shot consumption, and the ordinary-churn non-effect.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 77f3573 and refined in d3f9461 to preserve the original AirPods behavior. Explicit priority/topology work is retained during Bluetooth startup, then evaluated after startup settles using an off-main device snapshot. If the resolved UID and Core Audio device identity still match the active input, it is a logged no-op with no pipeline stop or rebuild; only a genuinely changed selection/identity enters the existing recovery path. Regression coverage now asserts the unchanged-active non-effect, changed UID/identity recovery, and idle-prewarm intent.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

FluidVoice PR build ready

Download FluidVoice-PR-856-9d2795666d6e

The artifact contains the ad-hoc-signed app ZIP, Xcode archive, build manifest, and installation instructions. It expires 5 days after the build.

Install the app

  1. Extract the downloaded artifact, then extract FluidVoice-PR-856.app.zip.

  2. Move FluidVoice fix(audio): stabilize microphone capture #856.app into the /Applications folder.

  3. Open Terminal and remove the download quarantine marker:

    xattr -dr com.apple.quarantine "/Applications/FluidVoice #856.app"
    
  4. In Applications, Control-click FluidVoice fix(audio): stabilize microphone capture #856.app and choose Open.

  5. If macOS still blocks it, open System Settings → Privacy & Security, click Open Anyway, and confirm.

This build has its own app identity, so its permissions are separate from the release version of FluidVoice.

View workflow run

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77f357301f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Fluid/Services/ASRService.swift Outdated
Comment on lines +2163 to +2164
if let deferredRecovery = self.deferredBluetoothStartupRouteRecovery.take() {
self.scheduleAudioRouteRecovery(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid replaying handled Bluetooth invalidations after startup

When a Bluetooth format invalidation occurs during startup, handleDirectCaptureFormatInvalidation both wakes the bounded retry loop and preserves a request via requiresIdlePrewarm; if that retry then delivers first PCM, this unconditional replay schedules an active-route recovery because isRunning is now true. scheduleAudioRouteRecovery immediately disables the capture pipeline, so a successfully stabilized microphone is stopped again just after onCaptureStarted, losing opening audio and handing the device to the older, non-Bluetooth-aware recovery loop. Replay explicit reconciliation work, but discard a hardware invalidation that the successful startup retry already handled.

Useful? React with 👍 / 👎.

Comment thread Sources/Fluid/Services/ASRService.swift Outdated
self.audioStartAttemptInputUID = device.uid
self.audioStartAttemptInputName = device.name
self.audioStartAttemptIsBluetooth = device.isBluetooth
self.audioStartAttemptIsBuiltIn = device.isBuiltIn

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict the watchdog to the internal microphone

When an analog headset is connected, Core Audio reports it with kAudioDeviceTransportTypeBuiltIn, so AudioDevice.isBuiltIn is also true for that external microphone; AudioDeviceService.swift:58-61 already distinguishes this case using the input data source. Assigning that broader property here makes the new silent-PCM watchdog rebuild analog-headset capture after three zero windows—for example, after using a headset mute—even though the recovery is intended only for the internal microphone.

Useful? React with 👍 / 👎.

@altic-dev altic-dev Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9d27956. The watchdog now uses the existing internal-microphone classification, which excludes the built-in transport when Core Audio reports the external microphone data source. Added coverage proving an analog headset remains external and cannot enter this recovery path.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3f94613cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"bluetooth_start_route_change_deferred event=\(reason) " +
"reconciliationPreserved=\(preserved)"
)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve post-readiness Bluetooth invalidations

When a format invalidation arrives after signalFirstPCM has fixed readiness to .ready but before start() sets isRunning, this branch still classifies it as Bluetooth startup churn. handleDirectCaptureFormatInvalidation has already disabled audioCapturePipeline, but the ordinary request is not preserved and this return does not advance the route generation, so routeStayedStable remains true and startup reports success with sampling disabled. Fresh evidence beyond the earlier handled-retry case is that AudioCaptureReadinessGate.finish ignores later invalidations once its result is .ready, leaving this post-readiness window unhandled; retain/replay this invalidation or make the readiness check observe it.

Useful? React with 👍 / 👎.

@altic-dev altic-dev Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9d27956 without replaying recovery after a healthy start. A direct-format invalidation during Bluetooth startup now advances the route generation, so the readiness result is treated as unstable even if first PCM won the race. The owning bounded startup loop retries the same Bluetooth input; ordinary profile churn still does not rebuild or delay healthy capture. Policy coverage includes retry, preserved reconciliation, and no-op cases.

self.inputUID = inputUID
self.startedAt = now
}
return self.elapsed(at: now) < Self.maximumDuration

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the full five-second Bluetooth retry budget

When a failure occurs shortly before the five-second deadline, this check permits another retry without limiting that attempt: startup can then spend 300 ms waiting for topology quiet and the full two-second first-PCM timeout. A retry approved at 4.9 seconds can therefore block until roughly 7.2 seconds after the first failure, and the initial attempt is excluded because startedAt is only set here. This defeats the advertised bounded five-second stabilization window and delays fallback or error feedback; cap subsequent waits by the remaining budget or avoid starting an attempt that cannot fit.

Useful? React with 👍 / 👎.

@altic-dev altic-dev Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Clarified in 9d27956; intentionally not changed to a hard wall-clock cutoff because that would reduce the AirPods stabilization window this PR is fixing. Five seconds is the retry-admission window: no new same-device retry begins after it, but an already admitted attempt keeps its normal readiness timeout rather than being cancelled while the route may be settling. Renamed the policy and log field and documented this explicitly.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d2795666d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

)
self.audioStartAttemptInputUID = device.uid
self.audioStartAttemptInputName = device.name
self.audioStartAttemptIsBluetooth = device.isBluetooth

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain Bluetooth identity before resolving the UID

When a selected Bluetooth endpoint disappears between directCoreAudioDeviceSelection and resolveDevice during startup topology churn, resolution throws before this assignment records that the attempt was Bluetooth. Consequently, BluetoothInputStabilization.shouldRetry receives isBluetoothInput: false; the first failure only gets the generic retry, and a second failure after the 300 ms topology wait excludes the AirPods UID and falls back instead of honoring the five-second stabilization window. Preserve the selected device's Bluetooth classification before re-resolving its unstable UID so these resolution failures enter the bounded Bluetooth retry path.

Useful? React with 👍 / 👎.

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