feat(voice): mute microphone from AirPods controls - #160
Conversation
9440025 to
b555feb
Compare
| } | ||
| // Observing this notification and reading the state opts this capture | ||
| // session into AirPods mute delivery. Rust's handler remains the event path. | ||
| inputMuteObserver = NotificationCenter.default.addObserver( |
There was a problem hiding this comment.
🤖 Adapted from voice-conversation-cli native capture and mute-observer lifecycle: https://github.com/squareup/voice-conversation-cli/blob/f33dcd81631c7eba4c33ac8b8e646f8681090165/Sources/VoiceCLI/MicCapture.swift and https://github.com/squareup/voice-conversation-cli/blob/f33dcd81631c7eba4c33ac8b8e646f8681090165/Sources/VoiceCLI/MenuBarController.swift#L591-L605
Berd retains its WebKit PCM path, so this adaptation installs a silent native tap and preserves route-change restart behavior plus the empirically required input-mute notification opt-in.
Reproduce the full comparison:
git diff --no-index <(git -C ~/Development/voice-conversation-cli show f33dcd81631c7eba4c33ac8b8e646f8681090165:Sources/VoiceCLI/MicCapture.swift) src-tauri/swift/BerdAirPodsBridge/Sources/BerdAirPodsBridge/BerdAirPodsBridge.swift
Relevant adaptation:
- final class MicCapture: NSObject, Sendable {
+ private final class AirPodsCapture: @unchecked Sendable {
- inputNode.installTap(...buffer delivery...)
+ inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { _, _ in }
+ inputMuteObserver = NotificationCenter.default.addObserver(
+ forName: AVAudioApplication.inputMuteStateChangeNotification,
+ object: nil,
+ queue: nil
+ ) { _ in
+ _ = AVAudioApplication.shared.isInputMuted
+ }| @@ -0,0 +1,19 @@ | |||
| #import "BerdObjCExceptionCatch.h" | |||
|
|
|||
| // Adapted from voice-conversation-cli's VCTryObjCBlock. | |||
There was a problem hiding this comment.
🤖 Adapted from voice-conversation-cli Objective-C exception containment: https://github.com/squareup/voice-conversation-cli/blob/f33dcd81631c7eba4c33ac8b8e646f8681090165/Sources/ObjCExceptionCatch/ObjCExceptionCatch.m
This keeps AVAudioEngine Objective-C exceptions from terminating Berd during device and route transitions.
Reproduce:
git diff --no-index <(git -C ~/Development/voice-conversation-cli show f33dcd81631c7eba4c33ac8b8e646f8681090165:Sources/ObjCExceptionCatch/ObjCExceptionCatch.m) src-tauri/swift/BerdAirPodsBridge/Sources/BerdObjCExceptionCatch/BerdObjCExceptionCatch.m
Actual adaptation:
- BOOL VCTryObjCBlock(...)
+ BOOL BerdTryObjCBlock(...)
- @"NSExceptionName": exception.name,
- domain: @"com.voice-conversation.objc-exception"
+ domain: @"com.berd.objc-exception"
morgmart
left a comment
There was a problem hiding this comment.
🤖 Automated code review
REQUEST_CHANGES: the native mute boundary introduces an unguarded last-completion-wins race that can leave the microphone opposite the user's latest choice. Two non-blocking issues also remain: the new mute toast bypasses localization, and a lifecycle race regression test was weakened so it no longer exercises a stale response. Supplied GitHub evidence was inspected: all eight check runs were completed successfully, while the combined commit status was pending with no commit statuses; required checks still govern merge readiness.
Deterministic publication result: 1 blocking and 2 non-blocking finding(s) publishable; 0 duplicate(s) suppressed.
d64e1b4 to
7f59ede
Compare
morgmart
left a comment
There was a problem hiding this comment.
🤖 Automated code review
COMMENT: one non-blocking lifecycle consistency issue remains. The native status exposes whether mute control is available but not the current native mute state, so a renderer that initializes or reloads during an already-muted conversation cannot reconstruct the user-visible control state. The earlier stale-request race, localization concern, and stale-start test gap are fixed at this head. Supplied GitHub evidence was inspected: Clean-room frozen install and DCO Check succeeded; the frontend and three platform Rust checks were still in progress, and the combined commit status was pending. Required checks still govern merge readiness.
Deterministic publication result: 0 blocking and 1 non-blocking finding(s) publishable; 0 duplicate(s) suppressed.
morgmart
left a comment
There was a problem hiding this comment.
🤖 Automated code review
APPROVE: fresh static review of the full exact three-dot comparison found no publishable findings. Final self-check covered headset and UI mute/unmute flows, renderer recovery, browser/native state consistency, lifecycle start/stop/failure, queued and partial audio discard, async ordering and never-completes behavior, stale status/event races, ownership and consent boundaries, accessibility, localization, navigation, design-system rules, and test honesty. The previously reported stale-request race, localization issue, stale-start coverage gap, and unrecoverable mute state are fixed at this head and have substantive human-account replies in resolved threads. Supplied GitHub evidence was inspected: all eight check runs completed successfully; the combined commit status remained pending with no commit statuses, and required checks still govern merge readiness.
Deterministic publication result: 0 blocking and 0 non-blocking finding(s) publishable; 0 duplicate(s) suppressed.
Pending checks: 1 check(s) are not complete.
This approval reflects the completed code review only; merge readiness remains governed by the repository's required checks.
Summary
AirPods and supported Beats mute controls now mute microphone input during an active Berd voice conversation. Berd keeps its existing browser audio capture, while a lifecycle-scoped native macOS input session opts Berd into headset mute delivery and drives the same
AVAudioApplicationpath used by the UI button. Muting clears queued and partially decoded speech without affecting assistant output.Verified on AirPods Pro 3: both the stem control and Berd microphone button toggle the UI and play the native mute and unmute indicator sounds.
Reviewer-reproducible examples
On macOS 14 or later with supported AirPods or Beats: