Files
Aime/app/aime-usb-audio-setup.sh
T
schakal00783andClaude Fable 5 7391b0ded7 Mikro->Mix gegatet: Doppelweg-Hall der Gegenseite beseitigt
Cepstral-Beweis auf #71: stabiler 80ms-Peak nur auf Gegenseiten-Segmenten
= Muschel-Bleed durchs offene Mikro zusaetzlich zum digitalen Pfad.
cr_mic2mix-Loopback entfernt, stattdessen pw-link micgate.out -> callmix
(Fallback ohne Gate bleibt). Verschmierte Embeddings (verschmolzene
Remote-Sprecher) sollten damit ebenfalls weg sein - echter Call offen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 00:11:58 +02:00

120 lines
6.5 KiB
Bash

#!/bin/bash
# CallRec USB-headset audio topology (persistent).
# Logitech H390 = user mic + user ears. UAC2 gadget = PC bridge.
# - PC audio (gadget capture, PC->device) -> headset sink (user hears Windows)
# - PC audio -> callmix (far side of the recording)
# - headset mic -> callmix (near side of the recording)
# - headset mic -> micgate -> gadget sink (device->PC) (far end / PC hears the user)
# CallRec records callmix.monitor (both sides).
# Idempotent: if callmix already exists, does nothing.
#
# NOISE GATE (2026-07-08): the user's boom mic picks up the far-end acoustically
# (proven: muting the Logi mic silences the gadget sink completely; NOT via the
# headset earcup). Teams/Discord AGC then renormalizes it, so lowering mic gain
# alone does nothing -- the far end hears itself with the loopback round-trip
# delay. Fix: route mic->PC THROUGH a LADSPA gate (aime-micgate.service, sink
# "micgate"). In the user's speech pauses the gate closes -> the gadget sink goes
# to absolute silence -> the far end can't hear itself; AGC can't amplify silence.
# Mic HW gain is kept LOW (not the old +11 dB max) on purpose: it keeps the idle
# bleed below the gate threshold (-45 dB), so the gate stays shut when the user is
# quiet even while the far end is loud.
set -u
export XDG_RUNTIME_DIR=/run/user/1000
PCAUD=alsa_input.platform-fc000000.usb.stereo-fallback # PC->device (Windows playing)
PCSINK=alsa_output.platform-fc000000.usb.stereo-fallback # device->PC (what Windows records)
HSSINK=alsa_output.usb-Logi_USB_Headset_Logi_USB_Headset-00.analog-stereo
HSMIC=alsa_input.usb-Logi_USB_Headset_Logi_USB_Headset-00.mono-fallback
# already configured?
if pactl list sinks short 2>/dev/null | grep -qw callmix; then
echo "callmix already present; nothing to do"
exit 0
fi
# wait for the gadget + headset PipeWire nodes to appear (headset/UAC2 enumerate
# a little after boot)
for i in $(seq 1 60); do
if pactl list sources short 2>/dev/null | grep -q "$PCAUD" \
&& pactl list sinks short 2>/dev/null | grep -q "$HSSINK" \
&& pactl list sources short 2>/dev/null | grep -q "$HSMIC"; then
break
fi
sleep 1
done
# wait briefly for the noise-gate sink (aime-micgate.service, a separate pipewire
# filter-chain client). Route the user's mic THROUGH it if present; fall back to
# the raw gadget sink so mic->PC still works even if the gate failed to start.
MIC_OUT="$PCSINK"
for i in $(seq 1 15); do
if pactl list sinks short 2>/dev/null | grep -qw micgate; then MIC_OUT=micgate; break; fi
sleep 1
done
# set the volume of a loopback's sink-input, found by its media.name tag (robust
# across reboots / stream-restore reshuffles, unlike numeric indices).
si_vol() { # $1 = media.name, $2 = volume (e.g. 60%)
local si
si=$(pactl list sink-inputs 2>/dev/null | awk -v n="$1" '
/^Sink Input #/{id=$3; gsub("#","",id)}
index($0, "media.name = \"" n "\""){print id; exit}')
[ -n "$si" ] && pactl set-sink-input-volume "$si" "$2" >/dev/null 2>&1
}
# headset mic hardware gain kept LOW (see NOISE GATE note): +1.4 dB, not +11 max.
# Low analog gain -> low idle bleed -> gate stays shut in the user's pauses.
amixer -c Headset sset Mic 12 unmute >/dev/null 2>&1 # belt (overridden by the source-vol below)
pactl set-source-volume "$HSMIC" 62% >/dev/null 2>&1 # WirePlumber-owned; 62%% -> HW Mic step 12 (~+1.4 dB). 100%% would map to +11 dB max and re-open the gate on far-end bleed.
# virtual recording mix
pactl load-module module-null-sink sink_name=callmix \
sink_properties=device.description=CallRecMix >/dev/null 2>&1
pactl set-sink-volume callmix 100% >/dev/null 2>&1
# routes — each loopback tagged so we can set per-stream gain by name
pactl load-module module-loopback source="$PCAUD" sink="$HSSINK" latency_msec=60 sink_input_properties=media.name=cr_pc2hs >/dev/null 2>&1 # PC -> headset (monitor)
pactl load-module module-loopback source="$PCAUD" sink=callmix latency_msec=60 sink_input_properties=media.name=cr_pc2mix >/dev/null 2>&1 # PC -> mix
pactl load-module module-loopback source="$HSMIC" sink="$MIC_OUT" latency_msec=60 sink_input_properties=media.name=cr_mic2pc >/dev/null 2>&1 # mic -> (gate ->) PC
# mic -> mix: GATED. The old direct HSMIC->callmix loopback put the far end into
# the recording TWICE — digitally (cr_pc2mix) AND ~80 ms later as earcup bleed
# through the always-open mic (cepstral peak 79-80 ms on #71) -> far-end voices
# sounded like a reverberant room AND their smeared embeddings made diarization
# merge two remote speakers into one. Tapping the gate's OUTPUT instead keeps the
# mic out of the mix in the user's speech pauses (same trick as the Teams
# self-echo fix / iPhone uplink). Fallback: no gate -> old direct loopback.
if [ "$MIC_OUT" = micgate ]; then
for i in $(seq 1 10); do
pw-link micgate.out:output_FL callmix:playback_FL >/dev/null 2>&1 && \
pw-link micgate.out:output_FR callmix:playback_FR >/dev/null 2>&1 && break
sleep 1
done
else
pactl load-module module-loopback source="$HSMIC" sink=callmix latency_msec=60 sink_input_properties=media.name=cr_mic2mix >/dev/null 2>&1 # mic -> mix (UNgated fallback)
fi
# GAIN STAGING (clip-safe): PC media hits ~0 dBFS, so knock it down hard in the
# mix; the mic runs near unity. Their sum must never reach 0 dBFS or clipping
# corrupts whisper + diarization (over-segments to many speakers). PC monitoring
# to the headset (cr_pc2hs) stays at unity — full volume.
sleep 1
si_vol cr_pc2mix 60% # far side into the recording (~-12 dB)
si_vol cr_mic2mix 100% # near side (user) into the mix — only exists in the UNgated fallback
si_vol cr_mic2pc 65% # near side to the far end (through the gate)
# NOTE: fine mic/PC balance still wants tuning on a REAL call. If the far end
# finds the user too quiet, raise cr_mic2pc / Mic gain -- but watch the gate:
# more gain -> more idle bleed -> may cross the -45 dB threshold and re-open the
# gate on the far-end bleed (echo returns). Retune the gate Threshold together.
# expose the mix as a REAL source (not a .monitor) so the CallRec UI lists it —
# recorder.js listSources() filters out every *.monitor.
pactl load-module module-remap-source master=callmix.monitor source_name=callrec_mix \
source_properties=device.description=CallRecMix >/dev/null 2>&1
# defaults: user hears via headset, CallRec records the mix
pactl set-default-sink "$HSSINK" >/dev/null 2>&1
pactl set-default-source callrec_mix >/dev/null 2>&1
echo "aime usb-audio topology loaded (mic->PC via micgate=$MIC_OUT)"