fix(iphone-bt): v3 — pw-link -l filtert nicht, v2 machte den Uplink stumm
Testanruf 13:51 loggte nur "-callrec_mix", kein +micgate und kein +far-end: v2 hat WirePlumbers Link getrennt und dann nichts verlinkt, der Uplink hatte keine Quelle und die Gegenseite hörte Stille. Ursache: `pw-link -l <node>` filtert nicht. Mit einem Node-Namen, der nicht als Port auflösbar ist, kommt der ganze Graph zurück — und der enthält immer micgate.out und callmix. Also war jedes "ist das schon verlinkt?"-grep wahr und nur das Trennen lief. Derselbe Ausdruck stand schon in v1, dort aber hinter dem Einmal-Guard, der nie erreicht wurde. v3: - links_of PORT parst die Peers eines exakten Ports aus einem Graph-Dump (awk), kein grep mehr über die gesamte Ausgabe. - Erst verlinken, dann trennen: callrec_mix->Uplink fliegt nur raus, wenn micgate.out am Uplink-Port bestätigt anliegt. Sonst bleibt der WirePlumber-Link und es wird !no-micgate-uplink-keeping-callrec_mix geloggt. Ein Echo ist schlecht, unhörbar sein ist schlimmer. - Log-Zustand als Array statt String: " " hätte -n passiert und bei fertig geroutetem Anruf jede Sekunde eine Journal-Zeile geschrieben. Getestet: links_of gegen den echten Graph (verlinkt/unverlinkt/nicht existent); Entscheidungslogik gegen einen synthetischen Mono-HFP-Graph — Default-Zustand, fertig geroutet (No-op), micgate nicht bestätigt (behält callrec_mix). Gegenprobe am echten Anruf steht noch aus.
This commit is contained in:
+84
-55
@@ -4,79 +4,108 @@
|
||||
# far-end voice (bluez_input) -> callmix (recorded digitally, clean)
|
||||
# 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).
|
||||
# Far-end -> headset earpiece is WirePlumber's own default link, NOT ours; callmix
|
||||
# is a pure recording sink with no output.
|
||||
#
|
||||
# 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.
|
||||
# v3 (2026-07-30): `pw-link -l <node>` DOES NOT FILTER. Given a node name it cannot
|
||||
# resolve as a port it prints the ENTIRE graph, which always contains micgate.out
|
||||
# and callmix -> every "is it already linked?" grep returned true, so v2 removed
|
||||
# callrec_mix->uplink and then linked nothing: the far end heard silence.
|
||||
# Link state is now parsed per PORT out of one graph dump (links_of).
|
||||
# v2 (2026-07-30): port names are discovered, not hardcoded (HFP/mSBC is MONO, so
|
||||
# the ports are playback_MONO/capture_MONO, not input_FL/input_FR -- the old
|
||||
# hardcoded grep never matched at all), and every link is verified on EVERY pass.
|
||||
# The old one-shot guard meant that if the far-end node appeared a tick later than
|
||||
# the uplink node (WirePlumber does not create the HFP nodes atomically), the
|
||||
# far-end was never linked for the whole call.
|
||||
export XDG_RUNTIME_DIR=/run/user/1000
|
||||
MAC=B4_96_A5_42_BD_D6
|
||||
|
||||
# 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<m; i++)); do
|
||||
pw-link $mode "${_src[i % n]}" "${_dst[i]}" 2>/dev/null
|
||||
done
|
||||
# links_of PORT -- the peers linked to exactly this port, one per line, read from
|
||||
# $GRAPH. pw-link -l prints a port header at column 0 followed by indented
|
||||
# "|-> peer" / "|<- peer" lines, so match the header exactly and strip the arrows.
|
||||
links_of() {
|
||||
printf '%s\n' "$GRAPH" | awk -v p="$1" '
|
||||
/^[^ \t]/ { inblk = ($0 == p); next }
|
||||
inblk { sub(/^[ \t]*\|[-<>]+[ \t]*/, ""); print }
|
||||
'
|
||||
}
|
||||
|
||||
log_once=""
|
||||
has_peer() { # has_peer REGEX PEER... -> 0 if any peer matches
|
||||
local re=$1; shift
|
||||
printf '%s\n' "$@" | grep -qE "$re"
|
||||
}
|
||||
|
||||
log_state=""
|
||||
while true; do
|
||||
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=""
|
||||
if [ ${#UP[@]} -eq 0 ]; then
|
||||
log_state=""
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
|
||||
# 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"
|
||||
GRAPH=$(pw-link -l 2>/dev/null)
|
||||
CH=()
|
||||
mapfile -t GATE < <(ports_out 'micgate\.out')
|
||||
|
||||
# --- uplink: exactly one gated-mic source per uplink port, no ungated mix ---
|
||||
for ((i = 0; i < ${#UP[@]}; i++)); do
|
||||
port=${UP[i]}
|
||||
mapfile -t peers < <(links_of "$port")
|
||||
|
||||
# ADD BEFORE REMOVE. One link per uplink port: a stereo gate into a mono HFP
|
||||
# port sends FL only, linking both would sum them into the same port (+6 dB).
|
||||
if [ ${#GATE[@]} -gt 0 ] && ! has_peer '^micgate\.out:' "${peers[@]}"; then
|
||||
pw-link "${GATE[i % ${#GATE[@]}]}" "$port" 2>/dev/null && CH+=(+micgate)
|
||||
GRAPH=$(pw-link -l 2>/dev/null)
|
||||
mapfile -t peers < <(links_of "$port")
|
||||
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
|
||||
# WirePlumber's callrec_mix -> uplink is the echo path. Drop every instance;
|
||||
# re-checked each pass because WP can add it at any point during the call.
|
||||
# SAFETY INVARIANT: only ever remove it once the gated mic is CONFIRMED linked.
|
||||
# v2 removed it and then linked nothing (the state check was broken) -> the
|
||||
# uplink had no source at all and the far end heard pure silence. An echo is
|
||||
# bad; being inaudible is worse.
|
||||
if has_peer '^micgate\.out:' "${peers[@]}"; then
|
||||
for pe in "${peers[@]}"; do
|
||||
case $pe in
|
||||
callrec_mix:*) pw-link -d "$pe" "$port" 2>/dev/null && CH+=(-callrec_mix) ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
CHANGED="$CHANGED (no-far-end-node-yet)"
|
||||
CH+=("!no-micgate-uplink-keeping-callrec_mix")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$CHANGED" ] && [ "$CHANGED" != "$log_once" ]; then
|
||||
logger -t aime-iphone-route "iPhone call $B:$CHANGED"
|
||||
log_once="$CHANGED"
|
||||
fi
|
||||
# --- far-end into the recording mix (this node often appears after the uplink) ---
|
||||
mapfile -t FAR < <(ports_out "bluez_input\.${MAC}")
|
||||
if [ ${#FAR[@]} -eq 0 ]; then
|
||||
CH+=("(no-far-end-node-yet)")
|
||||
else
|
||||
log_once=""
|
||||
mapfile -t REC < <(ports_in 'callmix')
|
||||
for ((j = 0; j < ${#REC[@]}; j++)); do
|
||||
rport=${REC[j]}
|
||||
mapfile -t rpeers < <(links_of "$rport")
|
||||
if ! has_peer "^bluez_input\.${MAC}" "${rpeers[@]}"; then
|
||||
pw-link "${FAR[j % ${#FAR[@]}]}" "$rport" 2>/dev/null && CH+=(+far-end)
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# collapse repeats (one entry per uplink/callmix channel) and log only on change.
|
||||
# An ARRAY, not a string: a whitespace-only string passes -n and would log on
|
||||
# every single pass once the call is fully routed.
|
||||
if [ ${#CH[@]} -gt 0 ]; then
|
||||
msg=$(printf '%s\n' "${CH[@]}" | awk '!seen[$0]++' | tr '\n' ' ')
|
||||
if [ "$msg" != "$log_state" ]; then
|
||||
logger -t aime-iphone-route "iPhone call ${UP[0]%%:*}: $msg"
|
||||
log_state="$msg"
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
+18
-3
@@ -58,8 +58,8 @@ sequenceDiagram
|
||||
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: nur wenn micgate BESTÄTIGT anliegt: callrec_mix → Uplink trennen
|
||||
W->>PW: Gegenseite → callmix verlinken, falls fehlt (Node kommt später!)
|
||||
end
|
||||
IP->>BZ: Anruf endet
|
||||
@@ -72,8 +72,23 @@ sequenceDiagram
|
||||
- **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.
|
||||
- **Erst verlinken, dann trennen.** `callrec_mix → Uplink` fliegt nur raus, wenn `micgate.out` am
|
||||
Uplink-Port bestätigt anliegt — sonst bleibt der Uplink ohne Quelle und die Gegenseite hört Stille.
|
||||
- ⚠️ **`pw-link -l <node>` filtert NICHT.** Bei einem Namen, der nicht als *Port* auflösbar ist, kommt der
|
||||
**gesamte Graph** zurück. Jedes `grep` darauf ist immer wahr. Link-Zustand deshalb nur per `links_of`
|
||||
(exakter Port-Header aus einem Graph-Dump) prüfen, nie per `grep` über die ganze Ausgabe.
|
||||
|
||||
## Kontrolle nach einem Anruf
|
||||
|
||||
`journalctl -t aime-iphone-route` — die eine Zeile sagt alles:
|
||||
|
||||
| Ausgabe | Bedeutung |
|
||||
|---|---|
|
||||
| `+micgate -callrec_mix +far-end` | korrekt geroutet |
|
||||
| keine Zeile | Watcher hat die HFP-Nodes nicht gesehen → Aufnahme hat nur die Nahseite |
|
||||
| nur `-callrec_mix` | Uplink ohne Quelle → **Gegenseite hört nichts** (v2-Bug, behoben) |
|
||||
| `!no-micgate-uplink-keeping-callrec_mix` | Uplink hörbar, aber mit Echo — micgate-Link ging nicht |
|
||||
| `(no-far-end-node-yet)` | unkritisch, wenn eine spätere Zeile `+far-end` zeigt |
|
||||
|
||||
## Offen
|
||||
|
||||
|
||||
+47
-1
@@ -3,7 +3,53 @@
|
||||
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
|
||||
## 2026-07-30 (nachmittags) — v2 machte den Uplink stumm, v3 behebt es
|
||||
|
||||
**Symptom (Nutzer, Testanruf 13:51):** „Die Gegenseite hat mich nicht mehr gehört." Regression aus v2 vom
|
||||
selben Tag. Das Journal zeigte den Beweis direkt:
|
||||
|
||||
```
|
||||
iPhone call bluez_output.B4_96_A5_42_BD_D6.1: -callrec_mix
|
||||
```
|
||||
|
||||
Nur `-callrec_mix`, kein `+micgate`, kein `+far-end` — v2 hat WirePlumbers Link **getrennt** und dann
|
||||
**nichts** verlinkt. Uplink ohne Quelle = die Gegenseite hört absolute Stille. Immerhin: v2 hat die
|
||||
HFP-Nodes erstmals überhaupt erkannt, der Portnamen-Fix von vormittags greift also.
|
||||
|
||||
**Ursache: `pw-link -l <node>` filtert nicht.** Reproduziert auf dem Board: mit einem Node-Namen, der nicht
|
||||
als *Port* auflösbar ist, gibt `pw-link -l` den **gesamten Graph** aus (sogar `pw-link -l micgate.out`
|
||||
liefert zusätzlich `micgate:playback_MONO`). Der Graph enthält immer `micgate.out` und `callmix` →
|
||||
|
||||
- `grep -q micgate.out` → wahr → „schon verlinkt" → **micgate-Link übersprungen**
|
||||
- `grep -q callmix` → wahr → **Gegenseiten-Link übersprungen**
|
||||
- `grep -q callrec_mix` → wahr → das Trennen war die einzige Aktion, die je lief
|
||||
|
||||
Derselbe Ausdruck steckte schon in v1, dort aber hinter dem Einmal-Guard, der ohnehin nie erreicht wurde.
|
||||
|
||||
**Fix `aime-iphone-route.sh` v3 (`.bak-v2`):**
|
||||
|
||||
- Neue `links_of PORT` parst die Peers **eines exakten Ports** aus einem einzigen Graph-Dump (awk: Header
|
||||
auf Spalte 0 exakt vergleichen, eingerückte `|->`/`|<-`-Zeilen als Peers). Kein `grep` mehr über alles.
|
||||
- **Sicherheitsbedingung: erst verlinken, dann trennen.** `callrec_mix → Uplink` wird nur entfernt, wenn
|
||||
`micgate.out` am Uplink-Port **bestätigt** anliegt (Graph danach neu gelesen). Sonst bleibt der
|
||||
WirePlumber-Link stehen und es wird `!no-micgate-uplink-keeping-callrec_mix` geloggt. Ein Echo ist
|
||||
schlecht, unhörbar sein ist schlimmer.
|
||||
- Log-Zustand als **Array** statt String: `CHANGED=" "` (nur Leerzeichen) hätte `[ -n ]` passiert und bei
|
||||
fertig geroutetem Anruf jede Sekunde eine Journal-Zeile geschrieben.
|
||||
|
||||
**Verifiziert:** `links_of` gegen den echten Graph (verlinkter Port, unverlinkter Port, nicht existierender
|
||||
Port — jeweils exakt). Entscheidungslogik gegen einen synthetischen **Mono**-HFP-Graph mit gestubbtem
|
||||
`pw-link`: Pass 1 (WirePlumber-Default aktiv) → `+micgate` zuerst, dann **beide** `callrec_mix`-Links weg,
|
||||
Gegenseite in **beide** callmix-Kanäle, und **genau ein** micgate-Link in den Mono-Port (zwei wären +6 dB);
|
||||
Pass 2 (fertig geroutet) → vollständiger No-op, keine Log-Zeile; Pass 3 (micgate nicht bestätigt) →
|
||||
`callrec_mix` bleibt stehen, kein Trennen. Nach dem Neustart Graph unverändert bei 32 Links, Script idle
|
||||
ohne Anruf.
|
||||
|
||||
**Noch offen:** Gegenprobe am echten Anruf. Erwartet wird eine Zeile mit `+micgate`, `-callrec_mix` und
|
||||
`+far-end`. Steht dort `!no-micgate-uplink-keeping-callrec_mix`, ist der Uplink hörbar aber mit Echo, und
|
||||
der micgate-Link braucht eine eigene Analyse.
|
||||
|
||||
## 2026-07-30 (vormittags) — 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
|
||||
|
||||
Reference in New Issue
Block a user