app/ = /opt/callrec (Node-Backend, Vite-Frontend, Python-Inferenz, Audio-Skripte, rkllm_server.cpp), system/ = systemd-Units, WirePlumber/PipeWire-Drop-ins, sudoers. Modelle, Binaries, Aufnahmen und node_modules bleiben draussen (.gitignore). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
5.7 KiB
Bash
104 lines
5.7 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=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)"
|