Skip to content

VEC-4 feat: use a Xiaomi Bluetooth remote as a wireless microphone - #104

Merged
IchenDEV merged 14 commits into
mainfrom
agent/vec-4-xiaomi-remote-mic
Sep 22, 2026
Merged

IchenDEV merged 14 commits into
mainfrom
agent/vec-4-xiaomi-remote-mic

Conversation

@IchenDEV

@IchenDEV IchenDEV commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Goal

Let a Xiaomi Bluetooth Remote 2 Pro act as Utter's microphone without
installing remote-mic-app or its virtual audio driver. The remote streams
16 kHz voice over the ATVV GATT profile; Utter connects over CoreBluetooth and
decodes the stream in-process.

What's in it

  • In-process ATVV protocol handling, ADPCM decode, framing, smoothing, and gain.
  • Main-queue CoreBluetooth scan/connect/handshake, reconnect, and source-bound
    callback/attempt isolation.
  • Remote capture integrated into the existing pipeline with system-input
    fallback, session cancellation, pre-roll, correct release/shutdown ownership,
    settings, Bluetooth usage text, entitlement, and localizations.
  • Production-path integration tests plus manager/peripheral/attempt mutation
    counterexamples.

Verified product and test scope

The verified product code/test head is
0998430e3e28215949d1b8d7398c795e4ab47923. Artifact source
31b3c7f614656c59855b7fd556734a11543fa9eb is a documentation-only
descendant. The current PR head
34fdf110aab0cddb01229c92474d00997bb70980 adds only the completed
acceptance handoff. Sources and Tests remain the verified trees
7bf86068d1142fdd2d1c1bae82131997f780356c and
431612931b709adf60b1f3b8180bc1c22b558418.

Mac mini verification (2026-09-22):

  • Host: Mac mini 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.
  • Manager, peripheral, and source-attempt mutations each exited 1 at their
    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.sh and explicit SDKROOT=MacOSX27.0.sdk
    scripts/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

  • Built source SHA:
    31b3c7f614656c59855b7fd556734a11543fa9eb.
  • Runner: macos-26-arm64, macOS 26.6.2; Xcode 26.6 (17F113).
  • App version: 0.0.46.
  • Download:
    https://github.com/IchenDEV/utter/actions/runs/35684827747/artifacts/10676765582
  • Artifact id/name: 10676765582 /
    utter-app-31b3c7f614656c59855b7fd556734a11543fa9eb.
  • Size/expiry: 33,050,656 bytes; expires 2026-10-06.
  • GitHub artifact ZIP digest:
    sha256:35da580f0a9652603dcab7982cc6b3a798196aa58e019aeb7dce7a5fe5e392ca.
  • Inner Utter-31b3c7f614656c59855b7fd556734a11543fa9eb.app.zip
    SHA-256:
    3a54ea580c2277c5cd3398a8d2912830b56fa3e3cd57c82843bf12b4f8aaf3c9.
  • App main binary SHA-256:
    db38a824064371f8438a8afc9631fab4975facbbb4641c5564728e00a9bf1ee8.
  • Independent re-download verification: outer artifact ZIP digest matched; the
    included checksum returned OK; extracting the inner archive reproduced the
    same 93,560,208-byte main-binary SHA-256.
  • The run proved the exact clean checkout, built and signed the app, verified it,
    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.plist to PlistBuddy and
exited 1. Workflow-only PR #107 fixed that error. Workflow-only PR #108 then
removed the one-time push bootstrap; default-branch head
2d93acbd5172cd0f3f694d57d6267ae72e45cf60 is 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

  • Hardware/permission testing is pending: pairing, Bluetooth permission,
    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.
  • Licensing remains pending human determination. Artifact construction for
    acceptance testing is not a finding that this implementation is independent,
    derivative, adequately attributed, or distributable.
  • Resource-risk, CODEOWNERS/SDLC, product merge, and release decisions remain
    pending their human owners.
  • The setting remains default-off. This PR is open and unmerged; no product
    release is published here.

Fixes VEC-4

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>
@IchenDEV IchenDEV changed the title feat: use a Xiaomi Bluetooth remote as a wireless microphone VEC-4 feat: use a Xiaomi Bluetooth remote as a wireless microphone Sep 21, 2026
IchenDEV and others added 12 commits September 21, 2026 15:30
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>
@IchenDEV
IchenDEV merged commit f9b23d9 into main Sep 22, 2026
3 checks passed
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