#!/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=callmix latency_msec=60 sink_input_properties=media.name=cr_mic2mix >/dev/null 2>&1 # mic -> 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 # 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 recording mix 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)"