Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ced7daf
hires-mic: dedicated branch + plan for a native Windows AirPods micro…
Aug 6, 2026
f9f8a17
hires-mic(Phase 1): LibrePodsMic virtual-audio driver base (ACX Audio…
Aug 6, 2026
2627fd4
hires-mic(Phase 1): install.ps1 for the LibrePodsMic virtual audio dr…
Aug 6, 2026
329bdd9
hires-mic: fix install.ps1 encoding (ASCII only, no em dashes)
Aug 6, 2026
f49cb9e
hires-mic(Phase 1): DONE — virtual microphone installs + shows in Win…
Aug 6, 2026
522f9e8
hires-mic(Phase 1b): capture-only driver (drop the phantom speaker)
Aug 6, 2026
6c6fdff
windows-tray: throttle the AAP receive loop (fix 7.5% CPU spin)
Aug 6, 2026
87ed69f
hires-mic(Phase 2): IOCTL PCM bridge into the capture stream
Aug 6, 2026
1456c23
hires-mic(Phase 2): lp-mic-test — feed a tone into the virtual mic
Aug 6, 2026
740914c
hires-mic: mark Phase 2 done (validated on hardware, clean tone)
Aug 6, 2026
379e25b
hires-mic(Phase 2): make the mic control device exclusive (one writer)
Aug 6, 2026
e810c95
hires-mic: lp-mic-rename — set the virtual mic's name (e.g. the AirPo…
Aug 6, 2026
66549f2
hires-mic: rename-mic.ps1 — set the virtual mic name via registry (pl…
Aug 6, 2026
1df638b
hires-mic: rename-mic.ps1 — handle REG_BINARY PROPVARIANT + list endp…
Aug 6, 2026
224d78d
windows-tray: theme-aware UI (dark/light) + Win11-flyout overlay style
Aug 6, 2026
b3855f1
windows-tray: acrylic flyout overlay + dark/light context menu
Aug 6, 2026
74912f6
windows-tray: revert overlay to the solid card (acrylic looked worse)
Aug 6, 2026
c843d32
hires-mic(Phase 3a): enable-command de-risk in the tray
Aug 6, 2026
bc6b41d
hires-mic: mark Phase 3a done (enable command + 0x58 stream validated)
Aug 6, 2026
09477d5
hires-mic(Phase 3b): decode AAC-ELD and feed the virtual mic
Aug 6, 2026
84a5e04
hires-mic(Phase 3b): add FFmpeg mingw import libs (force past lib/ ig…
Aug 6, 2026
f6be74e
hires-mic(Phase 3b): resample decoder output to 48 kHz (fix pitch)
Aug 6, 2026
4a9871a
hires-mic(Phase 3b): virtual mic offers 48 kHz only (fix deep/robotic…
Aug 6, 2026
8c9d352
windows-tray: single-instance guard (named mutex)
Aug 6, 2026
5b4705d
hires-mic(Phase 3b): prime the mic ring with a silence cushion (fix c…
Aug 6, 2026
885a739
hires-mic(Phase 3b): decode the mic as 64 kHz (fix deep pitch + clicks)
Aug 6, 2026
a324a84
hires-mic: mark Phase 3b done (voice decoded clean + in tune on hardw…
Aug 6, 2026
de5f40f
hires-mic(Phase 4): auto A2DP reset on mic stop (restore stereo)
Aug 6, 2026
34cb610
hires-mic(Phase 4): A2DP auto-reset works (correct service GUID + tim…
Aug 6, 2026
c726fcb
hires-mic: mark A2DP auto-reset done in the plan
Aug 6, 2026
d2716af
hires-mic(Phase 4): make-up gain + smaller latency cushion
Aug 6, 2026
3ae0bc5
hires-mic(Phase 4): soft-limit the make-up gain (tanh) to avoid clipping
Aug 6, 2026
57a2267
hires-mic(Phase 4): auto-enable the mic when an app records (+ UI)
Aug 6, 2026
4d13045
hires-mic(Phase 4): faster A2DP reset (shorter disconnect on mic stop)
Aug 6, 2026
2efd000
hires-mic(Phase 4): faster reset + manual mode toggle
Aug 6, 2026
a538d6e
hires-mic(Phase 4): manual-mode debounce + persistent reconnect card
Aug 6, 2026
7fb630c
hires-mic(Phase 4): longer manual debounce + card stays through recon…
Aug 6, 2026
4936f21
hires-mic(Phase 4): minimal FFmpeg build — 69 MB avcodec -> 0.7 MB
Aug 6, 2026
695568f
windows: commit Cargo.lock files
Aug 6, 2026
462b82c
hires-mic(Phase 4): name the virtual mic "LibrePods" (device-agnostic)
Aug 6, 2026
3573e9b
hires-mic: mark Phase 4 essentially complete (plug-and-play mic)
Aug 6, 2026
1ec34ff
hires-mic: fix lp-mic-rename IPolicyConfig crash (SetPropertyValue ne…
Aug 6, 2026
cf1d72e
hires-mic: rename-mic.ps1 matches the new LibrePods mic name
Aug 6, 2026
f6421a6
hires-mic: rename via direct registry write (IPolicyConfig was E_ACCE…
Aug 6, 2026
61a0585
hires-mic: lp-mic-rename renames ALL LibrePods endpoints (stale dupes)
Aug 6, 2026
cfda622
hires-mic: dynamic mic rename WORKS (REG_SZ + stable anchor)
Aug 7, 2026
46333e8
hires-mic: auto-rename the virtual mic to the connected device name
Aug 7, 2026
7fa5f87
docs: group Windows features + document the hi-res microphone
Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions crossplatform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
This directory brings the full **LibrePods** app to **Windows**, natively and
100% open-source, while **keeping Linux working from the same code**. It builds
on the Linux Rust app ([PR #241](https://github.com/librepods-org/librepods/pull/241))
and extends it into a single **cross-platform** crate plus an open-source
**Windows kernel driver** for the Apple Accessory Protocol (AAP).
and extends it into a single **cross-platform** crate plus two open-source
**Windows kernel drivers** — one for the Apple Accessory Protocol (AAP), one that
exposes the AirPods as a native **microphone**.

> Apple's AAP runs over a classic-Bluetooth L2CAP channel (PSM `0x1001`) that
> Windows won't let user-mode apps open. So we wrote a signed **KMDF profile
Expand All @@ -13,15 +14,28 @@ and extends it into a single **cross-platform** crate plus an open-source

## ✅ What works (validated on real AirPods Pro on Windows)

**Control & status**

| Feature | Status |
| --- | --- |
| **Battery** (L / R / Case, charging state) | ✅ matches iPhone |
| **Noise control** (Off / ANC / Transparency / Adaptive) | ✅ read + control |
| **Ear detection** | ✅ |
| **Auto-pause / resume** when you remove/insert a bud | ✅ via SMTC |
| **Conversational Awareness** (volume ducking when you speak) | ✅ via WASAPI |
| **Device info** (model, serials, firmware) | ✅ |
| **LE battery** via BLE advertisement (IRK/enc keys) | ✅ |

**Audio**

| Feature | Status |
| --- | --- |
| **Auto-pause / resume** when you remove/insert a bud | ✅ via SMTC |
| **Conversational Awareness** (volume ducking when you speak) | ✅ via WASAPI |
| **Hi-res microphone** — the AirPods AAC-ELD mic as a native Windows input | ✅ see below |

**App & UX**

| Feature | Status |
| --- | --- |
| **Full iced GUI** running natively | ✅ |
| **System tray** | ✅ (`tray-icon`) |
| **MagicPods-style centered popup** (connect / ANC / case) | ✅ (v1) |
Expand All @@ -31,6 +45,35 @@ The whole app also **compiles and links as a Windows `.exe`**
(`cargo build --target x86_64-pc-windows-gnu`), and **Linux stays green** at
every step.

## 🎙️ The hi-res microphone

Windows has no API to "create a virtual microphone", so we ship a **second**
kernel driver — `LibrePodsMic`, a virtual audio device (ACX) that appears as a
real capture endpoint. The tray reads the AirPods' uplink audio over AAP, decodes
it, and streams PCM into that device, so **any app (Teams, Zoom, Discord, OBS…)
can use the AirPods mic** — the Windows counterpart to Linux
[PR #655](https://github.com/librepods-org/librepods/pull/655).

```
AirPods ──AAP/L2CAP──▶ LibrePodsAAP ──IOCTL──▶ tray: decode AAC-ELD → resample → PCM
LibrePodsMic virtual mic ──▶ any app
```

| Capability | Status |
| --- | --- |
| AAC-ELD decode (FFmpeg/libavcodec, LGPL) + resample to 48 kHz | ✅ clean & in-tune |
| **Auto-activate** — hi-res stream turns on when an app records, off when it stops | ✅ (+ manual mode) |
| **A2DP auto-reset** — restores stereo playback after the mic degrades it to mono | ✅ |
| **Make-up gain** — the mic isn't quiet | ✅ (×3, soft-limited) |
| **Dynamic name** — the mic shows the connected device ("AirPods Pro de …", Beats…) | ✅ auto, no UAC |
| Minimal FFmpeg build (aac-only) — avcodec **69 MB → 0.7 MB** | ✅ |

The dynamic name is set by `lp-mic-rename` (writes the endpoint's `DeviceDesc` the
same way the Sound "Rename" UI does), launched by the tray through an elevated
on-demand scheduled task — so it happens automatically with no UAC prompt.

## How it's built — one crate, two operating systems

All OS-specific integration lives behind a `platform/` abstraction selected at
Expand All @@ -44,6 +87,7 @@ compile time (`#[cfg(target_os)]`). The protocol/UI code (`aacp`, `att`, `le`,
| LE advertisements | `bluer::monitor` | WinRT `BluetoothLEAdvertisementWatcher` |
| Audio routing / volume | PulseAudio | WASAPI (Core Audio) |
| Media control | MPRIS | SMTC |
| Microphone | PipeWire virtual input | **LibrePodsMic** virtual audio driver |
| System tray | `ksni` | `tray-icon` |
| Paths | XDG | `%APPDATA%` |

Expand All @@ -53,23 +97,31 @@ compile time (`#[cfg(target_os)]`). The protocol/UI code (`aacp`, `att`, `le`,
the `platform/` abstraction layer.
- **`windows-driver/LibrePodsAAP/`** — the open-source KMDF AAP kernel driver
(`+ prebuilt/` so it installs without the WDK; Test Mode).
- **`windows-driver/LibrePodsMic/`** — the virtual-microphone audio driver (ACX),
plus **`lp-mic-rename/`** (names the mic after the connected device).
- **`windows-app/`** — a lightweight native **tray app** (battery + ANC + volume
+ ear-detection auto-pause + the popup overlay) and an egui window, plus the
`lp-driver-test` CLI.
+ ear-detection auto-pause + the hi-res mic + the popup overlay) and an egui
window, plus the `lp-driver-test` CLI.

## Install (Windows, Test Mode)

**Ready-to-install builds are in [`dist/windows/`](dist/windows/)** — both apps,
the driver, and a one-shot `install.ps1`. The driver is test-signed, so it needs
Secure Boot **off** + `bcdedit /set testsigning on` + reboot; then run the
**Ready-to-install builds are in [`dist/windows/`](dist/windows/)** — the apps,
the driver, and a one-shot `install.ps1`. The drivers are test-signed, so they
need Secure Boot **off** + `bcdedit /set testsigning on` + reboot; then run the
installer (elevated). Full steps are in
[`dist/windows/README.md`](dist/windows/README.md); build instructions + the
technical log are in `HANDOFF.md`.

> **Signing note.** Kernel drivers must be signed to load. For personal use we
> test-sign (above). Distributing to others without Test Mode needs an **EV
> certificate + Microsoft Partner Center** attestation (Azure Trusted Signing
> covers the user-mode app for SmartScreen, but **not** kernel drivers).

## Not done yet / in progress

- One-click redistributable installer (bundle driver + app).
- One-click redistributable installer (bundle both drivers + app).
- Unify the interim tray app and the iced window into one app.
- Fold the `LibrePodsMic` driver install into the one-shot `install.ps1`.
- Release build packaging; precise BLE lid-open detection; heart-rate RE
(AirPods Pro 3).

Expand Down
19 changes: 19 additions & 0 deletions crossplatform/dist/windows/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item (Join-Path $here 'librepods-tray.exe') $dest -Force
Copy-Item (Join-Path $here 'librepods.exe') $dest -Force

# lp-mic-rename + its elevated on-demand task: lets the (unelevated) tray rename
# the virtual mic to the connected device's name without a UAC prompt.
$renameExe = Join-Path $here 'lp-mic-rename.exe'
if (Test-Path $renameExe) {
Copy-Item $renameExe $dest -Force
$renameDest = Join-Path $dest 'lp-mic-rename.exe'
Write-Host '==> Registering the elevated mic-rename task...'
$taskName = 'LibrePods Rename Mic'
$action = New-ScheduledTaskAction -Execute $renameDest
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 2) `
-StartWhenAvailable
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal `
-Settings $settings -Description 'Rename the LibrePods virtual mic to the connected device name.' `
-Force | Out-Null
}

Write-Host '==> Adding the tray app to startup...'
$startup = [Environment]::GetFolderPath('Startup')
$ws = New-Object -ComObject WScript.Shell
Expand Down
118 changes: 118 additions & 0 deletions crossplatform/hires-mic/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Hi-res AirPods microphone on Windows — feature branch

**Branch:** `windows-hires-mic` (off `cross-platform`; merges back into it when done).

**Goal:** expose the AirPods' hi-res (AAC-ELD) microphone as a **native Windows
microphone**, so any app (Teams, Zoom, Discord, OBS…) can use it — the same
feature the Linux side is adding in
[PR #655](https://github.com/librepods-org/librepods/pull/655), but with a
self-contained **virtual audio driver** instead of PipeWire.

## Why a driver (not VB-Cable)

Windows has no API to "create a virtual microphone" — it needs a virtual audio
device driver. We already ship a signed kernel driver (`LibrePodsAAP`), so a
second one is the clean, dependency-free path (no third-party VB-Cable). Base it
on Microsoft's **SYSVAD** sample (a virtual audio device with capture + render
endpoints, no hardware). NOTE: audio drivers use **PortCls/AVStream** or the
newer **ACX** framework — different from the KMDF+BRB approach of `LibrePodsAAP`.

## Architecture

```
AirPods ──AAP / L2CAP──▶ LibrePodsAAP driver ──IOCTL──▶ app
│ decode AAC-ELD (FFmpeg / libavcodec)
│ → PCM
LibrePodsMic virtual audio driver
Windows sees a "LibrePods Microphone"
(Teams / Zoom / Discord / OBS / …)
```

The protocol (enable-mic control command, uplink packet framing, AAC-ELD params)
is platform-neutral and comes from PR #655 — it lands in the shared
`crossplatform-rust` crate (`aacp`/`media_controller`), gated per platform for
the sink.

## Phases (incremental, each independently testable)

1. **Virtual-mic driver base** — build + test-sign + install SYSVAD (or a trimmed
fork) so Windows shows a "LibrePods Microphone" capture endpoint. Prove it
appears in Sound settings and apps. *(driver: `windows-driver/LibrePodsMic/`)*
2. **PCM bridge** — an IOCTL/shared-ring for user mode to push PCM samples into
the driver; feed a test tone / a WAV → verify it's audible on the virtual mic
(record in Voice Recorder / Audacity).
3. **Protocol port** — from PR #655: the AAP command that enables the hi-res mic,
the uplink packet framing + watchdog, and AAC-ELD decoding via FFmpeg
(libavcodec) — in the shared crate, `platform::MicSink` for the OS sink.
4. **Integration** — talk into the AirPods → decoded audio reaches the virtual
mic. Conversation-awareness pause during capture, AGC toggle, settings
persistence (mirror #655).

## Risks / open questions

- **Audio driver complexity** — WaveRT buffering, formats, timing. SYSVAD is a
large sample; getting a stable capture endpoint is the bulk of the work.
- **AAC-ELD patents** — the decoder (FFmpeg) is patent-encumbered; distribution
implications (PR #655 raises the same). Keep the decoder optional / documented.
- **Latency** — L2CAP → decode → driver ring; needs a small, steady buffer.
- **Test-signing** — same Test Mode requirement as the AAP driver.

## References

- Microsoft SYSVAD (virtual audio device sample), and the ACX audio samples.
- LibrePods PR #655 (Linux hi-res mic: AAC-ELD + PipeWire) — the protocol RE.
- `../windows-driver/LibrePodsAAP/` — our existing driver + build/sign/install loop.

## Status

- **Phase 1 — DONE** ✅ (`windows-driver/LibrePodsMic/`, based on the MS ACX
AudioCodec sample). Builds with WDK 28000, installs via `install.ps1`, and
Windows shows a virtual **"Microphone (AudioCodec Device)"** (confirmed on
hardware). The audio source is still the sample's dummy; feeding real audio is
next.
- **Phase 1b — DONE** ✅ (`522f9e8`): trimmed to **capture-only** — dropped the
render (speaker) circuit in `Device.cpp` (create/add/remove) so only a mic
endpoint exists (no phantom speaker grabbing default output). Builds clean.
`install.ps1` now `devcon remove`s any prior device before installing so
re-running updates in place.
- **Phase 2 — DONE** ✅ (`87ed69f`, validated on hardware): `Common/MicPipe.{h,cpp}`
— a spin-locked global PCM ring buffer + a control device `\\.\LibrePodsMic`
exposing `IOCTL_LIBREPODS_MIC_WRITE_PCM` (0x0022A000). `StreamEngine.cpp`
`ProcessPacket` drains the ring instead of the WAV/tone dummy. Proven end to
end: `lp-mic-test` (a user-mode tone feeder, `windows-driver/lp-mic-test/`)
pushed a 440 Hz sine and it was **recorded and audible** on "Microphone
(AudioCodec Device)". The audio-driver de-risk is complete.
- **Phase 3a — DONE** ✅ (`c843d32`, validated on hardware): the AAP enable
command works. The tray's "Hi-res microphone (test)" toggle sends `START_AUDIO`
(`04 00 04 00 58 …`, from PR #655); the AirPods enter mic mode (A2DP playback
drops to right-only mono, as expected — needs an A2DP reset like #655), and the
receive loop confirmed the **0x58 uplink audio packets flow** ("receiving
audio" card). Constants + `is_audio_packet` in `aap.rs`; protocol in
[[hires-mic-protocol]].
- **Phase 3b — DONE** ✅ (validated on hardware, user's voice recorded clean &
in tune). The tray decodes the 0x58 AUs (AAC-ELD) via an FFmpeg libavcodec
shim (LGPL, `eld_shim.c`), resamples, and streams to `\\.\LibrePodsMic`. Key
fixes: mic frames are **480 samples @ 64 kHz** (not 48 kHz — the 4-byte ASC
lies; confirmed by the +180/AU timestamp = a 24 kHz clock over 7.5 ms frames),
so resample **64000 → 48000**; capture circuit restricted to **48 kHz only**;
and a **~150 ms silence cushion** on the ring to absorb the bursty per-packet
feed. Pitch went ~105 → ~122 Hz (ref ~148), zero click/gap artifacts. Audio is
a bit quiet (a gain stage would help) and mono (single mic capsule).
- **Phase 4 — essentially COMPLETE** ✅ (plug-and-play). Done:
- **A2DP auto-reset** — toggle the AirPods' AudioSink (0x110B) service to
restore stereo after the mic stops (0x110D gave ERROR 1060; found via
BluetoothEnumerateInstalledServices), with a persistent "Restoring stereo…"
card through the reconnect.
- **Auto-enable** — the driver's capture-activity counter
(IOCTL_LIBREPODS_MIC_STATUS) lets the tray auto-start the hi-res stream when
an app records and auto-stop (debounced) when it finishes. + a manual mode.
- **Make-up gain** (×3, tanh soft-limit) so the mic isn't quiet.
- **Minimal FFmpeg** (7.1, aac-only) — avcodec 69 MB → 0.7 MB.
- **Name** "LibrePods" (device-agnostic).
- Single-instance guard.
- Follow-ups: exact per-device dynamic name (IPolicyConfig, needs a debugger),
VendorID spoofing (Apple DID), 2 s stall watchdog, apple-wireshark RE.
- Phase 3 (protocol from PR #655 + AAC-ELD) and Phase 4 (integration) to follow.
1 change: 1 addition & 0 deletions crossplatform/windows-app/librepods-tray/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crossplatform/windows-app/librepods-tray/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ imageproc = "0.25"
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_IO",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_Devices_DeviceAndDriverInstallation",
"Win32_Devices_Bluetooth",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_Graphics_Dwm",
"Win32_System_LibraryLoader",
] }
# `windows` (full COM/WinRT) for Core Audio volume control + SMTC media control.
Expand All @@ -31,6 +34,9 @@ windows = { version = "0.58", features = [
"Media_Control", # GlobalSystemMediaTransportControlsSession* (auto-pause)
] }

[build-dependencies]
cc = "1"

[[bin]]
name = "librepods-tray"
path = "src/main.rs"
Expand Down
23 changes: 23 additions & 0 deletions crossplatform/windows-app/librepods-tray/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::path::PathBuf;

fn main() {
// The AAC-ELD decoder (FFmpeg) is only used on Windows.
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let ff = manifest.join("vendor/ffmpeg");

// Compile the tiny C shim against the vendored FFmpeg headers.
cc::Build::new()
.file("src/eld_shim.c")
.include(ff.join("include"))
.compile("eld_shim");

// Link the FFmpeg import libs (avcodec pulls avutil + swresample).
println!("cargo:rustc-link-search=native={}", ff.join("lib").display());
println!("cargo:rustc-link-lib=avcodec");
println!("cargo:rustc-link-lib=avutil");
println!("cargo:rustc-link-lib=swresample");
println!("cargo:rerun-if-changed=src/eld_shim.c");
}
Loading