#!/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). # # 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 # 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 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 1 done