From e1c81bea9adeabe7beddd6e9d9ce1983a8b8068c Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Thu, 30 Jul 2026 15:48:59 +0200 Subject: [PATCH] =?UTF-8?q?fix(iphone-bt):=20iPhone-Route-Watcher=20greift?= =?UTF-8?q?=20wieder=20=E2=80=94=20Gegenseite=20fehlte=20in=20Aufnahme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aime-iphone-route.sh v1 hat seit dem Reboot am 29.07. nie gegriffen (0 Zeilen in journalctl -t aime-iphone-route über den ganzen Boot). Zwei Gründe: 1. Die Portnamen waren hartcodiert auf input_FL/input_FR. HFP/mSBC ist mono, die Ports heißen playback_MONO/capture_MONO — der grep matchte nie. 2. Der Guard "micgate.out schon verlinkt -> überspringen" war einmal pro Anruf. WirePlumber legt die HFP-Nodes nicht atomar an, der Uplink erscheint vor der Gegenseite. War bluez_input in genau diesem Durchlauf noch nicht da, wurde die Gegenseite für den ganzen Anruf nicht mehr nachverlinkt. Ein erst mitten im Anruf gesetztes callrec_mix->Uplink wurde ebenso nie entfernt. Folge: Gegenseite -> callmix fehlte (nicht in der Aufnahme, #80/#81 hatten je nur 1 Sprecher) und der Uplink führte den ungegateten Mix statt micgate.out, also wieder das Echo von vor dem Fix am 13.07. v2 ermittelt die Portnamen über pw-link -i/-o und prüft alle drei Links in jedem Durchlauf einzeln nach. link_all legt genau einen Link pro Ziel-Port an: eine Mono-Quelle speist beide Kanäle, eine Stereo-Quelle auf einem Mono-HFP-Port sendet nur FL, weil beide Links in denselben Port summieren würden (+6 dB Uplink). Gegenprobe am echten Anruf steht noch aus. --- app/aime-iphone-route.sh | 88 +++++++++++++++++++++++++++++++++------- docs/06-iphone-bt.md | 23 ++++++++--- docs/08-changelog.md | 49 ++++++++++++++++++++++ 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/app/aime-iphone-route.sh b/app/aime-iphone-route.sh index cf9b5d6..e391e93 100644 --- a/app/aime-iphone-route.sh +++ b/app/aime-iphone-route.sh @@ -1,24 +1,82 @@ #!/bin/bash -# Auto-route iPhone HFP calls (the nodes exist only during a call): +# Auto-route iPhone HFP calls (the bluez nodes exist only during a live call): # uplink (what far-end hears) <- micgate.out (GATED mic only -> no self-echo) # far-end voice (bluez_input) -> callmix (recorded digitally, clean) -# and remove WirePlumber's default callrec_mix->uplink auto-link (that mix carried -# the ungated mic which picks up the far-end acoustically = echo). +# and remove WirePlumber's default callrec_mix->uplink auto-link (that mix carries +# the ungated mic AND the far-end, i.e. a digital echo loop = far-end hears itself). +# +# v2 (2026-07-30): two fixes after a call recorded near-end only + far-end doubling +# 1. every link is verified on EVERY pass, not once per call. The old one-shot +# guard ("micgate.out already linked -> skip") meant: if the far-end node +# appeared a tick later than the uplink node (WirePlumber does not create the +# HFP nodes atomically), far-end->callmix was never made for the whole call, +# and a callrec_mix->uplink link added mid-call was never removed. +# 2. port names are discovered instead of hardcoded. HFP/mSBC is MONO, so the +# ports are playback_MONO/capture_MONO, not input_FL/input_FR. The old +# hardcoded FL/FR grep never matched a mono node at all. export XDG_RUNTIME_DIR=/run/user/1000 MAC=B4_96_A5_42_BD_D6 -PL(){ pw-link "$@" 2>/dev/null; } + +# ports_in/ports_out: list "node:port" for nodes whose name matches $1 +ports_in() { pw-link -i 2>/dev/null | grep -E "^$1"; } +ports_out() { pw-link -o 2>/dev/null | grep -E "^$1"; } + +# link_all SRC_PORTS DST_PORTS -- exactly one link per DESTINATION port, cycling +# the sources. So a mono source feeds both destination channels, and a stereo +# source feeding a mono HFP port sends FL only (linking both would sum them into +# the same port = +6 dB on the uplink). +link_all() { + local -n _src=$1 _dst=$2 + local mode=$3 i n=${#_src[@]} m=${#_dst[@]} + [ "$n" -eq 0 ] || [ "$m" -eq 0 ] && return + for ((i=0; i/dev/null + done +} + +log_once="" while true; do - # uplink sink node (has input_FL) present = a call is up - UPPORT=$(pw-link -i 2>/dev/null | grep -m1 "bluez_output\.${MAC}.*:input_FL") - if [ -n "$UPPORT" ]; then - B=${UPPORT%%:*} - I=$(pw-link -o 2>/dev/null | grep -m1 "bluez_input\.${MAC}.*:output_FL"); I=${I%%:*} - if ! pw-link -l "$B" 2>/dev/null | grep -q micgate.out; then - PL -d callrec_mix:capture_FL "$B:input_FL"; PL -d callrec_mix:capture_FR "$B:input_FR" - PL micgate.out:output_FL "$B:input_FL"; PL micgate.out:output_FR "$B:input_FR" - [ -n "$I" ] && { PL "$I:output_FL" callmix:playback_FL; PL "$I:output_FR" callmix:playback_FR; } - logger -t aime-iphone-route "applied iPhone call routing ($B)" + mapfile -t UP < <(ports_in "bluez_output\.${MAC}") + if [ ${#UP[@]} -gt 0 ]; then + B=${UP[0]%%:*} + LINKS=$(pw-link -l "$B" 2>/dev/null) + CHANGED="" + + # 1) drop WirePlumber's ungated-mix -> uplink link (echo source). Checked every + # pass because WP can (re-)add it at any point during the call. + if echo "$LINKS" | grep -q 'callrec_mix'; then + mapfile -t MIX < <(ports_out 'callrec_mix') + link_all MIX UP -d + CHANGED="$CHANGED -callrec_mix" fi + + # 2) ensure gated mic -> uplink + if ! echo "$LINKS" | grep -q 'micgate\.out'; then + mapfile -t GATE < <(ports_out 'micgate\.out') + link_all GATE UP "" + CHANGED="$CHANGED +micgate" + fi + + # 3) ensure far-end -> record mix. Retried every pass: this node often shows up + # later than the uplink node. + mapfile -t FAR < <(ports_out "bluez_input\.${MAC}") + if [ ${#FAR[@]} -gt 0 ]; then + I=${FAR[0]%%:*} + if ! pw-link -l "$I" 2>/dev/null | grep -q 'callmix'; then + mapfile -t REC < <(ports_in 'callmix') + link_all FAR REC "" + CHANGED="$CHANGED +far-end" + fi + else + CHANGED="$CHANGED (no-far-end-node-yet)" + fi + + if [ -n "$CHANGED" ] && [ "$CHANGED" != "$log_once" ]; then + logger -t aime-iphone-route "iPhone call $B:$CHANGED" + log_once="$CHANGED" + fi + else + log_once="" fi - sleep 2 + sleep 1 done diff --git a/docs/06-iphone-bt.md b/docs/06-iphone-bt.md index 828075f..eb591e3 100644 --- a/docs/06-iphone-bt.md +++ b/docs/06-iphone-bt.md @@ -40,20 +40,28 @@ flowchart LR **Echo-Fix:** WirePlumber verlinkte den Uplink zunächst mit `callrec_mix` (ungegatetes Mikro + Gegenseite selbst!) → Gegenseite hörte sich selbst. Korrekt: Uplink ← **micgate.out**. Vom Nutzer bestätigt echofrei. +Gegenseite → **Headset-Hörer** macht WirePlumber selbst (Default-Sink); der Watcher baut diesen Link +**nicht**. `callmix` ist eine reine Aufnahme-Senke ohne Ausgang — Gegenseite → `callmix` macht die Gegenseite +also aufnehmbar, nicht hörbar. + ## Automatik ```mermaid sequenceDiagram participant IP as iPhone participant BZ as BlueZ/WirePlumber - participant W as aime-iphone-route.sh (Watcher) + participant W as aime-iphone-route.sh (Watcher, 1 s Takt) participant PW as PipeWire IP->>BZ: Anruf beginnt (eSCO aufgebaut) - BZ->>PW: HFP-Nodes erscheinen (bluez_input/.0, bluez_output/.1) - W->>PW: erkennt neue Nodes - W->>PW: Gegenseite → callmix + Headset verlinken - W->>PW: Uplink ← micgate.out verlinken (Echo-Fix) + BZ->>PW: HFP-Nodes erscheinen — NICHT atomar, Uplink oft vor Gegenseite + BZ->>PW: Gegenseite → Headset-Hörer (WirePlumber-Default) + loop jede Sekunde, solange Uplink-Node existiert + W->>PW: Portnamen ermitteln (HFP = mono, playback_MONO/capture_MONO) + W->>PW: callrec_mix → Uplink trennen, falls vorhanden (Echo-Quelle) + W->>PW: Uplink ← micgate.out verlinken, falls fehlt + W->>PW: Gegenseite → callmix verlinken, falls fehlt (Node kommt später!) + end IP->>BZ: Anruf endet BZ->>PW: HFP-Nodes verschwinden (profile=off — normal) ``` @@ -61,6 +69,11 @@ sequenceDiagram - Watcher persistent: `/opt/callrec/aime-iphone-route.sh` + `aime-iphone-route.service`; überlebt Reboots (Config/Service/Bond+Trusted verifiziert). - Der Watcher-Neustart lässt callmix/Loopbacks intakt. +- **Jeder Link wird in JEDEM Durchlauf geprüft, nicht einmal pro Anruf** — siehe Changelog 2026-07-30. +- **Portnamen werden ermittelt, nicht hartcodiert.** HFP/mSBC ist mono, die Ports heißen + `playback_MONO`/`capture_MONO`, nicht `input_FL`/`input_FR`. +- Kontrolle nach einem Anruf: `journalctl -t aime-iphone-route`. **Keine Zeile = der Watcher hat nichts + getan** und die Aufnahme enthält nur die Nahseite. ## Offen diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 8dbafdb..ee749c9 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,55 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 2026-07-30 — iPhone-Anruf: Gegenseite fehlt in der Aufnahme + Gegenseite hört sich doppelt + +**Ausgangslage (Nutzer):** Ein BT-Telefonat wurde aufgenommen, in der Aufnahme ist nur die Nahseite zu +hören; die Gegenseite berichtete zusätzlich, sich selbst stark doppelt zu hören. Der Pfad hatte am +2026-07-13 nachweislich funktioniert. + +**Messung:** Aufnahme #80 (13:16, 44 s) → Diarisierung `1 Sprecher` (bekannte Stimme „sascha", cos 0,779); +#81 (13:38) ebenso 1 Sprecher. `journalctl -t aime-iphone-route` über den **gesamten** Boot seit +2026-07-29 12:15 → `-- No entries --`. Watcher-Prozess lief (PID 1446, 27 h), Service `active`, WirePlumber- +Drop-in intakt (`bluez5.roles`, `hfphsp-backend=native`), iPhone `Paired/Bonded/Trusted/Connected = yes`, +Profil-Enum bietet `audio-gateway (A2DP Source & HSP/HFP AG)`. Der Nutzer hat die Gegenseite im H390-Headset +gehört → die HFP-Nodes **existierten**, der Watcher hat sie nur nicht erkannt. + +**Ursache: eine, mit zwei Symptomen.** `aime-iphone-route.sh` v1 hat nie gegriffen: + +1. *Hartcodierte Portnamen.* Gesucht wurde `bluez_output.…:input_FL`. HFP/mSBC ist **mono**, die Ports + heißen `playback_MONO`/`capture_MONO` → der `grep` matchte nie, das Script tat buchstäblich nichts. + Erklärt die 0 Log-Zeilen. +2. *Einmal-Guard.* `if ! pw-link -l "$B" | grep -q micgate.out` übersprang den gesamten Block, sobald + micgate einmal verlinkt war. Da WirePlumber die HFP-Nodes **nicht atomar** anlegt (Uplink erscheint + typischerweise vor der Gegenseite), blieb `bluez_input` in genau diesem einen Durchlauf leer → die + Gegenseite wurde für den ganzen Anruf nie nachverlinkt. Ebenso wurde ein erst mitten im Anruf von + WirePlumber gesetztes `callrec_mix → Uplink` nie mehr entfernt. + +Daraus: Gegenseite → `callmix` fehlt → **nicht in der Aufnahme**. Und `callrec_mix → Uplink` bleibt stehen → +Uplink führt den ungegateten Mix statt `micgate.out` → **Rückfall auf genau den Echo-Zustand vor dem +Fix vom 2026-07-13**. + +**Fix `aime-iphone-route.sh` v2 (`.bak-v1`):** Portnamen werden über `pw-link -i/-o` **ermittelt** statt +hartcodiert; neue `link_all`-Hilfsfunktion legt **genau einen Link pro Ziel-Port** (mono-Quelle speist beide +Kanäle; Stereo-Quelle auf mono-HFP-Port sendet nur FL — beide zu verlinken würde sie in denselben Port +summieren = +6 dB auf dem Uplink). Alle drei Links werden in **jedem** Durchlauf einzeln geprüft und +nachgezogen (Takt 2 s → 1 s), das Einmal-Guard ist weg. `logger` schreibt nur bei Zustandswechsel, damit +das Journal nicht flutet. + +**Verifiziert:** `bash -n` OK; `link_all` mit gestubbtem `pw-link` gegen alle drei Fälle getestet +(stereo→stereo, stereo→mono, mono→stereo) und gegen leere Arrays; Graph nach dem Service-Neustart +unverändert bei 32 Links, Script idle ohne HFP-Nodes. **Noch offen: Gegenprobe am echten Anruf** — +`journalctl -t aime-iphone-route` muss dann `+micgate` und `+far-end` zeigen. + +**Doku:** Kapitel 06 — Watcher-Sequenzdiagramm auf den Ein-Sekunden-Prüfzyklus umgestellt, Klarstellung +dass Gegenseite → Headset von WirePlumber kommt und nicht vom Watcher (das alte Diagramm behauptete das +Gegenteil), Kontrollkommando ergänzt. + +**Nebenbefund (offen):** `callrec.service` ist am 2026-07-30 13:39:33 mit `Error: write EPIPE` in +`llm.js:84` gestorben (rkllm-Socket beim Schreiben weg), systemd hat neu gestartet (Restart-Counter 1). +Der Socket-Write in `llm.js` hat keinen `error`-Handler → unhandled `'error'` event reißt den ganzen +Server mit. Die Zusammenfassung von #81 dürfte dadurch fehlen. + ## 2026-07-29 — #79: verlorener Sprecher + magere Zusammenfassung (Vergleich gegen MS Teams) **Ausgangslage (Nutzer):** Aufnahme #79 (23-min-Marketing-Jour-fixe) wurde trotz live getippter Namen