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.
112 lines
4.6 KiB
Bash
112 lines
4.6 KiB
Bash
#!/bin/bash
|
|
# 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 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.
|
|
#
|
|
# 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() { pw-link -i 2>/dev/null | grep -E "^$1"; }
|
|
ports_out() { pw-link -o 2>/dev/null | grep -E "^$1"; }
|
|
|
|
# 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 }
|
|
'
|
|
}
|
|
|
|
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[@]} -eq 0 ]; then
|
|
log_state=""
|
|
sleep 1
|
|
continue
|
|
fi
|
|
|
|
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
|
|
|
|
# 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
|
|
CH+=("!no-micgate-uplink-keeping-callrec_mix")
|
|
fi
|
|
done
|
|
|
|
# --- 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
|
|
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
|