VEC-4 feat: use a Xiaomi Bluetooth remote as a wireless microphone - #104
Merged
Merged
Conversation
The remote streams 16 kHz voice over the ATVV GATT profile. Connect to it over CoreBluetooth and decode that stream in-process so Utter needs neither the vendor's virtual audio driver nor a second app. - Sources/RemoteMic: ATVV protocol/ADPCM decoder, CoreBluetooth bridge, and a capture source mirroring AudioCaptureManager - AudioCaptureManager prefers the remote when enabled and ready, and falls back to the system input otherwise - remoteMicEnabled / remoteMicGainDB settings plus General-tab controls - Bluetooth usage description and entitlement; en/zh-Hans strings - RemoteMicProtocolTests cover capability parsing, ADPCM, framing, PCM gain Default off. Hardware verification and the GPL/MIT licensing position are recorded as open gates in the SDLC bundle. Co-authored-by: multica-agent <github@multica.ai>
Independent review of the first head required four code fixes before this could pass (licensing and hardware verification remain human items): - wire the remote's voice key: MIC_OPEN_REQUEST / STREAM_START on the ATVV control channel now drive Utter's recording path, STREAM_STOP and disconnect stop it, so no HID F5->Fn remap or Input Monitoring is needed - stop the fallback leak: RemoteMicWantedState holds the want and a failed start tears down the callback, capture, and temp file - gate the handshake: capabilities are only requested after both notifications are confirmed, once per attempt, with connection and initialization timeouts, didFailToConnect recovery, and a generation guard against late callbacks - add RemoteMicHandshakeTests and RemoteMicWantedStateTests (20 remote-mic tests total; suite 653) Co-authored-by: multica-agent <github@multica.ai>
…ation Review found the press-start-release-stop path was not a cancellable, run-once session, and that the claimed handshake generation isolation did not exist. - RemoteMicSession latches press -> starting -> recording; a release, disconnect, or feature shutdown before the commit cancels the pending start, so a short press cannot begin recording afterwards - RemoteMicPreRoll keeps audio that arrives before the pipeline commits so the opening word is not clipped - endCapture closes the microphone exactly once in every phase; previously STREAM_STOP reset state before releasing, making the close unreachable - deactivate() and disabling the setting now end a live session - RemoteMicHandshake.reject capability responses that were not requested, so a late frame on a reused peripheral cannot mark a new attempt ready; didUpdateValueFor checks peripheral identity - correct 0x08 to START_SEARCH and cite the AOSP ATVV reference firmware; session latches on AUDIO_START, which needs no host MIC_OPEN - tests: session/ordering/pre-roll counterexamples, unrequested-capability rejection; mutation check confirms a release during starting fails the suite Co-authored-by: multica-agent <github@multica.ai>
Review found the cancel path only owned the layer above the pipeline: the task that actually awaits VoicePipeline.start had no handle, so a release during a cold model load could still reach recording or fall back to the system mic, and a normal release never stopped the pipeline. The claimed peripheral attempt isolation also did not exist for a reused CBPeripheral. - startRecording returns the task owning the whole pipeline start; the remote path stores and cancels it and passes the latch into start - RemoteMicStartGuard re-checks the latch after the model wait; a released or cancelled start aborts and never falls back to the system mic - release cancels that task and stops the pipeline exactly once - RemoteMicHandshake binds an attempt id; control/audio callbacks carry the attempt that raised them so a stale frame on a reused peripheral is rejected even after the new attempt requested capabilities - tests: RemoteMicStartGuardTests and RemoteMicAttemptIsolationTests, both shown to fail under the previous behaviour Co-authored-by: multica-agent <github@multica.ai>
…ack source Review found the release path cancelled capture before stopping, which nilled the WAV the pipeline was about to transcribe, and that the attempt was read as the live generation when a callback was delivered rather than the attempt that raised it. - RemoteMicReleaseDecision.applyRelease drives the production release: a committed recording is stopped, only an uncommitted start is cancelled - RemoteMicShutdownDecision stops an active recording when the feature is disabled, since the bridge's release callback is then suppressed - the bridge tags the peripheral with the attempt it connected for and attributes every callback to that tag, so a stale callback on a reused CBPeripheral is rejected after the new attempt has requested capabilities - RemoteMicAudioRouting (used by the bridge) drops audio with no live session - real-chain counterexamples: RemoteMicPipelineIntegrationTests drive the actual VoicePipeline.start await through an injected model-load barrier and capture spy; RemoteMicReleasePathTests drive applyRelease - correct the suite figures to 696 executed / 10 skipped / 0 failures, 63 remote-mic tests, and record that all 10 skips are environment/model gates (this tree has no live-download gate) Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
This was referenced Sep 22, 2026
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal
Let a Xiaomi Bluetooth Remote 2 Pro act as Utter's microphone without
installing
remote-mic-appor its virtual audio driver. The remote streams16 kHz voice over the ATVV GATT profile; Utter connects over CoreBluetooth and
decodes the stream in-process.
What's in it
callback/attempt isolation.
fallback, session cancellation, pre-roll, correct release/shutdown ownership,
settings, Bluetooth usage text, entitlement, and localizations.
counterexamples.
Verified product and test scope
The verified product code/test head is
0998430e3e28215949d1b8d7398c795e4ab47923. Artifact source31b3c7f614656c59855b7fd556734a11543fa9ebis a documentation-onlydescendant. The current PR head
34fdf110aab0cddb01229c92474d00997bb70980adds only the completedacceptance handoff. Sources and Tests remain the verified trees
7bf86068d1142fdd2d1c1bae82131997f780356cand431612931b709adf60b1f3b8180bc1c22b558418.Mac mini verification (2026-09-22):
Mac16,10, Apple M4, macOS 27.2; Xcode 27.0(
27A266a) and Swift 6.4.swift test --filter RemoteMic: 68 tests, 0 failures, exit 0.swift test --filter RemoteMicCallbackRoutingTests: 7 tests, 0 failures,exit 0.
unique target assertion; every restore returned to exact head/tree/clean;
harness wrapper exit 0.
swift test: 701 tests, 10 documented environment/model skips, 0 failures,exit 0.
scripts/sdlc-checks.shand explicitSDKROOT=MacOSX27.0.sdkscripts/ci-basic-checks.sh: exit 0.scripts/build-app.sh --app-only: Release arm64 app/CLI/AppIcon built;ad-hoc hardened-runtime signing and release artifact verification passed.
Downloadable CI acceptance artifact
Successful fixed-source run:
https://github.com/IchenDEV/utter/actions/runs/35684827747
31b3c7f614656c59855b7fd556734a11543fa9eb.macos-26-arm64, macOS 26.6.2; Xcode 26.6 (17F113).0.0.46.https://github.com/IchenDEV/utter/actions/runs/35684827747/artifacts/10676765582
10676765582/utter-app-31b3c7f614656c59855b7fd556734a11543fa9eb.sha256:35da580f0a9652603dcab7982cc6b3a798196aa58e019aeb7dce7a5fe5e392ca.Utter-31b3c7f614656c59855b7fd556734a11543fa9eb.app.zipSHA-256:
3a54ea580c2277c5cd3398a8d2912830b56fa3e3cd57c82843bf12b4f8aaf3c9.db38a824064371f8438a8afc9631fab4975facbbb4641c5564728e00a9bf1ee8.included checksum returned
OK; extracting the inner archive reproduced thesame 93,560,208-byte main-binary SHA-256.
archived it, re-extracted/re-verified it, then uploaded the archive, checksum,
and manifest.
Failed run
https://github.com/IchenDEV/utter/actions/runs/35683480777 remains preserved:
the product build/sign/verification succeeded, but the delivery workflow passed
the app directory rather than
Contents/Info.plisttoPlistBuddyandexited 1. Workflow-only PR #107 fixed that error. Workflow-only PR #108 then
removed the one-time push bootstrap; default-branch head
2d93acbd5172cd0f3f694d57d6267ae72e45cf60is manual-dispatch only.None of PRs #105-#108 merged #104 product code.
Detailed download verification and real-device steps:
docs/sdlc/changes/2026-09-21-remote-mic-integration/acceptance-handoff.md.Human acceptance gates
voice-key start/stop, first/last frame, disable during start/recording,
disconnect/reconnect, same-remote reuse, system-input fallback, and real
16 kHz mono audio.
acceptance testing is not a finding that this implementation is independent,
derivative, adequately attributed, or distributable.
pending their human owners.
release is published here.
Fixes VEC-4