Files
alan b93327dc26 Ask for the headset profile at capture start; relaunch the daemon on update
The daemon now requests the Bluetooth headset profile itself (pactl
set-card-profile) the moment a take starts, instead of waiting for
WirePlumber to notice the stream and run its switch timer. The microphone
link comes up 0.15-0.18 s after the key instead of 0.21-0.27 s; WirePlumber
still restores A2DP when the stream goes away. First real audio is now at
0.82-0.86 s, which is the headset's own floor.

When the plugin is updated while the daemon runs, the daemon sees its source
change, exits with code 4 once idle, and the shell service relaunches it
without counting that as a crash.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xoc9DJCViR7dg9eKzzAcfQ
2026-09-09 12:17:06 +01:00

2.9 KiB
Raw Permalink Blame History

How we cut Bluetooth microphone startup time

Measured on a Sony WH-1000XM5 over an Intel AX210, PipeWire 1.6.8, WirePlumber 0.5.17, kernel 7.2.3. Times are after the dictation key press.

before after
mic link (eSCO) up 0.71.0 s, sometimes never 0.150.18 s
first real audio at the daemon 1.51.7 s, or never 0.820.86 s
"Listening" in the bar 2.1 s on a timer, even with no audio 0.860.94 s, only on real audio

"After" is with the daemon recording straight from bluez_input.<addr>; via camera-effects-mic add ~0.25 s (its 250 ms poll and processing).

What changed

Change Where it lives Gain
Daemon indicator: green on real audio only (RMS > 0.0005 or three consecutive non-silent chunks), no timer daemon/sttd.py the bar cannot claim to listen to a dead mic
Daemon asks for the headset profile itself at capture start (pactl set-card-profile), before WirePlumber notices the stream daemon/sttd.py link up ~0.1 s sooner (0.210.27 s → 0.150.18 s); WirePlumber still restores A2DP
Record from bluez_input.<addr> instead of camera-effects-mic ~/.config/speech-to-text/config.json ~0.25 s sooner, no noise suppression on dictation
WirePlumber profile-switch timeout 500 ms → 50 ms ~/.local/share/wireplumber/scripts/device/autoswitch-bluetooth-profile.lua link up ~0.45 s sooner
A2DP auto-connect rule narrowed to the XM5 ~/.config/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf no 5 s pages of absent devices at login
btusb driver patch 0002 /usr/lib/modules/<kernel>/updates/btusb.ko mic link no longer comes up silent when a page overlaps it
PipeWire bluez5 patches 0001 + 0003 ~/.local/lib/spa-0.2/bluez5/ via SPA_PLUGIN_DIR drop-ins no stale-error dead takes; back-to-back takes work

Only the first two rows are part of this repo. The rest are machine-level and are described, with patches and measurements, in HANDOFF.md.

Why ~0.85 s is the floor

key press
0.00 s ─┬─ daemon asks for the headset profile, starts pw-record
        │
0.07 s ─┼─ eSCO connect() reaches the kernel
        │   headset leaves sniff mode, radio sets up the link
0.16 s ─┼─ eSCO link up
        │
        │   headset sends encoded digital silence while its own
        │   mic path starts and mSBC syncs: ~0.6 s
        │   (measured at the decoder; Linux cannot shorten it)
        │
0.80 s ─┼─ first non-zero samples leave the headset
        │   decode + 50 ms chunking
0.85 s ─┴─ first real audio reaches the daemon

Nothing measurable is left on the Linux side.

The headset's silence after link-up and the eSCO setup are hardware. Words spoken in the first ~0.9 s are lost unless the microphone is already in hands-free mode, which drops playback to mono, so it is off by default (warmMic).