Windows: native hi-res AirPods microphone (AAC-ELD virtual mic) - #1
Merged
Conversation
…phone New feature branch (off cross-platform, merges back when ready) to expose the AirPods' hi-res AAC-ELD mic as a native Windows microphone via our own virtual audio driver (SYSVAD-based) instead of a third-party VB-Cable — the Windows counterpart to Linux PR librepods-org#655. crossplatform/hires-mic/PLAN.md lays out the architecture (AAP driver -> app decode -> LibrePodsMic virtual driver -> Windows mic), the incremental phases, risks (audio-driver complexity, AAC-ELD patents), and references. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Codec) — builds Import the Microsoft ACX AudioCodec sample (MIT, from Windows-driver-samples audio/Acx/Samples) as the base for the virtual microphone driver: a ROOT-enumerated (software, no-hardware) audio device with a capture circuit, built on ACX over KMDF — the same framework family as LibrePodsAAP (vs SYSVAD's PortCls). Confirmed it BUILDS with VS2026 + WDK 28000 (AudioCodec.sys, EXIT 0) — resolving the biggest Phase-1 unknown. README notes origin/license + status. Next: install the ROOT device + verify a virtual mic appears, then trim to capture-only and add the PCM bridge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iver Test-signs the built AudioCodec.sys, generates+signs a catalog (inf2cat), trusts a test cert, and creates the ROOT\AudioCodec device via devcon so a virtual microphone endpoint appears. Elevated + Test Mode, like the AAP driver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PowerShell read the UTF-8 em dash as garbage and broke string parsing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dows Confirmed on hardware: install.ps1 signs+catalogs the ACX driver and creates the ROOT\AudioCodec device via devcon; Windows shows 'Microphone (AudioCodec Device)' in Sound -> Input. The virtual-mic foundation is proven. Next: Phase 2 (IOCTL PCM bridge to feed the AirPods' decoded audio into the capture circuit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Device.cpp: don't create/add/remove the render (speaker) circuit — LibrePodsMic is a virtual *microphone*, so exposing a render endpoint only risked Windows grabbing the default output (the brief static on install) and cluttered the device list. Capture circuit only. Builds clean. install.ps1: `devcon remove ROOT\AudioCodec` before install so re-running updates in place instead of stacking a second virtual device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The driver's ACL read (IOCTL_LP_RECEIVE) completes immediately with 0 bytes when the channel is idle (ACL_SHORT_TRANSFER_OK), so the background receive loop was spinning a core (~7.5% CPU, measured) and, worse, flooding the Bluetooth stack with thousands of back-to-back ACL reads per second — which contends with the A2DP audio stream and is the likely source of the crackle/static. Yield the CPU (150 ms) whenever a read returns no data, so the loop can't spin. Pushed events (battery / ANC / ear-detection) still arrive within ~150 ms since we only sleep on an empty read. Measured after: 0.1% CPU. A fully event-driven (async, kernel-pended) read would take idle reads to zero, but that's a driver change (reinstall + reboot); this gets ~99% of the benefit without touching the signed driver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add MicPipe: a global byte ring buffer (spin-locked, single-instance mic) that
user mode fills with decoded PCM and the ACX capture stream engine drains one
packet per notification tick.
- Common/MicPipe.{h,cpp}: the ring + a driver-scoped control device
(\\.\LibrePodsMic) exposing IOCTL_LIBREPODS_MIC_WRITE_PCM (0x0022A000,
METHOD_BUFFERED). Write appends (drops oldest on overflow); Read fills a
packet, zero-filling underrun. Read is DISPATCH_LEVEL-safe (non-paged, spin
lock). World RW SDDL so a non-elevated app can push audio.
- StreamEngine.cpp ProcessPacket: fill the capture packet from MicPipeRead
instead of the sample's WAV-file / tone dummy sources.
- Device.cpp: MicPipeInit + MicPipeCreateControlDevice at device add
(best-effort; never fails enumeration).
- SamplesCommon.vcxproj: compile MicPipe.cpp.
Builds clean (EXIT 0). Format is mono 16-bit PCM at 44100/48000 Hz (whatever the
client opens the capture endpoint with). Next: user-mode feeder to validate the
pipe end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A tiny user-mode tool that opens \\.\LibrePodsMic and streams a 440 Hz sine (mono 16-bit, 48 kHz by default) via IOCTL_LIBREPODS_MIC_WRITE_PCM, in real-time 10 ms chunks. Record from "Microphone (AudioCodec Device)" to hear it — proves the user-mode -> IOCTL -> ring -> ACX capture path end to end. Raw kernel32 FFI, no deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bridge proven end-to-end: lp-mic-test's 440 Hz sine recorded and audible on the virtual mic, clean waveform with a single feeder. Phase 3 (AAP enable-mic + AAC-ELD decode from PR librepods-org#655) is next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A virtual mic has a single audio source. Two processes writing the ring concurrently interleave and sound like static (seen while testing with multiple tone feeders running at once). WdfDeviceInitSetExclusive refuses a second open, so this class of corruption can't happen — the right behavior for Phase 3 where the app opens \\.\LibrePodsMic once and feeds the decoded AirPods audio. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ds') Standalone tool that finds the AudioCodec capture endpoint and sets its PKEY_Device_FriendlyName via IPolicyConfig (CPolicyConfigClient) — user-mode, no admin, no reboot. So the virtual mic can show "AirPods Pro de Pedro" instead of the driver's static "CustomName2 (AudioCodec Device)" in Discord/Teams/Zoom. The app will call this at runtime in Phase 4; this proves the mechanism. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…an B) The IPolicyConfig runtime path (lp-mic-rename) crashed on Win11 (the interface vtable layout varies by Windows version). This is the reliable fallback: write PKEY_Device_FriendlyName on the AudioCodec capture endpoint in the registry (elevated) and restart AudioEndpointBuilder to apply without a reboot — the same mechanism SoundVolumeView uses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oints MMDevices property values are REG_BINARY (4-byte type tag + UTF-16), not plain strings, so the first version never matched. Decode/encode the PROPVARIANT properly, match on decoded DeviceDesc OR FriendlyName, and list all capture endpoints for visibility. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow the Windows theme instead of a fixed dark look: - theme.rs: read AppsUseLightTheme / SystemUsesLightTheme from the registry. - Overlay: restyle like the Win11 volume/brightness flyout — native DWM rounded corners + immersive dark-mode chrome + a subtle border, and a theme-aware fill (dark card/light text in dark mode, light card/dark text in light mode). Replaces the color-key rounding hack. Re-reads the theme on each show. - Tray icon: draw the battery % digits dark on a light taskbar, white on a dark one (SystemUsesLightTheme) — otherwise white digits vanished on a light taskbar. Note: the flyout's translucent acrylic *blur* isn't included (that needs per-pixel-alpha layered compositing); this matches the shape, color, and theme. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Overlay: add the Win11 acrylic backdrop (DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW) with the frame extended across the client, painting the client "glass" (black) so the translucent material shows through. Falls back to the solid theme card on OSes that don't support it (hr < 0). - Context menu: Win32 menus ignore dark mode by default; opt in via the undocumented uxtheme SetPreferredAppMode (135) + FlushMenuThemes (136), keyed to the app theme (theme::apply_menu_theme), applied at startup and each refresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The acrylic backdrop washed the card out and muddied the corner; the solid theme-aware card with native rounded corners + border looks cleaner. Keep the rounded corners, theme-following colors, and the dark/light context menu; drop only the acrylic backdrop + glass fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the AAP hi-res mic start/stop commands (from PR librepods-org#655) and a "Hi-res microphone (test)" menu toggle. On enable, send START_AUDIO over the existing AAP session; the receive loop counts the 0x58 uplink audio packets and pops a "receiving audio" card once the stream is confirmed flowing. No decode yet — this proves the enable command works on real AirPods before investing in the AAC-ELD (FFmpeg) decode. aap.rs: START_AUDIO/STOP_AUDIO + is_audio_packet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tray now turns the AirPods' hi-res mic into real audio: enable → decode → virtual mic. Wired end to end. - eld_shim.c + build.rs: a tiny C shim over FFmpeg libavcodec (LGPL, native AAC-ELD) behind a stable ABI (eld_open/decode/close), compiled via cc against vendored FFmpeg headers and linked to the import libs. (libfdk-aac is non-free and won't cross-compile for mingw64, so FFmpeg native like PR librepods-org#655.) - eld.rs: Rust wrapper; opens the decoder with the AirPods ASC F8 E6 30 00 (AOT 39 ELD, 48 kHz mono), decodes AUs to i16. - micpipe.rs: writes decoded PCM into \\.\LibrePodsMic (IOCTL 0x0022A000). - aap.rs for_each_au: parse the 0x58 packet's AUs (22-byte header, len at off+4). - main.rs: on the "Hi-res microphone" toggle, the receive loop decodes each 0x58 packet's AUs and streams the PCM to the virtual mic. - vendor/ffmpeg: committed headers + import libs; the ~75 MB runtime DLLs are gitignored (fetched, shipped alongside the exe). Builds + runs on hardware (FFmpeg DLLs load). Pending: on-hardware voice test, plus A2DP reset (playback drops to right-only mono while the mic is active) and the 2s stall watchdog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nore) The vendored .dll.a import libs are needed to link the AAC-ELD shim; the repo's lib/ ignore rule was hiding them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AAC-ELD decoder outputs at its own rate (voice ELD is typically 24 kHz), but we fed those samples into the 48 kHz capture endpoint -> playback ran ~2x slow (deep 'censored voice'). Resample the decoded audio (whatever rate) to 48 kHz mono s16 via libswresample before feeding the mic pipe, so the pitch is correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… pitch) Diagnosed from the user's recordings: the virtual mic was captured at 44100 Hz (Voice Recorder's pick) while we feed audio resampled to 48000 Hz, so playback ran ~8% slow → deep "robotic" voice (duration ratio 11.91/11.05 = 1.078 ~= 48000/44100). Drop the 44100 format from the capture circuit so every app opens at 48000, matching our feed. (eld_shim: add eld_in_rate diagnostic helper.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Launching the tray while a copy was still starting left two icons fighting over the single-open driver and the exclusive mic pipe. Bail on startup if the named mutex already exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…licks) Diagnosed via a dump of our decoded PCM: the decode is clean (no clicks), so the periodic clicks in the recording came from the ring, not the codec. We feed in per-packet bursts (~80 ms of audio at once) while the capture endpoint drains steadily, so with no head start the ring underran between bursts — a click per packet. Prime the ring with ~150 ms of silence on start-up to absorb the burstiness. Also decode a whole packet's AUs and write once. Removed the debug packet/PCM dumps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AirPods hi-res mic AAC-ELD frames are 480 samples at 64 kHz (7.5 ms), but the 4-byte ASC advertises samplingFrequencyIndex 3 (48000), so FFmpeg labels them 48000. Confirmed by the 0x58 AU timestamps incrementing +180 per frame — a 24 kHz clock over 7.5 ms frames (7.5ms * 24000 = 180) — and by the measured deep pitch (~1.33x). Feeding 64 kHz content as 48 kHz played it ~0.75x slow (deep) AND overfed the ring (the clicks). Resample from the true 64 kHz -> 48 kHz, which fixes both at once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…are) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hi-res mic puts the AirPods into a bidirectional "call" mode that degrades A2DP playback to mono/right and stays that way until reconnected (the user was restarting Bluetooth each time). On mic-off, toggle the AirPods' A2DP service (BluetoothSetServiceState disable→enable, off the UI thread) to reconnect the audio link and restore stereo — the scoped, programmatic equivalent of disconnect+reconnect. Best-effort (silently ignored if it needs elevation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing) The A2DP audio service the AirPods actually expose is AudioSink (0x110B), not AdvancedAudioDistribution (0x110D) — toggling 0x110D returned ERROR 1060 (service not installed). Enumerating the device's installed services found the right GUID. Toggling 0x110B disable→enable (with a 1 s settle after the mic stop and a 2 s teardown before reconnect) reliably restores stereo — no Bluetooth restart needed. Removed the debug service-dump and the result-code overlay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measured from the user's recordings: the AirPods mic was ~4 dB quieter than a reference with ~22 dB of headroom, and the 150 ms ring cushion added noticeable start latency + clipped the tail on stop. Add a x3 (~+9.5 dB) make-up gain on the decoded float (before swr, so loud transients clamp cleanly) and drop the cushion to 80 ms (still covers the ~30 ms per-packet bursts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The x3 gain sounded blown out on loud transients (peaks hit -7.8 dB and louder ones hard-clipped). Pass the gained float through tanh: near-linear x3 for normal speech, smooth saturation on peaks instead of harsh clipping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Driver: add a capture-activity counter (advances on each MicPipeRead) exposed via IOCTL_LIBREPODS_MIC_STATUS (0x00226004). Tray: open the exclusive mic-pipe handle once and share it between the receive thread (writes) and a new poll thread. The poll reads the counter every 500 ms; when an app starts recording from the virtual mic it auto-sends START_AUDIO (hi-res on), and when it stops (debounced ~1.5 s) it sends STOP_AUDIO + the A2DP stereo reset. No manual toggle needed: replaced the "Hi-res microphone (test)" menu item with an auto-updating "Microphone: idle / recording" status line. Needs a driver reinstall for the new IOCTL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cut the settle+teardown from 1s+2s to 0.4s+1.4s so the stereo-restore reconnect is a ~1.8 s blip instead of ~3 s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- a2dp reset trimmed to 150 ms settle + 1000 ms teardown (shorter playback blip; the reconnect handshake itself is Windows/BT and can't be sped up). - Menu: "Auto-enable on recording" checkbox (default on) + a "Hi-res microphone (manual)" override. In manual mode the poll stops auto-managing and the user drives the mic directly (with the same STOP + stereo reset on off). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Manual mic-off now uses the same 1.5 s debounce as auto (keeps decoding the tail before STOP), so it no longer clips the end of a recording. - Show "Restoring stereo…" through the whole A2DP reconnect and "Stereo restored" when it finishes (both auto and manual), and bump the overlay auto-hide to 4.5 s so the card stays up until the switch completes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nect Manual mic-off debounce 1.5s -> 3.5s (more tail margin, per feedback). a2dp reset now waits ~1.5s after ENABLE for the reconnect handshake before returning, so the 'Restoring stereo...' card stays up through the whole reconnect and 'Stereo restored' lands when the stereo is actually back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the BtbN full FFmpeg DLLs with a minimal FFmpeg 7.1 build (LGPL, only the AAC decoder: --disable-everything --enable-decoder=aac...). avcodec 69 MB -> 0.7 MB; the three DLLs total ~1.6 MB (was ~73 MB), small enough to commit so the repo is self-contained. Re-vendored include/ + import libs + DLLs (avcodec-61, avutil-59, swresample-5); the tray relinks against them unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the capture endpoint from the sample's "CustomName2 (AudioCodec Device)" to "LibrePods" (+ DeviceDesc "LibrePods", FriendlyName "LibrePods Microphone"). A device-agnostic static name — correct whether the user has AirPods, Beats, or any Apple-protocol accessory. The exact per-device dynamic name (matching the connected device) needs the IPolicyConfig runtime rename (crashes on Win11 without a debugger to fix the vtable) — left as a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eds bFxStore) The AV was the SetPropertyValue signature: it takes a leading BOOL bFxStore (PCWSTR, BOOL, const PROPERTYKEY&, PROPVARIANT*), not the 3-arg form. With that fixed the call returns cleanly — HRESULT 0x80070005 (E_ACCESSDENIED): renaming an audio endpoint's friendly name needs elevation. So the rename works; it just has to run elevated (a scheduled task will automate it without UAC). Also match the new "LibrePods" mic name + log each step to a file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IPolicyConfig::SetPropertyValue(FriendlyName) returns E_ACCESSDENIED even elevated (the property is protected against that path), so the registry write is the reliable rename. Update the endpoint match for the new 'LibrePods' name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SSDENIED)
IPolicyConfig::SetPropertyValue(FriendlyName) returns E_ACCESSDENIED even when
elevated, so write the endpoint's PKEY_Device_FriendlyName registry blob directly
(VT_LPWSTR: 0x1F 00 00 00 + UTF-16LE name + NUL) under
MMDevices\Audio\Capture\{endpoint}\Properties, then restart AudioEndpointBuilder.
The endpoint GUID comes from IMMDevice::GetId (trailing brace group). Must run
elevated. This is the reliable, byte-controlled rename (vs the finicky PowerShell
blob decode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The registry FriendlyName write was correct (verified: reads back 'AirPods Pro de Pedro'), but reinstalls leave several stale LibrePods capture endpoints and the tool renamed only the first match — a stale one, not the active endpoint Windows shows. Now it matches on the constant DeviceDesc and renames every LibrePods endpoint, so the active one is always covered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes made the rename finally show in Sound/Discord:
1. Format: the Sound UI's Rename writes PKEY_Device_DeviceDesc ({a45c254e},2)
as a plain REG_SZ string, NOT a REG_BINARY PROPVARIANT blob (which Windows
ignores -> display falls back to the INF name). We now write REG_SZ.
2. Matching: identify our endpoint by STABLE, name-independent properties
(device name {b3f8fa53},6 == 'LibrePods' from the INF, or hardware id
{a8b865dd},8 contains 'AudioCodec') instead of the display name, so re-runs
still find it after a rename.
Also: skip endpoints already named correctly and only restart AudioEndpointBuilder
when something changed (no needless audio blip). Dropped the COM/IMMDevice
dependency -> pure registry, smaller binary. Confirmed on hardware: the mic now
shows 'AirPods Pro de Pedro' in Sound > Input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tray now renames the virtual mic to whatever it's connected to, with no UAC prompt: - bt::find_airpods matches Beats too (Powerbeats/Fit Pro/Studio Buds/...), not just AirPods, since they share the AAP chip/endpoint -> dynamic name. - New rename module: on launch it writes the device name to %LOCALAPPDATA%\LibrePods\micname.txt and fires an elevated on-demand scheduled task (schtasks /run) that runs lp-mic-rename with highest privileges. - lp-mic-rename reads the name from that file when no CLI arg is given, and is idempotent (skips + no service restart when already named), so firing it every launch is cheap. - install.ps1 copies lp-mic-rename.exe and registers the 'LibrePods Rename Mic' task (RunLevel Highest, interactive user, on-demand). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganize crossplatform/README.md by category (Control & status / Audio / App & UX), add a dedicated hi-res microphone section (LibrePodsMic virtual audio driver, AAC-ELD decode, auto-activate, A2DP reset, gain, dynamic name, minimal FFmpeg), add the microphone row to the platform table, mention the second driver + lp-mic-rename in the layout, and add a driver-signing note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Native Windows hi-res AirPods microphone 🎙️
Exposes the AirPods' hi-res AAC-ELD microphone as a native Windows input,
so any app (Teams, Zoom, Discord, OBS…) can use it — the Windows counterpart to
the Linux side's PR #655,
built with a self-contained virtual audio driver instead of PipeWire.
Validated end-to-end on real AirPods Pro (voice recorded clean & in tune).
What's in it
New driver —
LibrePodsMic(windows-driver/LibrePodsMic/): a capture-onlyvirtual audio device (ACX, based on the MS AudioCodec sample). A spin-locked PCM
ring + control device
\\.\LibrePodsMic(IOCTLs for PCM write + a capture-activitycounter) lets user mode feed it audio.
Tray pipeline (
windows-app/librepods-tray/): reads the AAP0x58uplinkpackets, decodes AAC-ELD via an FFmpeg/libavcodec C shim (LGPL), resamples
64 kHz → 48 kHz, and streams PCM into the driver.
Plug-and-play polish (Phase 4):
and off (debounced) when it stops, via the driver's capture counter. + manual mode.
AudioSink service to restore stereo when the mic stops.
("AirPods Pro de …", and Beats too), set automatically with no UAC prompt
via
lp-mic-rename+ an elevated on-demand scheduled task the tray triggers.Key reverse-engineering findings
AU timestamp = a 24 kHz clock over 7.5 ms frames) — decoding as 48 k played deep
& clicky.
PKEY_Device_DeviceDescas a plainREG_SZ string (not FriendlyName, not a REG_BINARY PROPVARIANT blob) — that was
the last blocker for the dynamic name.
Notes
crossplatform/Windows paths.crossplatform/README.md(regrouped + a hi-res mic section),crossplatform/hires-mic/PLAN.md.