Bluetooth mic startup: honest Listening indicator, startup-time investigation
The bar turned green after 2 s on a timer even when the microphone delivered nothing. It now turns green only on real audio: RMS above 0.0005, or three consecutive chunks that are not digital silence (the virtual mic emits one stray nonzero chunk right after start, and a Bluetooth headset's floor ramps in from a few LSB). Measured 1.2-1.4 s after the key, within 0.14 s of the first real samples. diagnostics/bluetooth/ records how startup went from ~1.6 s (or never) to ~1.1-1.4 s: a btusb driver bug, two PipeWire bluez5 bugs, a 500 ms WirePlumber switch timeout and an over-broad auto-connect rule. Those fixes are machine level and live outside this repo; the patches, tools and measurements are here. STARTUP-TIME.md is the summary and explains the ~0.9 s hardware floor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xoc9DJCViR7dg9eKzzAcfQ
This commit is contained in:
@@ -1,2 +1,7 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
diagnostics/**/*.ko
|
||||||
|
diagnostics/**/*.so
|
||||||
|
diagnostics/**/*.log
|
||||||
|
diagnostics/**/*.btsnoop
|
||||||
|
__pycache__/
|
||||||
|
|||||||
+9
-3
@@ -528,6 +528,7 @@ class Recorder:
|
|||||||
self.error = ""
|
self.error = ""
|
||||||
self.listening = False # real audio is arriving (a Bluetooth mic sends silence while it switches profile)
|
self.listening = False # real audio is arriving (a Bluetooth mic sends silence while it switches profile)
|
||||||
self.chunks = 0
|
self.chunks = 0
|
||||||
|
self.nonzero_run = 0 # consecutive chunks that were not digital silence
|
||||||
self.standby = False # warm mode: the stream runs but only the last second is kept
|
self.standby = False # warm mode: the stream runs but only the last second is kept
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -558,9 +559,14 @@ class Recorder:
|
|||||||
rms = math.sqrt(sum(s * s for s in samples) / max(1, n)) / 32768.0
|
rms = math.sqrt(sum(s * s for s in samples) / max(1, n)) / 32768.0
|
||||||
level = min(1.0, math.sqrt(rms * 12.0)) # perceptual-ish: speech at normal level fills most of the bar
|
level = min(1.0, math.sqrt(rms * 12.0)) # perceptual-ish: speech at normal level fills most of the bar
|
||||||
self.chunks += 1
|
self.chunks += 1
|
||||||
# Green means "your voice is getting through": any real signal (room noise counts),
|
# Green means "your voice is getting through". Clear input (speech, room noise on most
|
||||||
# or 2 s of chunks for a microphone that is digitally silent.
|
# mics) turns it on at once. Otherwise wait for three consecutive chunks that are not
|
||||||
if rms > 0.0005 or self.chunks >= 40:
|
# digital silence: a Bluetooth headset delivers exact zeros until its microphone link
|
||||||
|
# is up, the virtual mic emits one stray nonzero chunk right after start (processing
|
||||||
|
# residue), and the headset's own floor then ramps in from a few LSB with the odd
|
||||||
|
# all-zero chunk. No time-based fallback: a mic that only sends zeros is not listening.
|
||||||
|
self.nonzero_run = self.nonzero_run + 1 if rms > 0 else 0
|
||||||
|
if rms > 0.0005 or self.nonzero_run >= 3:
|
||||||
self.listening = True
|
self.listening = True
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.buf += data
|
self.buf += data
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
--- a/spa/plugins/bluez5/backend-native.c
|
||||||
|
+++ b/spa/plugins/bluez5/backend-native.c
|
||||||
|
@@ -2835,6 +2835,11 @@
|
||||||
|
|
||||||
|
if (td->err != -EINPROGRESS)
|
||||||
|
sco_ready(t);
|
||||||
|
+ else
|
||||||
|
+ /* A second node can acquire the same transport while connect() is
|
||||||
|
+ * pending. Do not let it re-emit an error from the previous attempt.
|
||||||
|
+ */
|
||||||
|
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING);
|
||||||
|
|
||||||
|
return 0;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
From: Alan Silva <alanfortlink@gmail.com>
|
||||||
|
Subject: [PATCH] Bluetooth: btusb: switch the isoc interface on every SCO enable notification
|
||||||
|
|
||||||
|
hci_conn_num(hdev, SCO_LINK) also counts (e)SCO links that are still being
|
||||||
|
set up, since hci_connect_sco() adds them to the connection hash before the
|
||||||
|
Synchronous Connection Complete event arrives. btusb_notify() only compares
|
||||||
|
that count with data->sco_num and stores whatever event it was called with
|
||||||
|
as the air mode. Two things go wrong when an unrelated connection event
|
||||||
|
arrives while an (e)SCO link is pending:
|
||||||
|
|
||||||
|
1. The unrelated event (e.g. HCI_NOTIFY_CONN_DEL from a failed ACL page to
|
||||||
|
another device) changes the count from 0 to 1, so btusb_work() runs
|
||||||
|
with air_mode = HCI_NOTIFY_CONN_DEL, computes new_alts = 0 and submits
|
||||||
|
isochronous URBs on the alternate setting 0 endpoints, whose
|
||||||
|
wMaxPacketSize is 0. usb_submit_urb() fails with -EMSGSIZE:
|
||||||
|
Bluetooth: hci0: urb 00000000d42f828b submission failed (90)
|
||||||
|
and BTUSB_ISOC_RUNNING is cleared again.
|
||||||
|
|
||||||
|
2. When HCI_NOTIFY_ENABLE_SCO_TRANSP finally arrives for that link, the
|
||||||
|
count already equals data->sco_num, so nothing is scheduled. The
|
||||||
|
interface stays on alternate setting 0, no isochronous URBs are
|
||||||
|
submitted, and the SCO link carries no audio in either direction for
|
||||||
|
its whole lifetime. The user sees a working headset with a dead
|
||||||
|
microphone.
|
||||||
|
|
||||||
|
Reproduced on an Intel AX210 (8087:0032) with a Sony WH-1000XM5 and
|
||||||
|
PipeWire 1.6.8: PipeWire re-tries ConnectProfile() on absent paired devices
|
||||||
|
when it starts, bluetoothd pages them, the page times out (status 0x04)
|
||||||
|
while dictation opens the headset microphone, and the mSBC link comes up
|
||||||
|
silent. Traced with kprobes on btusb_notify/btusb_work/btusb_submit_isoc_urb.
|
||||||
|
|
||||||
|
Program the alternate setting on every ENABLE_SCO_* notification, and on
|
||||||
|
other events only react when links went away.
|
||||||
|
---
|
||||||
|
--- a/drivers/bluetooth/btusb.c 2026-09-09 02:54:49.664029766 +0100
|
||||||
|
+++ b/drivers/bluetooth/btusb.c 2026-09-09 02:55:52.408391149 +0100
|
||||||
|
@@ -2289,13 +2289,33 @@
|
||||||
|
static void btusb_notify(struct hci_dev *hdev, unsigned int evt)
|
||||||
|
{
|
||||||
|
struct btusb_data *data = hci_get_drvdata(hdev);
|
||||||
|
+ int sco_num = hci_conn_num(hdev, SCO_LINK);
|
||||||
|
|
||||||
|
BT_DBG("%s evt %d", hdev->name, evt);
|
||||||
|
|
||||||
|
- if (hci_conn_num(hdev, SCO_LINK) != data->sco_num) {
|
||||||
|
- data->sco_num = hci_conn_num(hdev, SCO_LINK);
|
||||||
|
+ switch (evt) {
|
||||||
|
+ case HCI_NOTIFY_ENABLE_SCO_CVSD:
|
||||||
|
+ case HCI_NOTIFY_ENABLE_SCO_TRANSP:
|
||||||
|
+ /* A (e)SCO link just came up: program the isochronous
|
||||||
|
+ * alternate setting for its air mode, even if the link was
|
||||||
|
+ * already counted while it was still being set up.
|
||||||
|
+ */
|
||||||
|
+ data->sco_num = sco_num;
|
||||||
|
data->air_mode = evt;
|
||||||
|
schedule_work(&data->work);
|
||||||
|
+ break;
|
||||||
|
+ default:
|
||||||
|
+ /* hci_conn_num() also counts (e)SCO links that are still
|
||||||
|
+ * being set up, and only the ENABLE_SCO_* notifications
|
||||||
|
+ * carry an air mode. Any other event may only tear the
|
||||||
|
+ * isochronous interface down or adjust it for fewer links;
|
||||||
|
+ * a pending link is handled once it is enabled.
|
||||||
|
+ */
|
||||||
|
+ if (sco_num < data->sco_num) {
|
||||||
|
+ data->sco_num = sco_num;
|
||||||
|
+ schedule_work(&data->work);
|
||||||
|
+ }
|
||||||
|
+ break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
--- a/spa/plugins/bluez5/media-sink.c
|
||||||
|
+++ b/spa/plugins/bluez5/media-sink.c
|
||||||
|
@@ -152,6 +152,7 @@
|
||||||
|
unsigned int start_ready:1;
|
||||||
|
unsigned int transport_started:1;
|
||||||
|
unsigned int following:1;
|
||||||
|
+ bool transport_wrote; /* data left for the remote since transport start (HFP: only after RX was seen) */
|
||||||
|
unsigned int is_output:1;
|
||||||
|
unsigned int flush_pending:1;
|
||||||
|
unsigned int iso_pending:1;
|
||||||
|
@@ -748,6 +749,8 @@
|
||||||
|
written = spa_bt_send(this->flush_source.fd, this->buffer, this->buffer_used,
|
||||||
|
&this->tx_latency, SPA_TIMESPEC_TO_NSEC(&ts_pre));
|
||||||
|
}
|
||||||
|
+ if (written > 0)
|
||||||
|
+ this->transport_wrote = true;
|
||||||
|
|
||||||
|
if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
|
||||||
|
struct timespec ts;
|
||||||
|
@@ -1517,6 +1520,7 @@
|
||||||
|
struct impl *this = user_data;
|
||||||
|
|
||||||
|
this->transport_started = true;
|
||||||
|
+ this->transport_wrote = false;
|
||||||
|
if (this->transport->iso_io)
|
||||||
|
spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this);
|
||||||
|
return 0;
|
||||||
|
@@ -2466,7 +2470,8 @@
|
||||||
|
else
|
||||||
|
transport_stop(this);
|
||||||
|
|
||||||
|
- if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && was_started && !this->is_duplex && this->is_output) {
|
||||||
|
+ if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && was_started && !this->is_duplex && this->is_output &&
|
||||||
|
+ !(this->codec->kind == MEDIA_CODEC_HFP && this->transport_wrote)) {
|
||||||
|
/*
|
||||||
|
* If establishing connection fails due to remote end not activating
|
||||||
|
* the transport, we won't get a write error, but instead see a transport
|
||||||
|
@@ -2474,6 +2479,13 @@
|
||||||
|
*
|
||||||
|
* Treat this as a transport error, so that upper levels don't try to
|
||||||
|
* retry too often.
|
||||||
|
+ *
|
||||||
|
+ * An HFP link that already carried audio both ways (sco-io only writes
|
||||||
|
+ * after the first packet came in) and then hangs up is not a failed
|
||||||
|
+ * activation: headsets drop the SCO link when the profile switches back
|
||||||
|
+ * to A2DP after every call. Counting that as an error made
|
||||||
|
+ * spa_bt_transport_acquire() refuse the next acquire once three such
|
||||||
|
+ * hangups landed within its error window.
|
||||||
|
*/
|
||||||
|
|
||||||
|
spa_log_debug(this->log, "%p: transport %p becomes inactive: stop and indicate error",
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
# XM5 dictation startup: investigation handoff
|
||||||
|
|
||||||
|
Date: 2026-09-09. Status: **fixed and installed** (see the resolution section
|
||||||
|
below). The older sections are kept as the investigation record.
|
||||||
|
|
||||||
|
## Resolution, 2026-09-09 afternoon session
|
||||||
|
|
||||||
|
Three independent faults were reproduced, fixed and re-measured with
|
||||||
|
`tools/trial2.py` (kernel kprobes with the `mono` trace clock, the daemon's own
|
||||||
|
level stream, the card profile before/during/after each take). Everything below
|
||||||
|
is installed on this machine; nothing was submitted upstream.
|
||||||
|
|
||||||
|
### 1. Silent first start: btusb alternate-setting bug (patch `0002`)
|
||||||
|
|
||||||
|
Reproduced on the first stock attempt (`tools/stock-repro-1.log`): eSCO
|
||||||
|
`connect()` at 0.76 s stuck behind two pages, `btusb_switch_alt_setting(0)` and
|
||||||
|
the isochronous submit failure at 0.95 s, link up at 6.19 s with **zero** SCO
|
||||||
|
URBs for its whole life. With the patched module the exact same race
|
||||||
|
(`tools/patched-repro-3.log`: eSCO request stalled 3 s behind a page timeout,
|
||||||
|
`HCI_NOTIFY_CONN_DEL` ignored, alt setting 6 programmed on
|
||||||
|
`ENABLE_SCO_TRANSP`) delivered audio.
|
||||||
|
|
||||||
|
Installed: `/usr/lib/modules/7.2.3-arch1-3/updates/btusb.ko` (+ `depmod`);
|
||||||
|
`modinfo -n btusb` resolves there. btusb is not in the UKI, so no initramfs
|
||||||
|
rebuild was needed. A kernel upgrade replaces the whole modules directory and
|
||||||
|
silently returns to the stock driver; rebuild `tools/btusb.c` against the new
|
||||||
|
kernel and reinstall. `/etc/modprobe.d` needs nothing.
|
||||||
|
|
||||||
|
The trigger was `~/.config/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf`
|
||||||
|
matching every card. It is now narrowed to `bluez_card.88_C9_E8_A7_EC_7E`.
|
||||||
|
Disabling it entirely was tried first and is wrong: after a WirePlumber restart
|
||||||
|
the XM5's A2DP profile does not come back on its own (the card offered only
|
||||||
|
`headset-head-unit`), so the XM5 needs the rule. Absent devices are no longer
|
||||||
|
paged (`journalctl -u bluetooth` shows no "Host is down" after a restart).
|
||||||
|
|
||||||
|
### 2. Autoswitch delay: 50 ms script now in effect
|
||||||
|
|
||||||
|
`~/.local/share/wireplumber/scripts/device/autoswitch-bluetooth-profile.lua`
|
||||||
|
(the packaged script with `PROFILE_SWITCH_TIMEOUT_MSEC = 50`). eSCO connect
|
||||||
|
moved from 0.65–0.85 s to 0.26–0.49 s after the start command. The
|
||||||
|
ineffective copy under `~/.config/wireplumber/scripts/device/` is still there
|
||||||
|
(this session could not delete it); it is harmless.
|
||||||
|
|
||||||
|
### 3. PipeWire: stale error replay and a hangup counted as failure (patches `0001`, `0003`)
|
||||||
|
|
||||||
|
With 1 and 2 in place, 1 take in 3 still came up silent with the stock plugin:
|
||||||
|
`bluez_input ... running -> error` right at acquire, the `0001` replay
|
||||||
|
(`tools/normal-5.log`, journal 11:26:46). `0001` is a real, independent bug,
|
||||||
|
not a symptom of the paging.
|
||||||
|
|
||||||
|
With `0001` alone, the 4th of 4 quick takes failed with
|
||||||
|
`spa.audioadapter: can't send command 2: Input/output error` and no eSCO
|
||||||
|
request at all (`tools/pw-4.log`). Cause: every take ends with
|
||||||
|
`Failure in Bluetooth audio transport .../fd60` because `media-sink.c`
|
||||||
|
converts the SCO hangup (the headset drops SCO when the profile goes back to
|
||||||
|
A2DP) into `SPA_BT_TRANSPORT_STATE_ERROR`, and `spa_bt_transport_acquire()`
|
||||||
|
returns `-EIO` once `error_count >= 3` within a 6 s window
|
||||||
|
(`TRANSPORT_ERROR_TIMEOUT = 2 * BLUEZ_ACTION_RATE_MSEC`). Four takes each
|
||||||
|
started within ~8 s of the previous one ending is enough. Patch `0003` skips
|
||||||
|
that escalation for an HFP sink that already wrote data (sco-io only writes
|
||||||
|
after the first packet came in, so this implies the link was really up).
|
||||||
|
|
||||||
|
Installed: `~/.local/lib/spa-0.2/bluez5/libspa-bluez5.so` (PipeWire 1.6.8 +
|
||||||
|
`0001` + `0003`, built with `--prefix=/usr` so `bluez-hardware.conf` resolves
|
||||||
|
to `/usr/share`), selected by
|
||||||
|
`~/.config/systemd/user/{pipewire,wireplumber}.service.d/spa-plugin-dir.conf`
|
||||||
|
(`SPA_PLUGIN_DIR=/home/tank/.local/lib/spa-0.2:/usr/lib/spa-0.2`). Codec
|
||||||
|
plugins still come from `/usr/lib/spa-0.2/bluez5`. WirePlumber is the process
|
||||||
|
that maps the plugin. After a PipeWire upgrade rebuild against the new source
|
||||||
|
(the build needs `gdbus-codegen`, extracted from `glib2-devel` without
|
||||||
|
installing it, and a `gio-2.0.pc` copy pointing at it) or delete the two
|
||||||
|
drop-ins to fall back to the packaged plugin. A copy of the built plugin is in
|
||||||
|
`tools/libspa-bluez5.so`.
|
||||||
|
|
||||||
|
Result: 8 of 8 rapid takes (`tools/pw2-*.log`) and 6 more after that with no
|
||||||
|
transport failure, no start error, A2DP restored 2 s after every take.
|
||||||
|
|
||||||
|
### 4. Honest "Listening" indicator (`daemon/sttd.py`)
|
||||||
|
|
||||||
|
The 40-chunk fallback lit the indicator at 2.1 s on a dead microphone. The
|
||||||
|
level stream showed why a plain "first nonzero chunk" test is wrong too: the
|
||||||
|
virtual mic emits one stray nonzero chunk at ~0.15 s (level 0.02, processing
|
||||||
|
residue), then exact zeros until the headset floor ramps in from a few LSB
|
||||||
|
with the odd all-zero chunk (`tools/win-*.log`). The daemon now turns
|
||||||
|
`listening` on at RMS > 0.0005 or after three consecutive nonzero chunks, and
|
||||||
|
never on a timer. Measured: 1.19–1.43 s after the start command, 0–0.14 s
|
||||||
|
after the first nonzero chunk (`tools/ind2-*.log`).
|
||||||
|
|
||||||
|
### Timeline now (normal path, quiet room)
|
||||||
|
|
||||||
|
| after F13 | event |
|
||||||
|
|---|---|
|
||||||
|
| 0.26–0.49 s | eSCO link up |
|
||||||
|
| 1.06–1.43 s | first nonzero PCM at the daemon |
|
||||||
|
| 1.19–1.43 s | UI "Listening" |
|
||||||
|
|
||||||
|
The 0.7–0.9 s between link-up and first PCM is the headset's own silence plus
|
||||||
|
the Camera Effects path; see the earlier analysis. The remaining Linux-side
|
||||||
|
gain would be requesting the headset profile from the daemon at capture start
|
||||||
|
(~0.25 s), not done: the autoswitch script only restores a profile it switched
|
||||||
|
itself, so a daemon-side switch would leave the headset in mono after the take.
|
||||||
|
|
||||||
|
### Not fixable
|
||||||
|
|
||||||
|
Stereo playback while the headset microphone is in use: A2DP is one-way and
|
||||||
|
the WH-1000XM5 has no LE Audio. Every OS drops to mono HFP for its mic.
|
||||||
|
|
||||||
|
### Files added this session
|
||||||
|
|
||||||
|
`0003-media-sink-hfp-hangup-after-audio-is-not-a-failure.patch`,
|
||||||
|
`tools/trial2.py` (trial driver, replaces `trial.sh`, which needed a `/tmp`
|
||||||
|
script that is gone), `tools/libspa-bluez5.so`, `tools/*.log`.
|
||||||
|
`tools/kernel-probes.sh` now disables the events before rewriting them, so it
|
||||||
|
can be re-run after a module reload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Earlier record (superseded where the resolution above says so)
|
||||||
|
|
||||||
|
## Update, 2026-09-09 later session: silent first start root-caused
|
||||||
|
|
||||||
|
Status: **the "first microphone start gets no audio at all" failure is fully
|
||||||
|
explained and reproduced under kernel tracing; a kernel driver patch is written and
|
||||||
|
built but not loaded. The remaining startup delay is measured and its floor is set
|
||||||
|
by the headset, not by Linux.**
|
||||||
|
|
||||||
|
### The silent start is a btusb driver bug, triggered by paging absent devices
|
||||||
|
|
||||||
|
Reproduced twice out of two attempts by restarting WirePlumber and starting
|
||||||
|
dictation about 10 s later. Kernel event order (kprobes, see `tools/`):
|
||||||
|
|
||||||
|
1. PipeWire `connect()`s the mSBC SCO socket: `hci_conn_add_unset(ESCO)` puts the
|
||||||
|
pending link in the connection hash (counted by `hci_conn_num(SCO_LINK)`), and
|
||||||
|
the Enhanced Setup Synchronous Connection command is queued.
|
||||||
|
2. The command does **not** go out for 2.3 s: bluetoothd is paging another paired
|
||||||
|
device at that moment, and the kernel's synchronous Create Connection request
|
||||||
|
holds the HCI command queue until its Connect Complete arrives (page timeout,
|
||||||
|
~5.1 s). Any dictation start that overlaps such a page stalls the same way.
|
||||||
|
3. Connect Complete arrives with status 0x04 (Page Timeout) for that other device;
|
||||||
|
its `hci_conn_del()` notifies btusb with `HCI_NOTIFY_CONN_DEL`.
|
||||||
|
4. `btusb_notify()` only compares `hci_conn_num(SCO_LINK)` (now 1, the pending
|
||||||
|
link) with its `data->sco_num` (0), stores CONN_DEL as the "air mode" and runs
|
||||||
|
`btusb_work()`, which computes `new_alts = 0` and submits isochronous URBs on the
|
||||||
|
alternate-setting-0 endpoints (wMaxPacketSize 0). `usb_submit_urb()` returns
|
||||||
|
-EMSGSIZE: that is the `urb ... submission failed (90)` line. The `len 0 mtu 0`
|
||||||
|
debug print from `__fill_isoc_descriptor` confirms it.
|
||||||
|
5. 0.14 s later the eSCO link comes up (Synchronous Connect Complete, air mode
|
||||||
|
transparent) and `HCI_NOTIFY_ENABLE_SCO_TRANSP` is sent, but the count already
|
||||||
|
matches `data->sco_num`, so nothing is scheduled. The interface stays on
|
||||||
|
alternate setting 0 for the life of the link: no SCO URBs, no audio either way.
|
||||||
|
PipeWire waits for the first incoming packet (USB adapters) forever, the daemon
|
||||||
|
sees silence, and "Listening" appears only from the 40-chunk fallback.
|
||||||
|
|
||||||
|
All four historical `submission failed (90)` entries (00:49, 00:53, 01:39, 02:17)
|
||||||
|
sit 2–5 s after a WirePlumber restart, and the "physical HFP nodes enter error"
|
||||||
|
failures in the stock trials are the same event seen from PipeWire (the pending
|
||||||
|
SCO connect fails with the paging device's status). The `0001` PipeWire patch
|
||||||
|
therefore addresses a symptom of this, not an independent bug.
|
||||||
|
|
||||||
|
Why a page was in flight: the user's own
|
||||||
|
`~/.config/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf` sets
|
||||||
|
`bluez5.auto-connect = [ a2dp_sink a2dp_source ]`. On every WirePlumber start
|
||||||
|
PipeWire calls `ConnectProfile()` on every paired A2DP device that is not connected
|
||||||
|
("Nothing Ear (3)" and "RB Meta 042Q" here), bluetoothd pages each for 5.1 s, and
|
||||||
|
the whole HCI command queue is blocked while it does. In normal use that is the
|
||||||
|
first ~11 s after login or after any WirePlumber restart; every absent paired
|
||||||
|
device extends it by ~5 s. Dropping that rule, or narrowing its match to the
|
||||||
|
devices it was written for, removes the trigger. The btusb bug itself fires on
|
||||||
|
any non-SCO connection add/remove that lands while an eSCO link is pending (an
|
||||||
|
LE device connecting, another ACL dropping), so it stays worth fixing.
|
||||||
|
|
||||||
|
Fix: `0002-btusb-program-isoc-alt-setting-on-every-sco-enable.patch` changes
|
||||||
|
`btusb_notify()` to program the interface on every `ENABLE_SCO_*` notification and
|
||||||
|
to ignore count increases from other events. `tools/btusb.ko` is that patch built
|
||||||
|
against the exact stable 7.2.3 source (identical to the local driver source) with
|
||||||
|
the installed headers, vermagic `7.2.3-arch1-3 SMP preempt mod_unload`. It is
|
||||||
|
**not loaded**; test with `sudo rmmod btusb && sudo insmod tools/btusb.ko`
|
||||||
|
(headset reconnects), revert with `sudo rmmod btusb && sudo modprobe btusb`.
|
||||||
|
Not submitted upstream.
|
||||||
|
|
||||||
|
### Normal-path timing, stock, ACL in sniff mode (trace of 02:49:34.93)
|
||||||
|
|
||||||
|
| after F13 | event |
|
||||||
|
|---|---|
|
||||||
|
| 0.01 s | daemon recording, pw-record running |
|
||||||
|
| 0.28 s | Camera Effects helper stream running, autoswitch script triggered |
|
||||||
|
| 0.79 s | autoswitch 500 ms timer fires, profile set to headset-head-unit |
|
||||||
|
| 0.82 s | SCO `connect()`; Exit Sniff Mode, Mode Change 65 ms later |
|
||||||
|
| 1.01 s | eSCO up (Enhanced Setup Sync took 117 ms); first SCO packet 19 ms later |
|
||||||
|
| ~1.2 s | first decoded PCM, all zeros (earlier instrumented runs) |
|
||||||
|
| 1.61 s | first nonzero input at Camera Effects; 1.72 s first nonzero daemon level |
|
||||||
|
| 2.07 s | UI "Listening" (40-chunk fallback; room noise is below the RMS threshold) |
|
||||||
|
|
||||||
|
The 0.5–0.6 s between eSCO link-up and the first nonzero sample is the headset:
|
||||||
|
it sends encoded digital silence for roughly 300 ms after the link opens, plus
|
||||||
|
mSBC sync. Linux cannot shorten it. What Linux can shorten:
|
||||||
|
|
||||||
|
- The 500 ms autoswitch timer (`PROFILE_SWITCH_TIMEOUT_MSEC` in
|
||||||
|
`/usr/share/wireplumber/scripts/device/autoswitch-bluetooth-profile.lua`):
|
||||||
|
~0.45 s. A copy with 50 ms placed at
|
||||||
|
`~/.config/wireplumber/scripts/device/autoswitch-bluetooth-profile.lua` was
|
||||||
|
**not** picked up (measured: still 500 ms). It is still there; remove it, or move
|
||||||
|
it to `~/.local/share/wireplumber/scripts/device/` (where the user's other
|
||||||
|
script lives) and re-measure the gap between `Triggering profile switch` and
|
||||||
|
`Switching profile` in the WirePlumber journal at info level.
|
||||||
|
- The ~0.27 s before the profile switch is even requested (Camera Effects 250 ms
|
||||||
|
maintenance poll opening the helper stream, then WirePlumber noticing it).
|
||||||
|
Requesting the headset profile directly when capture starts would remove most
|
||||||
|
of it.
|
||||||
|
|
||||||
|
With both, eSCO link-up lands near 0.3–0.35 s and first real audio near 0.9 s.
|
||||||
|
That is the floor with this headset. Words spoken before ~0.9 s are lost unless
|
||||||
|
the microphone is already in HFP, which the user has rejected; the honest
|
||||||
|
alternative is an indicator that turns on only when nonzero physical input
|
||||||
|
arrives, which the current 0.0005 RMS threshold and 40-chunk fallback do not give
|
||||||
|
(quiet-room noise measured RMS ~1e-4, Camera Effects residue ~4e-5).
|
||||||
|
|
||||||
|
### Stereo playback while the microphone is used
|
||||||
|
|
||||||
|
Not possible with this headset on any OS. A2DP is one-way; the microphone
|
||||||
|
needs an HFP SCO link, and the WH-1000XM5 has no LE Audio. Windows and macOS drop
|
||||||
|
to the same mono hands-free profile when its microphone is used.
|
||||||
|
|
||||||
|
### State after this session
|
||||||
|
|
||||||
|
Stock btusb loaded (srcversion matches the packaged module), all kprobes and
|
||||||
|
dynamic-debug prints removed, no btmon running, WirePlumber restarted with default
|
||||||
|
logging and `WIREPLUMBER_DEBUG` unset, dictation idle, warm mic off. The only
|
||||||
|
persistent change is the ineffective script copy under
|
||||||
|
`~/.config/wireplumber/scripts/device/`. `tools/` holds the probe installer
|
||||||
|
(`kernel-probes.sh`, `off` to remove), the trial driver (`trial.sh`, needs
|
||||||
|
`/tmp/stt-startup-trace.py`), the restart-and-retry reproducer (`repro-loop.sh`),
|
||||||
|
and the patched driver source and module.
|
||||||
|
|
||||||
|
## Required outcome
|
||||||
|
|
||||||
|
F13 dictation must use the Sony WH-1000XM5's own microphone, start immediately
|
||||||
|
without losing opening words, and preserve headphone playback quality. The user
|
||||||
|
rejects substituting the webcam microphone and keeping HFP/mic capture permanently
|
||||||
|
open. Do not compare this with Deadlock. Mac/Windows microphone selection was not
|
||||||
|
verified; do not assume that comparison establishes an identical Bluetooth path.
|
||||||
|
|
||||||
|
## Verified state at handoff
|
||||||
|
|
||||||
|
- Stock WirePlumber is active/running, with no service drop-ins and no test SPA or
|
||||||
|
data-directory environment overrides.
|
||||||
|
- Camera Effects explicitly selects `bluez_input.88:C9:E8:A7:EC:7E` (WH-1000XM5).
|
||||||
|
- Speech-to-text is idle, warm capture is false, Camera Effects is not capturing.
|
||||||
|
- No btmon, pkexec, startup trace, or patched-test process remains running.
|
||||||
|
- The earlier unwanted webcam-microphone substitution was reverted.
|
||||||
|
- No kernel/module replacement, system package installation, permanent candidate
|
||||||
|
PipeWire installation, or upstream submission was done.
|
||||||
|
- Repository status is only untracked `diagnostics/`; STT runtime source was not
|
||||||
|
changed by this investigation.
|
||||||
|
|
||||||
|
## Actual application path
|
||||||
|
|
||||||
|
`F13 -> speech-to-text/bin/stt -> daemon/sttd.py -> pw-record -> camera-effects-mic`
|
||||||
|
|
||||||
|
VoxType is the transcription engine here, not the ordinary VoxType microphone
|
||||||
|
daemon. Testing its separate CLI does not reproduce the F13 startup path.
|
||||||
|
|
||||||
|
Camera Effects creates `camera-effects-mic-input`, which links through the
|
||||||
|
WirePlumber Bluetooth loopback to the physical HFP source. A helper capture named
|
||||||
|
`camera-effects-mic-headset` triggers automatic profile switching. The user's
|
||||||
|
Camera Effects permission rule hides raw microphones from unrelated clients;
|
||||||
|
do not bypass that rule by impersonating an allowed client.
|
||||||
|
|
||||||
|
STT requests raw S16, 16 kHz, mono audio in 50 ms chunks. Its `listening` flag
|
||||||
|
becomes true when RMS exceeds 0.0005, or after 40 chunks (two seconds of PCM).
|
||||||
|
This flag controls UI readiness, not recording commencement: preceding chunks
|
||||||
|
are already collected. Therefore quiet input can show a two-second delay, and
|
||||||
|
silence from a disconnected microphone can falsely become "Listening."
|
||||||
|
|
||||||
|
Camera Effects maintenance polls every 250 ms. Its virtual stream supplies
|
||||||
|
silence before the physical mic is ready and may initially contain residual
|
||||||
|
buffered audio. First virtual buffers or early virtual nonzero samples are not
|
||||||
|
reliable proof that current headset speech is being captured. Read-only input
|
||||||
|
metering and instrumentation at the Bluetooth decoder are more meaningful.
|
||||||
|
|
||||||
|
## Measurements
|
||||||
|
|
||||||
|
Tests invoked the same daemon start command used by F13, without keyboard
|
||||||
|
dispatch. Six-second takes were cancelled, not pasted or saved as completed
|
||||||
|
history. Live transcription may nevertheless have processed data during them.
|
||||||
|
|
||||||
|
Stock observations:
|
||||||
|
|
||||||
|
- Daemon recording state begins in roughly 12–20 ms; virtual PCM around 160 ms.
|
||||||
|
- WirePlumber intentionally waits 500 ms before a headset-profile switch and
|
||||||
|
restarts that timer on further relevant graph events. Restoration waits 2 s.
|
||||||
|
- Profile change generally starts around 0.6–0.9 s.
|
||||||
|
- HCI synchronous connection completion was around 0.87–1.02 s. That is not
|
||||||
|
equivalent to microphone audio being ready.
|
||||||
|
- Some trials fail entirely: physical HFP nodes enter error, or usable audio
|
||||||
|
never arrives. A stock failing trial stopped accumulating PCM at 0.8 s.
|
||||||
|
- Kernel logs sometimes contain `Bluetooth: hci0: urb ... submission failed (90)`.
|
||||||
|
Error 90 is EMSGSIZE. Which USB submission path causes it is not yet proven.
|
||||||
|
|
||||||
|
With the candidate PipeWire fix and temporary decoder instrumentation, two
|
||||||
|
successful trials measured: incoming encoded packets at 0.97–0.99 s; first decoded
|
||||||
|
PCM at 1.16–1.19 s; first nonzero decoded PCM at 1.48–1.50 s; nonzero PCM sent to
|
||||||
|
the graph at 1.53–1.55 s; Camera Effects input signal around 1.62 s; UI readiness
|
||||||
|
around 2.14 s. This narrows the delay but does not establish speech intelligibility.
|
||||||
|
Initial zero packets versus codec synchronization versus headset startup remains
|
||||||
|
unresolved. The H2 reader itself parses a 60-byte frame without an explicit timer.
|
||||||
|
|
||||||
|
The first trial after the instrumented WirePlumber restart received no encoded
|
||||||
|
audio within six seconds, with a USB submission error around 4.46 s.
|
||||||
|
|
||||||
|
## Candidate actual PipeWire bug
|
||||||
|
|
||||||
|
An HFP transport can retain ERROR from an earlier use. `sco_acquire_cb()` starts
|
||||||
|
an asynchronous connection without changing that stale state. A second node
|
||||||
|
acquiring the same transport takes the reference-count branch in
|
||||||
|
`spa_bt_transport_acquire()` and re-emits the old ERROR, even though the new
|
||||||
|
connection subsequently succeeds.
|
||||||
|
|
||||||
|
Observed debug sequence: 01:32:25.430 acquire, .447 second acquire/error replay,
|
||||||
|
.538 successful connection (BST). The candidate sets PENDING while the asynchronous
|
||||||
|
acquire is in progress. It does not signal readiness early or suppress real errors.
|
||||||
|
|
||||||
|
Files in this directory:
|
||||||
|
|
||||||
|
- `0001-bluez5-mark-sco-acquire-pending.patch`
|
||||||
|
- `test_sco_acquire.py`: extracts the actual acquire functions and compiles them
|
||||||
|
with mocked I/O. Original source fails; patched source passes cold/retry
|
||||||
|
concurrent acquire, delayed readiness, synchronous success, and real failures.
|
||||||
|
|
||||||
|
The Bluetooth component builds. No stale-error replay was observed in the three
|
||||||
|
initial patched trials, but silent startup remained. This is a partial candidate,
|
||||||
|
not proof of an end-to-end fix. The relevant upstream master acquire function
|
||||||
|
inspected during the investigation matched 1.6.8; nothing was submitted upstream.
|
||||||
|
|
||||||
|
## Important integration-test confounds
|
||||||
|
|
||||||
|
The local Meson build used its default `/usr/local` prefix. A later log showed
|
||||||
|
that the test plugin could not load the installed Bluetooth hardware-quirks file:
|
||||||
|
it searched `/usr/local/share/spa-0.2/bluez5/bluez-hardware.conf`, while the file is
|
||||||
|
at `/usr/share/spa-0.2/bluez5/bluez-hardware.conf`. Correct build prefix/data paths
|
||||||
|
before attributing any patched-versus-stock behavior to the candidate patch.
|
||||||
|
|
||||||
|
A separate 50 ms WirePlumber-delay experiment previously removed roughly 450 ms
|
||||||
|
but did not eliminate failures. The latest attempted combined test did not run:
|
||||||
|
`WIREPLUMBER_DATA_DIR` pointed to a copied system data directory without the user's
|
||||||
|
required `camera-effects-hide-mics.lua`. WirePlumber hit its restart limit. The
|
||||||
|
override was removed and stock service restored/reset successfully. Do not count
|
||||||
|
this attempt as latency data. Preserve user scripts when building another overlay.
|
||||||
|
|
||||||
|
## Machine and relevant paths
|
||||||
|
|
||||||
|
- PipeWire 1.6.8, WirePlumber 0.5.17, BlueZ 5.87, kernel 7.2.3-arch1-3.
|
||||||
|
- Intel AX210 Bluetooth USB 8087:0032; device `/sys/bus/usb/devices/1-9`,
|
||||||
|
isochronous interface `1-9:1.1`, full-speed USB.
|
||||||
|
- Sony WH-1000XM5 address 88:C9:E8:A7:EC:7E.
|
||||||
|
- Observed profiles: A2DP AAC/SBC/SBC-XQ and HFP CVSD/mSBC. No LE Audio profile
|
||||||
|
observed. Idle playback is AAC stereo; mic use selects mSBC mono HFP.
|
||||||
|
- Those observations do not establish that a Linux-only update can provide the
|
||||||
|
headset mic plus unchanged stereo playback. That part of the requirement is
|
||||||
|
unresolved; do not promise it as a consequence of reducing switching delay.
|
||||||
|
- Camera Effects running binary: `/usr/local/lib/camera-effects/camera-effects-server`.
|
||||||
|
- Camera Effects sources: `/home/tank/.config/omarchy/plugins/alanfortlink.camera-effects/daemon/src/`.
|
||||||
|
- STT config: `/home/tank/.config/speech-to-text/config.json`.
|
||||||
|
- Camera Effects config: `/home/tank/.config/camera-effects/config.json`.
|
||||||
|
- WirePlumber user scripts include both `~/.config/wireplumber/scripts/` and
|
||||||
|
`~/.local/share/wireplumber/scripts/camera-effects-hide-mics.lua`.
|
||||||
|
|
||||||
|
## Temporary working materials (may disappear after reboot)
|
||||||
|
|
||||||
|
Root: `/tmp/stt-bluetooth-switch.uZDCcW/`
|
||||||
|
|
||||||
|
- `pipewire-1.6.8/`: official source plus candidate backend patch and temporary
|
||||||
|
`media-source.c` timing instrumentation. `build-test/` holds its Meson build.
|
||||||
|
- `backend-native.c`, `bluez5-dbus.c`, `media-source.c`: original source copies.
|
||||||
|
- `patched-spa/bluez5/libspa-bluez5.so`: experimental component; **not suitable
|
||||||
|
for permanent installation** given the caveat above.
|
||||||
|
- `build-tools.ini`, `build-tools/`: extracted matching glib2 development tool
|
||||||
|
used for gdbus-codegen, without installing system packages.
|
||||||
|
- `run-patched-test.sh`: temporary service override, three cancelled captures,
|
||||||
|
automatic restoration. Correct its environment setup before reuse.
|
||||||
|
- `autoswitch-bluetooth-profile.lua`: packaged script with 500 changed to 50 ms.
|
||||||
|
- `filter-hci.py`: btmon metadata-only filter, drops audio/keys/addresses. Fixed
|
||||||
|
SCO parser recognizes actual `> BR-ESCO:` RX and `< BR-ESCO:` TX headers.
|
||||||
|
Earlier zero RX/TX counts from the incorrect parser were invalid evidence.
|
||||||
|
- `btusb.c`: upstream v7.2 driver reference, not yet checked against all 7.2.3
|
||||||
|
changes; `bluez-packet.c`: BlueZ 5.87 monitor formatting reference.
|
||||||
|
- `/tmp/stt-startup-trace.py`: daemon start/cancel, graph and input-meter timing.
|
||||||
|
|
||||||
|
## Most useful remaining work
|
||||||
|
|
||||||
|
1. Correct the test build paths and repeat controlled stock/patched measurements.
|
||||||
|
2. Trace the first-start USB failure. `sudo -n true` succeeded at the end of this
|
||||||
|
investigation, so passwordless scoped tracing may now be available. Earlier
|
||||||
|
pending pkexec/btmon authentication was cancelled; it is not running.
|
||||||
|
3. Tracefs exists at `/sys/kernel/tracing`, accessible with sudo. No probes were
|
||||||
|
installed. No perf/bpftrace/bpftool is installed; kernel headers and BTF exist.
|
||||||
|
4. Determine which `usb_submit_urb` returns EMSGSIZE and the associated endpoint,
|
||||||
|
alternate setting, and packet lengths. USB alt6 permits 63 bytes; SCO frames
|
||||||
|
observed were 60 bytes. Kernel SCO_OPTIONS reports 96, which PipeWire already
|
||||||
|
knows is unreliable for USB and avoids by waiting for incoming packet size.
|
||||||
|
5. Investigate before changing that wait: native SCO waits for readable data on
|
||||||
|
USB before declaring transport ready; sco-io also waits for RX before TX.
|
||||||
|
No-RX startup could involve this interaction, the driver, or the headset.
|
||||||
|
6. Treat quality preservation as a separate capability question, not a side
|
||||||
|
effect of a latency patch. Do not restore webcam or warm-mic workarounds.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# F13 Bluetooth startup investigation
|
||||||
|
|
||||||
|
Status: **fixed and installed on 2026-09-09** — three faults (btusb
|
||||||
|
alternate-setting bug, PipeWire stale-error replay, PipeWire counting the
|
||||||
|
end-of-take SCO hangup as a failure), the 500 ms autoswitch delay, and the
|
||||||
|
daemon's false "Listening" indicator. Stereo playback with the headset
|
||||||
|
microphone is not possible with this headset. Full account in the resolution
|
||||||
|
section at the top of [HANDOFF.md](HANDOFF.md).
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Environment: PipeWire 1.6.8, WirePlumber 0.5.17, BlueZ 5.87,
|
||||||
|
Linux 7.2.3-arch1-3, Intel AX210 USB Bluetooth, Sony WH-1000XM5.
|
||||||
|
Measurements taken on 2026-09-09, with the dictation microphone initially idle.
|
||||||
|
|
||||||
|
The F13 extension uses its Python daemon and `pw-record`, targeting
|
||||||
|
`camera-effects-mic`. VoxType performs transcription, not microphone startup.
|
||||||
|
Tests invoked the same daemon start command, omitting keyboard dispatch, and
|
||||||
|
cancelled the take without pasting or adding completed recordings to history.
|
||||||
|
|
||||||
|
Observed on the stock system:
|
||||||
|
|
||||||
|
- Recording state starts in roughly 12–20 ms.
|
||||||
|
- The virtual microphone supplies buffers before the Bluetooth mic connects;
|
||||||
|
even an early nonzero level can be buffered data, not current microphone input.
|
||||||
|
- Successful HCI synchronous-connection completion was measured around
|
||||||
|
0.87–1.02 seconds after the start command. This does not establish when usable
|
||||||
|
speech first arrives.
|
||||||
|
- A failing trial accumulated 0.8 seconds of PCM, then stayed at that duration
|
||||||
|
through cancellation at six seconds. Both physical HFP nodes entered error.
|
||||||
|
- The UI also has a separate signal threshold / two-seconds-of-PCM fallback.
|
||||||
|
Its “Listening” indication is not proof of physical microphone readiness.
|
||||||
|
|
||||||
|
## Candidate PipeWire fix
|
||||||
|
|
||||||
|
An HFP transport can retain `SPA_BT_TRANSPORT_STATE_ERROR` from an earlier use.
|
||||||
|
`sco_acquire_cb()` starts an asynchronous connection but does not change that
|
||||||
|
state while waiting. A second node acquiring the same transport executes
|
||||||
|
`spa_bt_transport_acquire()`'s reference-count path, which re-emits the retained
|
||||||
|
error to both nodes. This can happen even though the new connection succeeds
|
||||||
|
shortly afterward.
|
||||||
|
|
||||||
|
The debug trace reproduced that sequence: acquire at 01:32:25.430,
|
||||||
|
second acquire and error re-emission at .447, successful completion at .538 BST.
|
||||||
|
|
||||||
|
`0001-bluez5-mark-sco-acquire-pending.patch` sets the state to `PENDING` during
|
||||||
|
the asynchronous acquire. It does not report readiness early, disable genuine
|
||||||
|
errors, change the Bluetooth codec, keep the microphone open, or shorten
|
||||||
|
WirePlumber's profile-switch delay. It applies to PipeWire 1.6.8; the corresponding
|
||||||
|
acquire function in the upstream master source inspected that day was unchanged.
|
||||||
|
|
||||||
|
## Validation and limits
|
||||||
|
|
||||||
|
`test_sco_acquire.py` compiles the actual two acquire functions with mocked I/O.
|
||||||
|
It fails against the unmodified source and passes with the patch. It covers
|
||||||
|
concurrent cold/retry acquisition, delayed readiness, synchronous success, and
|
||||||
|
real connection failures. It is a focused reproduction harness, not the full
|
||||||
|
PipeWire integration suite.
|
||||||
|
|
||||||
|
Run it against a PipeWire source checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 diagnostics/bluetooth/test_sco_acquire.py \
|
||||||
|
/path/to/pipewire/spa/plugins/bluez5/backend-native.c \
|
||||||
|
/path/to/pipewire/spa/plugins/bluez5/bluez5-dbus.c
|
||||||
|
```
|
||||||
|
|
||||||
|
The patched Bluetooth component compiled and was loaded in WirePlumber using
|
||||||
|
a temporary, process-specific SPA plugin path. Across three cancelled startup
|
||||||
|
trials, the HFP error-state replay was not observed, but the first trial still
|
||||||
|
produced only silence. The kernel logged `urb ... submission failed (90)` during
|
||||||
|
that trial. The other trials produced signal but indicated readiness at about
|
||||||
|
2.2 seconds. Similar USB errors had also occurred in earlier unpatched tests;
|
||||||
|
their cause and relation to ordinary cold-start delay remain unproven.
|
||||||
|
|
||||||
|
The temporary service override was removed and the stock service restored.
|
||||||
|
No system packages, permanent microphone settings, or speech-to-text behavior
|
||||||
|
were changed. Nothing has been submitted upstream.
|
||||||
|
|
||||||
|
Further work must distinguish the USB/SCO first-start failure from the
|
||||||
|
error-state replay and measure current microphone input, not virtual-buffer
|
||||||
|
arrival or the UI indicator. This patch alone does not meet the requested
|
||||||
|
instant-start / full-quality experience.
|
||||||
|
|
||||||
|
## Later measurements and test-environment caveat
|
||||||
|
|
||||||
|
Temporary instrumentation in `media-source.c` measured two successful starts
|
||||||
|
with the candidate patch and the stock WirePlumber 500 ms switch delay:
|
||||||
|
|
||||||
|
- First encoded SCO data: 0.97–0.99 s after the start request.
|
||||||
|
- First decoded PCM: 1.16–1.19 s.
|
||||||
|
- First nonzero decoded PCM: 1.48–1.50 s.
|
||||||
|
- First nonzero PCM delivered to the graph: 1.53–1.55 s.
|
||||||
|
- Camera Effects input meter first nonzero: about 1.62 s.
|
||||||
|
- UI listening indication: about 2.14 s.
|
||||||
|
|
||||||
|
Nonzero samples establish signal, not intelligibility or preservation of opening
|
||||||
|
words. The first trial after restarting WirePlumber had no encoded data during
|
||||||
|
the six-second capture and logged a USB submission error at about 4.46 s.
|
||||||
|
|
||||||
|
A later combined test with a 50 ms WirePlumber delay did **not run**: the temporary
|
||||||
|
data-directory override omitted the user's required `camera-effects-hide-mics.lua`
|
||||||
|
script, so WirePlumber failed to start. The override was removed, the service's
|
||||||
|
restart limit reset, and the stock service successfully started again.
|
||||||
|
|
||||||
|
That startup log also exposed a build-environment confound: the locally built
|
||||||
|
PipeWire plugin searched `/usr/local/share/spa-0.2/bluez5/bluez-hardware.conf`,
|
||||||
|
whereas the installed quirks file is under `/usr/share/spa-0.2/bluez5/`.
|
||||||
|
Correct the build prefix/data paths before relying on patched-versus-stock
|
||||||
|
integration comparisons. The focused acquire-function regression test is
|
||||||
|
independent of this configuration problem.
|
||||||
|
|
||||||
|
See [HANDOFF.md](HANDOFF.md) for the complete investigation handoff.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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.7–1.0 s, sometimes never | 0.26–0.49 s |
|
||||||
|
| first real audio at the daemon | 1.5–1.7 s, or never | 1.06–1.43 s |
|
||||||
|
| "Listening" in the bar | 2.1 s on a timer, even with no audio | 1.19–1.43 s, only on real audio |
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| 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 row is part of this repo. The rest are machine-level and are
|
||||||
|
described, with patches and measurements, in [HANDOFF.md](HANDOFF.md).
|
||||||
|
|
||||||
|
## Why ~0.9 s is the floor
|
||||||
|
|
||||||
|
```
|
||||||
|
key press
|
||||||
|
0.00 s ─┬─ daemon starts pw-record on camera-effects-mic
|
||||||
|
│
|
||||||
|
│ Camera Effects notices the client (250 ms poll),
|
||||||
|
│ opens its helper stream, WirePlumber waits 50 ms,
|
||||||
|
│ then asks BlueZ for the headset profile
|
||||||
|
│
|
||||||
|
0.30 s ─┼─ eSCO link up (the headset must leave sniff mode,
|
||||||
|
│ then the radio sets up the synchronous link: ~0.1 s)
|
||||||
|
│
|
||||||
|
│ headset sends encoded digital silence while its own
|
||||||
|
│ mic path starts and mSBC syncs: ~0.5–0.6 s
|
||||||
|
│ (measured at the decoder; Linux cannot shorten it)
|
||||||
|
│
|
||||||
|
0.85 s ─┼─ first non-zero samples leave the headset
|
||||||
|
│ Camera Effects processing + 50 ms chunking
|
||||||
|
0.90 s ─┴─ first real audio reaches the daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
What is left on the Linux side is the ~0.25 s before the profile switch is
|
||||||
|
even requested. The daemon could ask for the headset profile itself when
|
||||||
|
capture starts, but the WirePlumber autoswitch script only restores a profile
|
||||||
|
it switched, so the daemon would have to restore A2DP too. Not done.
|
||||||
|
|
||||||
|
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`).
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compile the actual acquire functions with mocked I/O; no Bluetooth access.
|
||||||
|
|
||||||
|
This is a focused reproduction harness, not PipeWire's full integration suite.
|
||||||
|
Usage: test-sco-acquire.py <backend-native.c> <bluez5-dbus.c>
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def function(path, signature):
|
||||||
|
source = pathlib.Path(path).read_text()
|
||||||
|
start = source.index(signature + "\n{")
|
||||||
|
end = source.index("\n}", start) + 2
|
||||||
|
return source[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
stub = r'''
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
enum { SPA_BT_TRANSPORT_STATE_ERROR = -1, SPA_BT_TRANSPORT_STATE_IDLE,
|
||||||
|
SPA_BT_TRANSPORT_STATE_PENDING, SPA_BT_TRANSPORT_STATE_ACTIVE };
|
||||||
|
struct impl { void *log; } backend;
|
||||||
|
struct spa_bt_monitor { void *log; } monitor;
|
||||||
|
struct transport_data { int err; bool requesting; } td;
|
||||||
|
struct spa_bt_transport {
|
||||||
|
void *backend, *user_data;
|
||||||
|
struct spa_bt_monitor *monitor;
|
||||||
|
int fd, state, acquire_refcount, error_count;
|
||||||
|
bool acquired;
|
||||||
|
uint64_t last_error_time;
|
||||||
|
};
|
||||||
|
#define SPA_CONTAINER_OF(p, type, member) ((type *)(p))
|
||||||
|
#define spa_log_debug(...) ((void)0)
|
||||||
|
#define spa_assert assert
|
||||||
|
#define TRANSPORT_ERROR_TIMEOUT 6000000000ULL
|
||||||
|
#define TRANSPORT_ERROR_MAX_RETRY 3
|
||||||
|
#define spa_bt_transport_impl(t, op, version, optional) sco_acquire_cb(t, optional)
|
||||||
|
static int error_events, connect_result, connect_error;
|
||||||
|
static uint64_t get_time_now(struct spa_bt_monitor *m) { return 10000000000ULL; }
|
||||||
|
static void spa_bt_transport_emit_state_changed(struct spa_bt_transport *t, int old, int state) {
|
||||||
|
if (state == SPA_BT_TRANSPORT_STATE_ERROR) ++error_events;
|
||||||
|
}
|
||||||
|
static void spa_bt_transport_set_state(struct spa_bt_transport *t, int state) {
|
||||||
|
int old = t->state;
|
||||||
|
if (old != state) {
|
||||||
|
t->state = state;
|
||||||
|
spa_bt_transport_emit_state_changed(t, old, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static int sco_do_connect(struct spa_bt_transport *t) {
|
||||||
|
td.err = connect_error;
|
||||||
|
return connect_result;
|
||||||
|
}
|
||||||
|
static void sco_start_source(struct spa_bt_transport *t) {}
|
||||||
|
static void sco_ready(struct spa_bt_transport *t) {
|
||||||
|
td.requesting = false;
|
||||||
|
spa_bt_transport_set_state(t, td.err ? SPA_BT_TRANSPORT_STATE_ERROR : SPA_BT_TRANSPORT_STATE_ACTIVE);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
tests = r'''
|
||||||
|
static struct spa_bt_transport fresh(int state) {
|
||||||
|
td = (struct transport_data){0};
|
||||||
|
error_events = 0;
|
||||||
|
connect_result = 42;
|
||||||
|
connect_error = -EINPROGRESS;
|
||||||
|
return (struct spa_bt_transport){ .backend=&backend, .user_data=&td,
|
||||||
|
.monitor=&monitor, .fd=-1, .state=state };
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
for (int initial = SPA_BT_TRANSPORT_STATE_ERROR; initial <= SPA_BT_TRANSPORT_STATE_IDLE; ++initial) {
|
||||||
|
struct spa_bt_transport t = fresh(initial);
|
||||||
|
assert(spa_bt_transport_acquire(&t, false) == 0);
|
||||||
|
if (t.state != SPA_BT_TRANSPORT_STATE_PENDING) {
|
||||||
|
fprintf(stderr, "FAIL: asynchronous acquire retained state %d (initial %d)\n", t.state, initial);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
assert(td.requesting && t.acquire_refcount == 1 && t.acquired);
|
||||||
|
assert(spa_bt_transport_acquire(&t, false) == 0);
|
||||||
|
assert(t.acquire_refcount == 2 && !error_events);
|
||||||
|
assert(t.state != SPA_BT_TRANSPORT_STATE_ACTIVE);
|
||||||
|
td.err = 0;
|
||||||
|
sco_ready(&t);
|
||||||
|
assert(t.state == SPA_BT_TRANSPORT_STATE_ACTIVE);
|
||||||
|
}
|
||||||
|
struct spa_bt_transport t = fresh(SPA_BT_TRANSPORT_STATE_IDLE);
|
||||||
|
connect_result = -1;
|
||||||
|
assert(spa_bt_transport_acquire(&t, false) < 0);
|
||||||
|
assert(t.state == SPA_BT_TRANSPORT_STATE_ERROR && !t.acquired && error_events == 1);
|
||||||
|
t = fresh(SPA_BT_TRANSPORT_STATE_IDLE);
|
||||||
|
t.fd = 42;
|
||||||
|
assert(spa_bt_transport_acquire(&t, true) == 0);
|
||||||
|
assert(t.state == SPA_BT_TRANSPORT_STATE_ACTIVE && !error_events);
|
||||||
|
t = fresh(SPA_BT_TRANSPORT_STATE_ERROR);
|
||||||
|
assert(spa_bt_transport_acquire(&t, false) == 0);
|
||||||
|
td.err = -ECONNREFUSED;
|
||||||
|
sco_ready(&t);
|
||||||
|
assert(t.state == SPA_BT_TRANSPORT_STATE_ERROR && error_events == 1);
|
||||||
|
puts("PASS: cold/retry concurrent acquire, delayed readiness, synchronous success, real failures");
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
source = stub + function(sys.argv[1], 'static int sco_acquire_cb(void *data, bool optional)')
|
||||||
|
source += function(sys.argv[2], 'int spa_bt_transport_acquire(struct spa_bt_transport *transport, bool optional)')
|
||||||
|
source += tests
|
||||||
|
with tempfile.TemporaryDirectory(prefix='stt-sco-unit-') as tmp:
|
||||||
|
binary = str(pathlib.Path(tmp) / 'test')
|
||||||
|
subprocess.run(['cc', '-std=c11', '-Wall', '-Wno-unused-variable', '-Wno-unused-parameter',
|
||||||
|
'-x', 'c', '-', '-o', binary], input=source, text=True, check=True)
|
||||||
|
sys.exit(subprocess.run([binary]).returncode)
|
||||||
File diff suppressed because it is too large
Load Diff
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install (or with "off": remove) the tracefs kprobes and dynamic-debug prints used to trace
|
||||||
|
# Bluetooth SCO setup through the kernel and btusb. Needs passwordless sudo.
|
||||||
|
S=/sys/kernel/tracing
|
||||||
|
if [ "${1:-}" = off ]; then
|
||||||
|
sudo -n sh -c "echo 0 > $S/events/bt/enable; echo > $S/kprobe_events; echo > $S/trace
|
||||||
|
echo 'module btusb -p' > /sys/kernel/debug/dynamic_debug/control
|
||||||
|
echo 'func hci_sync_conn_complete_evt -p' > /sys/kernel/debug/dynamic_debug/control"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
sudo -n sh -c "
|
||||||
|
echo 0 > $S/events/bt/enable 2>/dev/null
|
||||||
|
echo > $S/kprobe_events
|
||||||
|
echo 'p:bt/notify btusb_notify evt=\$arg2:u32' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/work btusb_work' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/switch_alt btusb_switch_alt_setting new_alts=\$arg2:s32' >> $S/kprobe_events
|
||||||
|
echo 'r:bt/switch_alt_ret btusb_switch_alt_setting ret=\$retval:s32' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/set_intf usb_set_interface ifnum=\$arg2:s32 alt=\$arg3:s32' >> $S/kprobe_events
|
||||||
|
echo 'r:bt/set_intf_ret usb_set_interface ret=\$retval:s32' >> $S/kprobe_events
|
||||||
|
echo 'r:bt/submit_isoc_ret btusb_submit_isoc_urb ret=\$retval:s32' >> $S/kprobe_events
|
||||||
|
echo 'r:bt/autopm_ret usb_autopm_get_interface ret=\$retval:s32' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/conn_add hci_conn_add_unset type=\$arg2:s32' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/conn_del hci_conn_del' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/conn_failed hci_conn_failed status=\$arg2:u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/conn_complete hci_conn_complete_evt status=+0(\$arg2):u8 link_type=+9(\$arg2):u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/disconn_complete hci_disconn_complete_evt status=+0(\$arg2):u8 handle=+1(\$arg2):u16 reason=+3(\$arg2):u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/mode_change hci_mode_change_evt status=+0(\$arg2):u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/sco_setup hci_sco_setup status=\$arg2:u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/cs_enh_setup_sync hci_cs_enhanced_setup_sync_conn status=\$arg2:u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/sync_complete hci_sync_conn_complete_evt' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/connect_cfm sco_connect_cfm status=\$arg2:u8' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/sco_tx hci_send_sco' >> $S/kprobe_events
|
||||||
|
echo 'p:bt/sco_rx_urb btusb_isoc_complete' >> $S/kprobe_events
|
||||||
|
echo 1 > $S/events/bt/enable
|
||||||
|
echo 'module btusb +pf' > /sys/kernel/debug/dynamic_debug/control
|
||||||
|
echo 'func hci_sync_conn_complete_evt +pf' > /sys/kernel/debug/dynamic_debug/control
|
||||||
|
echo > $S/trace
|
||||||
|
echo 1 > $S/tracing_on"
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Restart WirePlumber, run a traced trial, stop when the kernel logs submission failed (90).
|
||||||
|
SC=$(dirname "$0")
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
systemctl --user restart wireplumber; sleep 9
|
||||||
|
/home/tank/repos/speech-to-text/bin/stt status --json | grep -q '"state": *"idle"' || { echo "stt busy"; break; }
|
||||||
|
T0=$(date +%s)
|
||||||
|
"$SC/trial.sh" "repro-$i" > "$SC/repro-$i.log" 2>&1
|
||||||
|
if sudo -n journalctl -k --since "@$T0" --no-pager | grep -q 'submission failed (90)'; then echo "REPRODUCED on attempt $i"; exit 0; fi
|
||||||
|
echo "attempt $i: no failure"; sleep 3
|
||||||
|
done
|
||||||
|
echo "not reproduced"
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# One dictation startup trial: kernel probes + btmon + userspace timing. $1 = label
|
||||||
|
set -u
|
||||||
|
SC=$(dirname "$0"); L=${1:-trial}; S=/sys/kernel/tracing
|
||||||
|
sudo -n sh -c "echo > $S/trace"
|
||||||
|
sudo -n btmon -w "$SC/$L.btsnoop" >/dev/null 2>&1 &
|
||||||
|
BTMON=$!
|
||||||
|
sleep 0.5
|
||||||
|
T0=$(date +%s.%N)
|
||||||
|
echo "T0 unix $T0 monotonic $(python3 -c 'import time;print(round(time.monotonic(),3))')"
|
||||||
|
python3 /tmp/stt-startup-trace.py 2>&1 | grep -vE '"graph"|audio-progress'
|
||||||
|
sleep 0.5
|
||||||
|
sudo -n kill $BTMON; wait $BTMON 2>/dev/null
|
||||||
|
echo "--- ftrace"
|
||||||
|
sudo -n cat $S/trace | grep -v '^#' | grep -vE 'sco_tx|sco_rx_urb' | sed -E 's/^ +//; s/ \[[0-9]+\] [^ ]+ / /; s/\([^)]*\)//' | cut -c1-120
|
||||||
|
echo "--- sco tx/rx-urb per second"
|
||||||
|
sudo -n cat $S/trace | grep -E 'sco_tx|sco_rx_urb' | sed -E 's/.* ([0-9]+)\.[0-9]+: (sco_tx|sco_rx_urb).*/\1 \2/' | sort | uniq -c | tr '\n' ';'; echo
|
||||||
|
echo "--- dmesg"
|
||||||
|
sudo -n journalctl -k -o short-precise --since "@${T0%.*}" --no-pager | grep -E 'Bluetooth:|hci_sync_conn|hci_conn_request|btusb_notify|__set_isoc|btusb_submit_isoc|__fill_isoc' | cut -c1-160
|
||||||
|
echo "--- btmon (commands/events only)"
|
||||||
|
sudo -n chown $USER "$SC/$L.btsnoop"
|
||||||
|
btmon -r "$SC/$L.btsnoop" -t 2>/dev/null | grep -E '^[<>] HCI (Command|Event)|^\s+(Status|Handle|Link type|Air mode|Reason|Opcode|Voice setting|Transmit coding|Receive coding|RX packet length|TX packet length|Mode|Interval|Packet type|Retransmission|Max latency|Transmit bandwidth|Receive bandwidth):' | grep -vE 'HCI Event: Number of Completed|HCI Event: Command Complete.*(Read RSSI|Read Clock)|Vendor' | sed -E 's/\{[^}]*\}//' | cut -c1-140
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""One F13-path dictation startup trial, timed against the kernel.
|
||||||
|
|
||||||
|
usage: trial2.py LABEL [--wp-restart SECS] (--wp-restart: restart WirePlumber, wait SECS, then start)
|
||||||
|
Needs tools/kernel-probes.sh installed and `echo mono > /sys/kernel/tracing/trace_clock` (passwordless sudo).
|
||||||
|
Starts dictation the way F13 does (`stt start`), polls the daemon for 6 s, cancels the take (nothing is
|
||||||
|
pasted or kept), then prints a timeline relative to the start command.
|
||||||
|
"""
|
||||||
|
import json, os, re, socket, subprocess, sys, time
|
||||||
|
|
||||||
|
STT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "bin", "stt")
|
||||||
|
SOCK = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "speech-to-text", "ctl.sock")
|
||||||
|
TR = "/sys/kernel/tracing"
|
||||||
|
|
||||||
|
def sudo(cmd):
|
||||||
|
return subprocess.run(["sudo", "-n", "sh", "-c", cmd], capture_output=True, text=True).stdout
|
||||||
|
|
||||||
|
def profile():
|
||||||
|
out = subprocess.run(["pactl", "list", "cards"], capture_output=True, text=True).stdout
|
||||||
|
card = out.split("Name: bluez_card.88_C9_E8_A7_EC_7E", 1)
|
||||||
|
if len(card) < 2: return "no-card"
|
||||||
|
m = re.search(r"Active Profile: (\S+)", card[1].split("Card #")[0])
|
||||||
|
return m.group(1) if m else "?"
|
||||||
|
|
||||||
|
def status():
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(SOCK); s.settimeout(2)
|
||||||
|
buf = b""
|
||||||
|
while b"\n" not in buf:
|
||||||
|
buf += s.recv(65536)
|
||||||
|
s.close()
|
||||||
|
return json.loads(buf.split(b"\n", 1)[0])
|
||||||
|
|
||||||
|
label = sys.argv[1]
|
||||||
|
if "--wp-restart" in sys.argv:
|
||||||
|
wait = float(sys.argv[sys.argv.index("--wp-restart") + 1])
|
||||||
|
subprocess.run(["systemctl", "--user", "restart", "wireplumber"], check=True)
|
||||||
|
time.sleep(wait)
|
||||||
|
assert status()["state"] == "idle", "stt busy"
|
||||||
|
p0 = profile()
|
||||||
|
assert p0.startswith("a2dp"), f"headset not in A2DP before start: {p0}"
|
||||||
|
sudo(f"echo > {TR}/trace")
|
||||||
|
t0m, t0w = time.monotonic(), time.time()
|
||||||
|
subprocess.Popen([STT, "start"], stdout=subprocess.DEVNULL)
|
||||||
|
first_nz = first_sig = listening = None
|
||||||
|
levels = []; window = None
|
||||||
|
while time.monotonic() - t0m < 6.0:
|
||||||
|
st = status(); t = time.monotonic() - t0m
|
||||||
|
if st["state"] == "recording" and st["levels"]:
|
||||||
|
lv = st["levels"][-1]
|
||||||
|
levels.append((round(t, 2), lv))
|
||||||
|
if lv > 0 and first_nz is None: first_nz = t
|
||||||
|
if lv >= 0.05 and first_sig is None: first_sig = t # rms >= 2e-4
|
||||||
|
if st["listening"] and listening is None: listening = t
|
||||||
|
if window is None and t >= 2.3: window = (round(t, 2), st["levels"])
|
||||||
|
elif st["state"] != "recording" and st["state"] != "idle" and st["state"] != "opening":
|
||||||
|
pass
|
||||||
|
time.sleep(0.02)
|
||||||
|
subprocess.run([STT, "cancel"], stdout=subprocess.DEVNULL)
|
||||||
|
time.sleep(0.3)
|
||||||
|
p_rec = profile()
|
||||||
|
|
||||||
|
# kernel timeline (trace clock must be "mono")
|
||||||
|
ev = {}
|
||||||
|
trace = sudo(f"cat {TR}/trace")
|
||||||
|
rx = tx = 0
|
||||||
|
for line in trace.splitlines():
|
||||||
|
m = re.match(r"\s*\S+\s+\[\d+\]\s+\S+\s+([\d.]+):\s+(\S+):\s*(.*)", line)
|
||||||
|
if not m: continue
|
||||||
|
t = float(m.group(1)) - t0m; name = m.group(2); rest = m.group(3)
|
||||||
|
if name == "sco_rx_urb": rx += 1; ev.setdefault("first_sco_rx_urb", t); continue
|
||||||
|
if name == "sco_tx": tx += 1; ev.setdefault("first_sco_tx", t); continue
|
||||||
|
if name == "conn_add" and "type=2" in rest: ev.setdefault("esco_connect", t)
|
||||||
|
elif name == "cs_enh_setup_sync": ev.setdefault("enh_setup_cmd_status", t)
|
||||||
|
elif name == "sync_complete": ev.setdefault("esco_up", t)
|
||||||
|
elif name == "submit_isoc_ret" and "ret=-" in rest: ev.setdefault("isoc_submit_fail", t)
|
||||||
|
elif name == "notify": ev.setdefault("notify", []).append((round(t, 3), rest))
|
||||||
|
elif name == "switch_alt": ev.setdefault("switch_alt", []).append((round(t, 3), rest))
|
||||||
|
elif name == "conn_complete": ev.setdefault("conn_complete", []).append((round(t, 3), rest))
|
||||||
|
dmesg = sudo(f"journalctl -k --since @{int(t0w)-1} --no-pager -o short-precise | grep -E 'submission failed|Bluetooth' | cut -c1-140")
|
||||||
|
|
||||||
|
print(f"== {label}")
|
||||||
|
for k in ("esco_connect", "enh_setup_cmd_status", "esco_up", "first_sco_rx_urb", "first_sco_tx", "isoc_submit_fail"):
|
||||||
|
print(f"{k:22s} {ev[k]:.3f}" if k in ev else f"{k:22s} -")
|
||||||
|
print(f"sco rx urbs {rx} sco tx {tx}")
|
||||||
|
print(f"{'first nonzero level':22s} {first_nz:.3f}" if first_nz else f"{'first nonzero level':22s} -")
|
||||||
|
print(f"{'first level>=0.05':22s} {first_sig:.3f}" if first_sig else f"{'first level>=0.05':22s} -")
|
||||||
|
print(f"{'UI listening':22s} {listening:.3f}" if listening else f"{'UI listening':22s} -")
|
||||||
|
print("notify:", ev.get("notify")); print("switch_alt:", ev.get("switch_alt")); print("conn_complete:", ev.get("conn_complete"))
|
||||||
|
print("levels:", [l for l in levels if l[1] > 0][:12])
|
||||||
|
if window: print(f"level window at {window[0]} s (oldest first, 50 ms each):", window[1])
|
||||||
|
print("dmesg:", dmesg.strip() or "-")
|
||||||
|
print("ERROR90" if "submission failed (90)" in dmesg else "no error 90")
|
||||||
|
time.sleep(3.5)
|
||||||
|
print(f"profile: before {p0}, during {p_rec}, 3.5 s after cancel {profile()}")
|
||||||
Reference in New Issue
Block a user