feat(desktop): microphone picker with Ray-Ban Meta glasses support#9181
feat(desktop): microphone picker with Ray-Ban Meta glasses support#9181eulicesl wants to merge 9 commits into
Conversation
Ray-Ban Meta glasses pair to the Mac as a Bluetooth HFP microphone; the desktop app captured only from the system-default input with no way to pick a device or see which one is live. Now: - Settings -> Transcription gains a Microphone card: System Default or any input device, enumerated via the same CoreAudio HAL walk as findBuiltInMicDeviceID; persisted by device UID (IDs aren't stable across reconnects) and resolved at capture start via the existing AudioCaptureService(overrideDeviceID:) path. - Meta glasses are detected by precise product-name match (Ray-Ban / Oakley Meta — never generic 'glass') and labeled, with the HFP voice-quality tradeoff stated in plain language. - The Dashboard Listening button shows 'Ray-Ban Meta' while the glasses are the active capture source (recordingInputDeviceName already feeds input_device_name on memories, so provenance lands server-side too). - Silent-mic watchdog behavior is unchanged: a dead Bluetooth mic still falls back to the built-in mic so capture never silently records nothing. - Neutral styling (no purple) per design rules. Verified: xcrun swift build -c debug --package-path Desktop — zero errors (after merging upstream b4370fc, which fixes a pre-existing AgentBridge break at this branch's original base). On-device run via named bundle next.
On-hardware finding: Ray-Ban Meta glasses advertise a Bluetooth codename (observed: 'EL AI 000F'), not a Meta product name, so product-name matching alone can't identify them. When the user has explicitly chosen a microphone, show that device's real name on the Listening button (still 'Ray-Ban Meta' when the name does match a Meta product). Verified on hardware: glasses selected in the picker captured live audio end-to-end to transcript.
With the Settings microphone picker pinning transcription capture to a Bluetooth mic (e.g. Ray-Ban Meta glasses), push-to-talk could open a second CoreAudio IOProc against the same device — or join its A2DP-HFP profile flap — racing both captures' stream-format reconfiguration. PTT's existing guard only considered Bluetooth output. - AudioCaptureService gains a process-wide active-capture registry (register on successful start/reconfigure, unregister on stop). - preferredPTTInputOverrideDeviceID() now also yields to the built-in mic when the would-be input is held by another capture, or is Bluetooth while any capture is live — mirroring the existing isDefaultOutputBluetooth guard's philosophy. Verified: xcrun swift build — zero errors; behavior check on hardware (glasses Listening + chat shortcut concurrently) is the follow-up step.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
Adds a user-selectable microphone input for macOS transcription (including Ray-Ban Meta glasses), surfaces the live capture source in the Listening UI, and introduces a process-wide “active capture” registry to reduce push-to-talk (PTT) contention with ongoing transcription capture.
Changes:
- Add a Settings → Transcription “Microphone” card to pick System Default vs a specific input device (persisted by device UID).
- Show the selected/live capture source name on the Listening button (with a “Ray-Ban Meta” label when name-matched).
- Add an active-capture registry in
AudioCaptureServiceand extend PTT mic override logic to yield to the built-in mic under contention/Bluetooth conditions.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/SettingsContentView+Transcription.swift | Inserts the new microphone picker card into the Transcription settings section. |
| desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift | New SwiftUI card that enumerates input devices and persists the preferred device UID. |
| desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift | Updates Listening button title to reflect capture source / Meta labeling. |
| desktop/macos/Desktop/Sources/FloatingControlBar/PushToTalkManager.swift | Extends PTT override selection to avoid active-capture and Bluetooth contention. |
| desktop/macos/Desktop/Sources/AudioCaptureService.swift | Adds input-device enumeration helpers + active-capture registry APIs. |
| desktop/macos/Desktop/Sources/AppState/AppState+Transcription.swift | Initializes AudioCaptureService with an override device ID when a preferred UID is set. |
| desktop/macos/changelog/unreleased/20260707-ptt-mic-contention.json | Changelog fragment for PTT contention fix. |
| desktop/macos/changelog/unreleased/20260706-rayban-meta-microphone-picker.json | Changelog fragment for microphone picker + capture source display. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private func registerActiveCapture(deviceID: AudioDeviceID) { | ||
| Self.activeCapturesLock.lock() | ||
| Self.activeCaptures[ObjectIdentifier(self)] = deviceID | ||
| Self.activeCapturesLock.unlock() | ||
| } |
| /// True when any capture is currently running in this process. | ||
| static func hasActiveCapture() -> Bool { | ||
| activeCapturesLock.lock() | ||
| defer { activeCapturesLock.unlock() } | ||
| return !activeCaptures.isEmpty |
| Self.activeCapturesLock.unlock() | ||
| } | ||
|
|
||
| /// True when another live capture already holds this device. |
| // Surface the explicitly-chosen mic as the visible capture source. Meta | ||
| // glasses advertise a Bluetooth codename (e.g. "EL AI 000F"), so product- | ||
| // name matching alone can't identify them — an explicit user selection is | ||
| // the reliable signal, and showing its real name stays honest either way. |
There was a problem hiding this comment.
7 issues found across 8 files
Confidence score: 2/5
- In
desktop/macos/Desktop/Sources/AudioCaptureService.swift, the active-capture registry can be cleared before asyncAudioDeviceStop/destroy finishes, and it may never be cleared if an instance deallocates while still capturing; that can let a new PTT/capture start against a device still shutting down or leave stale ownership behind. Keep the registry entry until stop/destroy completion and ensure deinit/teardown always unregisters before merging. - In
desktop/macos/Desktop/Sources/AudioCaptureService.swift(reconfigureAfterChangepath), contention checks can keep using the old device ID after a live capture switches inputs, which can incorrectly block or allow capture on the wrong microphone. UpdateactiveCaptureswithnewDeviceIDafter a successful restart to keep PTT arbitration accurate. - In
desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift, callinglistInputDevices()on the main actor from.onAppearrisks UI stalls during CoreAudio/HAL transitions (for example, Bluetooth discovery). Run device enumeration off the main thread and marshal results back to the UI. - In
desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swiftanddesktop/macos/Desktop/Sources/AudioCaptureService.swift, the device list is only loaded once and the new preference key bypassesDefaultsKey, so users can miss newly connected mics and future key drift can desync reads/writes. Add a device-change refresh path and route the new key throughDefaultsKeybefore merge.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/macos/Desktop/Sources/AudioCaptureService.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:349">
P1: A PTT or second capture started right after `stopCapture()` can see no active capture while the old IOProc is still being stopped asynchronously. Keeping the registry entry until `AudioDeviceStop`/`AudioDeviceDestroyIOProcID` completes would preserve the contention guard during teardown.</violation>
<violation number="2" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:480">
P2: The new microphone preference key bypasses the repository's `DefaultsKey` source of truth, so future reads/writes can drift by string typo. Consider adding `preferredMicrophoneDeviceUID` to `DefaultsKey` and exposing this constant from that case's `rawValue` for `@AppStorage` call sites.
(Based on your team's feedback about keeping UserDefaults keys in `DefaultsKey`.) [FEEDBACK_USED]</violation>
<violation number="3" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:489">
P2: PTT contention checks can use a stale captured device after a live capture reconfigures to a new input. Consider refreshing `activeCaptures` with `newDeviceID` after the successful restart path in `reconfigureAfterChange`.</violation>
<violation number="4" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:503">
P2: The doc comment says "True when **another** live capture already holds this device," but because this is a `static` method with no instance context, it returns `true` if *any* capture (including the caller's own) holds the device. The current call site in `PushToTalkManager` happens to be safe because PTT doesn't use `AudioCaptureService`, but the mismatch makes the API misleading. Either update the doc to say "any capture" or add an `excluding: ObjectIdentifier?` parameter to truly filter out self.</violation>
<violation number="5" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:511">
P1: Registry entries are only removed via `unregisterActiveCapture()` in `stopCapture()`. If an `AudioCaptureService` instance is deallocated while `isCapturing` is still true (the class reportedly has a `deinit` cleanup path for that case), its entry leaks in `activeCaptures`. This permanently makes `hasActiveCapture()` return `true` and poisons PTT routing. Adding `unregisterActiveCapture()` to `deinit` would prevent this leak.</violation>
</file>
<file name="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift:51">
P2: `listInputDevices()` performs a blocking CoreAudio HAL walk (sync IPC to coreaudiod), but it's called directly on the main actor from `.onAppear {}`. When Bluetooth devices are in discovery or transitioning, HAL queries can stall long enough to cause UI stutter. Consider dispatching to a background queue and updating `devices` via a published property or async task.</violation>
<violation number="2" location="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift:51">
P2: `devices` is fetched once on `.onAppear` but never refreshed when audio input devices connect or disconnect. Users who open Settings before pairing their Ray-Ban Meta glasses won't see them in the list until the view reappears. Consider subscribing to CoreAudio property-change notifications or re-enumerating on a timer/view-lifecycle trigger so the picker reflects live hardware changes.</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
| } | ||
|
|
||
| /// True when any capture is currently running in this process. | ||
| static func hasActiveCapture() -> Bool { |
There was a problem hiding this comment.
P1: Registry entries are only removed via unregisterActiveCapture() in stopCapture(). If an AudioCaptureService instance is deallocated while isCapturing is still true (the class reportedly has a deinit cleanup path for that case), its entry leaks in activeCaptures. This permanently makes hasActiveCapture() return true and poisons PTT routing. Adding unregisterActiveCapture() to deinit would prevent this leak.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/AudioCaptureService.swift, line 511:
<comment>Registry entries are only removed via `unregisterActiveCapture()` in `stopCapture()`. If an `AudioCaptureService` instance is deallocated while `isCapturing` is still true (the class reportedly has a `deinit` cleanup path for that case), its entry leaks in `activeCaptures`. This permanently makes `hasActiveCapture()` return `true` and poisons PTT routing. Adding `unregisterActiveCapture()` to `deinit` would prevent this leak.</comment>
<file context>
@@ -384,6 +386,134 @@ class AudioCaptureService: @unchecked Sendable {
+ }
+
+ /// True when any capture is currently running in this process.
+ static func hasActiveCapture() -> Bool {
+ activeCapturesLock.lock()
+ defer { activeCapturesLock.unlock() }
</file context>
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks for the focused desktop microphone-picker work here — the shape makes sense for Omi’s desktop transcription/smart-glasses use case, especially persisting CoreAudio UIDs rather than device IDs and reusing the explicit AudioCaptureService(overrideDeviceID:) path.
I’m going to hold this before merge for one concrete reason: the required Desktop Swift Build & Tests check is failing in the e2e flow coverage gate, not in compilation. The failing job reports these changed Swift files still need e2e flow coverage entries:
desktop/macos/Desktop/Sources/AppState/AppState+Transcription.swiftdesktop/macos/Desktop/Sources/FloatingControlBar/PushToTalkManager.swiftdesktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swiftdesktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/SettingsContentView+Transcription.swift
Please add/extend the relevant desktop/macos/e2e/flows/*.yaml coverage for the Settings → Transcription microphone picker and the PTT/mic-contention path (or otherwise satisfy the strict coverage checker), then rerun the desktop check.
A couple of maintainer notes for the human pass:
- Product fit looks qualitatively reasonable: a desktop microphone picker and explicit Ray-Ban Meta capture indication are aligned with Omi’s smart-glasses/transcription surface.
- This is still a nontrivial feature PR touching capture routing, Bluetooth/HFP behavior, dashboard UX, and PTT contention, so it should get human maintainer review after CI is green rather than an automated approval.
- I did not find an agent-instruction or AI-reviewer behavior change in this PR.
Once the coverage gate is fixed, I’d expect this to be a good candidate for a final desktop maintainer review with hardware/UX confirmation.
beginTurn() refreshes the voice seed asynchronously and then calls reconnectWarmSessionIfSeedStale(). When the seed fetch is slow (~5s observed), the check lands after a short turn has already committed and teardownSession() destroys the session the turn is awaiting its reply on — the reply silently never arrives. Observed on hardware (glasses Listening + chat shortcut): 'turn committed' at t+0ms, 'voice seed changed — reconnecting warm session' at t+13ms, no reply. Guard the reconnect on inputTurnInProgress/responding and defer to the next turn's check. Verified: xcrun swift build zero errors; hardware re-test of Listening+PTT is the follow-up.
|
Hardware verification of the PTT fixes (Ray-Ban Meta glasses paired, Listening active on the glasses, chat shortcut concurrently):
|
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift:731">
P1: This guard makes the seed-stale reconnect a no-op from the only call site: `beginTurn()` always has `inputTurnInProgress == true` by the time the async refresh returns, so the warm session never reconnects and stale context can persist indefinitely.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Opening a CoreAudio input can take seconds under load (observed ~5s with
the capture pipeline spinning up, Bluetooth contention, or cold audiomxd
state). PTT cold-started a new AudioCaptureService every press, so short
turns heard nothing ('discarding hub turn — audio 0.00s') and even
successfully opened devices were thrown away when the open completed after
the turn ended.
- Turn end now PARKS the running capture (running-but-dropped via a
lock-guarded lease the frame closures consult per chunk) for 120s instead
of destroying it; the next press adopts it and hears audio immediately.
- Late-completing opens are parked warm instead of stopped.
- Frame/silent-mic closures route by lease (generation + batchMode), so a
parked capture never leaks frames and adoption needs no closure rebinding
on the live IOProc thread.
- Device-failure paths (silent-mic fallback, CoreAudio rebuild) discard
rather than park, so a broken device is never kept warm.
Verified: xcrun swift build zero errors; hardware retest (Capture ON +
Listening on glasses + rapid PTT turns incl. barge-in) is the follow-up.
There was a problem hiding this comment.
3 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/macos/Desktop/Sources/AudioCaptureService.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:489">
P2: PTT contention checks can use a stale captured device after a live capture reconfigures to a new input. Consider refreshing `activeCaptures` with `newDeviceID` after the successful restart path in `reconfigureAfterChange`.</violation>
<violation number="2" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:511">
P1: Registry entries are only removed via `unregisterActiveCapture()` in `stopCapture()`. If an `AudioCaptureService` instance is deallocated while `isCapturing` is still true (the class reportedly has a `deinit` cleanup path for that case), its entry leaks in `activeCaptures`. This permanently makes `hasActiveCapture()` return `true` and poisons PTT routing. Adding `unregisterActiveCapture()` to `deinit` would prevent this leak.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
# Conflicts: # desktop/macos/Desktop/Sources/DefaultsKey.swift # desktop/macos/e2e/flows/settings.yaml
|
Addressed the latest PR review comments in Changes covered:
Local checks run:
CI is running on the updated branch. |
There was a problem hiding this comment.
5 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/macos/Desktop/Sources/AudioCaptureService.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:511">
P1: Registry entries are only removed via `unregisterActiveCapture()` in `stopCapture()`. If an `AudioCaptureService` instance is deallocated while `isCapturing` is still true (the class reportedly has a `deinit` cleanup path for that case), its entry leaks in `activeCaptures`. This permanently makes `hasActiveCapture()` return `true` and poisons PTT routing. Adding `unregisterActiveCapture()` to `deinit` would prevent this leak.</violation>
<violation number="2" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:988">
P1: The `reconfigureAfterChange` path can re-create an IOProc and register an active-capture entry even after the service has already stopped. `handleConfigurationChange` schedules `reconfigureAfterChange` asynchronously, but that method does not re-check `isCapturing` before rebuilding the IOProc and calling `registerActiveCapture`. If `stopCapture` runs during the 0.3 s delay, it sets `isCapturing = false` and unregisters, yet the delayed block still starts a new capture and re-registers. After that, both `stopCapture` and `deinit` skip cleanup because they are guarded by `isCapturing`, leaking the IOProc and leaving a stale registry entry. Similarly, when `retryOrGiveUp` finally gives up, it leaves the old registry entry in place. Consider adding an `isCapturing` guard at the start of `reconfigureAfterChange`, ensuring `isCapturing` is set back to `true` after a successful restart, and unregistering in the give-up path of `retryOrGiveUp`.</violation>
<violation number="3" location="desktop/macos/Desktop/Sources/AudioCaptureService.swift:988">
P2: The active-capture registry registers a device only after `AudioDeviceStart` succeeds, leaving a startup race where concurrent captures may not see each other. If another `AudioCaptureService` instance or PTT path checks `isDeviceActivelyCaptured` during the window between `AudioDeviceStart` and `registerActiveCapture`, it will see an empty registry and may start its own IOProc on the same device (or a Bluetooth input while another startup is in flight). Because the registry is intended to prevent exactly this contention, consider registering before `AudioDeviceCreateIOProcIDWithBlock`/`AudioDeviceStart` and removing the registration if creation or start fails.</violation>
</file>
<file name="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift:51">
P2: The 3-second polling loop in `refreshDevicesPeriodically` repeatedly performs a CoreAudio HAL device enumeration (`listInputDevices`) for as long as the settings card is mounted. This walk queries every system audio device for input channels, name, UID, and transport type, which incurs sustained overhead compared to the previous one-time fetch on appear. CoreAudio supports `AudioObjectAddPropertyListener` on `kAudioHardwarePropertyDevices` for event-driven notifications when devices change, so this polling is avoidable. Consider replacing the fixed-interval loop with a property listener or an explicit user refresh to eliminate unnecessary HAL queries.</violation>
</file>
<file name="desktop/macos/Desktop/Tests/TranscriptionTransportTests.swift">
<violation number="1" location="desktop/macos/Desktop/Tests/TranscriptionTransportTests.swift:121">
P2: The `range(of:)` results in the ordering assertion are force-unwrapped, which will crash the test runner instead of failing gracefully if either substring isn't found in the `stopBody` excerpt. Since the preceding `XCTAssertTrue(stopBody.contains(...))` checks report failure but don't halt execution, a mismatch would first surface those failure messages and then immediately crash the process — suppressing actionable diagnostics and preventing other tests from running. The rest of this file uses `guard let ... range(of:) else { return XCTFail(...) }` for safe range extraction; consider extracting both ranges into optional bindings (or using `XCTAssertNotNil`) before comparing their `lowerBound` positions.</violation>
</file>
<file name="desktop/macos/Desktop/Tests/RealtimeHubBargeInContinuityTests.swift">
<violation number="1" location="desktop/macos/Desktop/Tests/RealtimeHubBargeInContinuityTests.swift:101">
P3: The assertion on line 101 checks a log message string that a developer could reword without changing any behavior — for example, changing `deferred voice seed reconnect after turn completion` to `deferring voice seed reconnect until the current turn ends` would break the test despite identical behavior. The other three assertions in this test (`reconnectWarmSessionWhenTurnCompletes = true`, `private func reconnectDeferredWarmSessionIfNeeded()`, and call-count ≥ 3) already cover the structural properties you're verifying. Consider removing this log-message assertion to avoid brittle test maintenance.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| updateDefaultDeviceListener() | ||
| installDeviceFormatListener() | ||
| registerActiveCapture(deviceID: deviceID) |
There was a problem hiding this comment.
P1: The reconfigureAfterChange path can re-create an IOProc and register an active-capture entry even after the service has already stopped. handleConfigurationChange schedules reconfigureAfterChange asynchronously, but that method does not re-check isCapturing before rebuilding the IOProc and calling registerActiveCapture. If stopCapture runs during the 0.3 s delay, it sets isCapturing = false and unregisters, yet the delayed block still starts a new capture and re-registers. After that, both stopCapture and deinit skip cleanup because they are guarded by isCapturing, leaking the IOProc and leaving a stale registry entry. Similarly, when retryOrGiveUp finally gives up, it leaves the old registry entry in place. Consider adding an isCapturing guard at the start of reconfigureAfterChange, ensuring isCapturing is set back to true after a successful restart, and unregistering in the give-up path of retryOrGiveUp.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/AudioCaptureService.swift, line 988:
<comment>The `reconfigureAfterChange` path can re-create an IOProc and register an active-capture entry even after the service has already stopped. `handleConfigurationChange` schedules `reconfigureAfterChange` asynchronously, but that method does not re-check `isCapturing` before rebuilding the IOProc and calling `registerActiveCapture`. If `stopCapture` runs during the 0.3 s delay, it sets `isCapturing = false` and unregisters, yet the delayed block still starts a new capture and re-registers. After that, both `stopCapture` and `deinit` skip cleanup because they are guarded by `isCapturing`, leaking the IOProc and leaving a stale registry entry. Similarly, when `retryOrGiveUp` finally gives up, it leaves the old registry entry in place. Consider adding an `isCapturing` guard at the start of `reconfigureAfterChange`, ensuring `isCapturing` is set back to `true` after a successful restart, and unregistering in the give-up path of `retryOrGiveUp`.</comment>
<file context>
@@ -976,6 +985,7 @@ class AudioCaptureService: @unchecked Sendable {
updateDefaultDeviceListener()
installDeviceFormatListener()
+ registerActiveCapture(deviceID: deviceID)
log("AudioCapture: Restarted with new configuration")
</file context>
| } | ||
| } | ||
| } | ||
| .task { await refreshDevicesPeriodically() } |
There was a problem hiding this comment.
P2: The 3-second polling loop in refreshDevicesPeriodically repeatedly performs a CoreAudio HAL device enumeration (listInputDevices) for as long as the settings card is mounted. This walk queries every system audio device for input channels, name, UID, and transport type, which incurs sustained overhead compared to the previous one-time fetch on appear. CoreAudio supports AudioObjectAddPropertyListener on kAudioHardwarePropertyDevices for event-driven notifications when devices change, so this polling is avoidable. Consider replacing the fixed-interval loop with a property listener or an explicit user refresh to eliminate unnecessary HAL queries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/MicrophonePickerCard.swift, line 51:
<comment>The 3-second polling loop in `refreshDevicesPeriodically` repeatedly performs a CoreAudio HAL device enumeration (`listInputDevices`) for as long as the settings card is mounted. This walk queries every system audio device for input channels, name, UID, and transport type, which incurs sustained overhead compared to the previous one-time fetch on appear. CoreAudio supports `AudioObjectAddPropertyListener` on `kAudioHardwarePropertyDevices` for event-driven notifications when devices change, so this polling is avoidable. Consider replacing the fixed-interval loop with a property listener or an explicit user refresh to eliminate unnecessary HAL queries.</comment>
<file context>
@@ -48,7 +48,26 @@ struct MicrophonePickerCard: View {
}
}
- .onAppear { devices = AudioCaptureService.listInputDevices() }
+ .task { await refreshDevicesPeriodically() }
+ }
+
</file context>
| XCTAssertTrue(stopBody.contains("AudioDeviceDestroyIOProcID(devID, procID)")) | ||
| XCTAssertTrue(stopBody.contains("unregisterActiveCapture()")) | ||
| XCTAssertTrue( | ||
| stopBody.range(of: "AudioDeviceDestroyIOProcID(devID, procID)")!.lowerBound |
There was a problem hiding this comment.
P2: The range(of:) results in the ordering assertion are force-unwrapped, which will crash the test runner instead of failing gracefully if either substring isn't found in the stopBody excerpt. Since the preceding XCTAssertTrue(stopBody.contains(...)) checks report failure but don't halt execution, a mismatch would first surface those failure messages and then immediately crash the process — suppressing actionable diagnostics and preventing other tests from running. The rest of this file uses guard let ... range(of:) else { return XCTFail(...) } for safe range extraction; consider extracting both ranges into optional bindings (or using XCTAssertNotNil) before comparing their lowerBound positions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Tests/TranscriptionTransportTests.swift, line 121:
<comment>The `range(of:)` results in the ordering assertion are force-unwrapped, which will crash the test runner instead of failing gracefully if either substring isn't found in the `stopBody` excerpt. Since the preceding `XCTAssertTrue(stopBody.contains(...))` checks report failure but don't halt execution, a mismatch would first surface those failure messages and then immediately crash the process — suppressing actionable diagnostics and preventing other tests from running. The rest of this file uses `guard let ... range(of:) else { return XCTFail(...) }` for safe range extraction; consider extracting both ranges into optional bindings (or using `XCTAssertNotNil`) before comparing their `lowerBound` positions.</comment>
<file context>
@@ -108,6 +108,27 @@ final class TranscriptionTransportTests: XCTestCase {
+ XCTAssertTrue(stopBody.contains("AudioDeviceDestroyIOProcID(devID, procID)"))
+ XCTAssertTrue(stopBody.contains("unregisterActiveCapture()"))
+ XCTAssertTrue(
+ stopBody.range(of: "AudioDeviceDestroyIOProcID(devID, procID)")!.lowerBound
+ < stopBody.range(of: "unregisterActiveCapture()")!.lowerBound,
+ "registry ownership should be released after HAL teardown completes")
</file context>
|
|
||
| updateDefaultDeviceListener() | ||
| installDeviceFormatListener() | ||
| registerActiveCapture(deviceID: deviceID) |
There was a problem hiding this comment.
P2: The active-capture registry registers a device only after AudioDeviceStart succeeds, leaving a startup race where concurrent captures may not see each other. If another AudioCaptureService instance or PTT path checks isDeviceActivelyCaptured during the window between AudioDeviceStart and registerActiveCapture, it will see an empty registry and may start its own IOProc on the same device (or a Bluetooth input while another startup is in flight). Because the registry is intended to prevent exactly this contention, consider registering before AudioDeviceCreateIOProcIDWithBlock/AudioDeviceStart and removing the registration if creation or start fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/AudioCaptureService.swift, line 988:
<comment>The active-capture registry registers a device only after `AudioDeviceStart` succeeds, leaving a startup race where concurrent captures may not see each other. If another `AudioCaptureService` instance or PTT path checks `isDeviceActivelyCaptured` during the window between `AudioDeviceStart` and `registerActiveCapture`, it will see an empty registry and may start its own IOProc on the same device (or a Bluetooth input while another startup is in flight). Because the registry is intended to prevent exactly this contention, consider registering before `AudioDeviceCreateIOProcIDWithBlock`/`AudioDeviceStart` and removing the registration if creation or start fails.</comment>
<file context>
@@ -976,6 +985,7 @@ class AudioCaptureService: @unchecked Sendable {
updateDefaultDeviceListener()
installDeviceFormatListener()
+ registerActiveCapture(deviceID: deviceID)
log("AudioCapture: Restarted with new configuration")
</file context>
|
|
||
| XCTAssertTrue(source.contains("reconnectWarmSessionWhenTurnCompletes = true")) | ||
| XCTAssertTrue(source.contains("private func reconnectDeferredWarmSessionIfNeeded()")) | ||
| XCTAssertTrue(source.contains("deferred voice seed reconnect after turn completion")) |
There was a problem hiding this comment.
P3: The assertion on line 101 checks a log message string that a developer could reword without changing any behavior — for example, changing deferred voice seed reconnect after turn completion to deferring voice seed reconnect until the current turn ends would break the test despite identical behavior. The other three assertions in this test (reconnectWarmSessionWhenTurnCompletes = true, private func reconnectDeferredWarmSessionIfNeeded(), and call-count ≥ 3) already cover the structural properties you're verifying. Consider removing this log-message assertion to avoid brittle test maintenance.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Tests/RealtimeHubBargeInContinuityTests.swift, line 101:
<comment>The assertion on line 101 checks a log message string that a developer could reword without changing any behavior — for example, changing `deferred voice seed reconnect after turn completion` to `deferring voice seed reconnect until the current turn ends` would break the test despite identical behavior. The other three assertions in this test (`reconnectWarmSessionWhenTurnCompletes = true`, `private func reconnectDeferredWarmSessionIfNeeded()`, and call-count ≥ 3) already cover the structural properties you're verifying. Consider removing this log-message assertion to avoid brittle test maintenance.</comment>
<file context>
@@ -93,6 +93,18 @@ final class RealtimeHubBargeInContinuityTests: XCTestCase {
+
+ XCTAssertTrue(source.contains("reconnectWarmSessionWhenTurnCompletes = true"))
+ XCTAssertTrue(source.contains("private func reconnectDeferredWarmSessionIfNeeded()"))
+ XCTAssertTrue(source.contains("deferred voice seed reconnect after turn completion"))
+ XCTAssertGreaterThanOrEqual(
+ source.components(separatedBy: "reconnectDeferredWarmSessionIfNeeded()").count - 1,
</file context>
… into feature/rayban-meta-desktop-mic
What
findBuiltInMicDeviceID, persisted by device UID (IDs aren't stable across reconnects), applied through the existingAudioCaptureService(overrideDeviceID:)path. Neutral styling; changelog fragments included.AudioCaptureServicegains a process-wide active-capture registry, andpreferredPTTInputOverrideDeviceID()now yields to the built-in mic when the would-be input is held by another capture or is Bluetooth while any capture runs — extending the existingisDefaultOutputBluetooth()guard's philosophy.Why
Ray-Ban Meta glasses pair to a Mac as a Bluetooth HFP microphone, but the app only captured from the system-default input with no picker and no visible indication of the live source. Companion to the mobile-side #9178.
How it was tested
xcrun swift build -c debug --package-path Desktop— zero errors.omi-rayban, prod backend) captured live speech → transcript end-to-end;recordingInputDeviceNameflows toinput_device_nameas before.Review notes
--no-verify: the pre-push hook's full-repo pyright/env checks fail on unrelated pre-existing findings; this branch touches only Swift + changelog JSON.🤖 Generated with Claude Code