732 lines
23 KiB
Bash
Executable File
732 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ------------------------------------------------------------
|
|
# Streaming-Audio-Setup für Fedora / PipeWire / OBS / Discord
|
|
# ------------------------------------------------------------
|
|
#
|
|
# install = virtuelle PipeWire-Geräte dauerhaft anlegen
|
|
# cleanup = alte manuelle null-sink-Module und doppelte alte Configs entfernen/umbenennen
|
|
# route = Standardmikrofon setzen, aktive Apps verschieben und Links setzen
|
|
# status = Diagnose anzeigen
|
|
# restore-mic = echtes ZOOM/H6 wieder als System-Standardmikrofon setzen
|
|
#
|
|
# Ziel:
|
|
# - Mikrofon -> OBS-Filter -> DiscordMicMix -> Discord-Mikrofon -> Discord
|
|
# - Soundboard/Stream-Deck -> SoundboardSounds -> Line-Out + OBS
|
|
# - SoundboardSounds -> eigener 75-%-Loopback -> DiscordMicMix
|
|
# - Firebot/Chromium -> FirebotSounds -> Line-Out + OBS
|
|
# - Firefox -> FirebotSounds -> Line-Out + OBS
|
|
# - GeForce NOW -> GameAudio -> Line-Out + OBS
|
|
# - Discord-Leute -> Discord -> Line-Out + OBS
|
|
# - Spiel/Systemsound -> GameAudio -> Line-Out + OBS
|
|
#
|
|
# Wichtig:
|
|
# - In OBS muss als Audiomonitoring-Gerät "DiscordMicMix" eingestellt sein.
|
|
# - Beim Mikrofon in OBS muss "Monitor und Ausgabe" aktiviert sein.
|
|
# - Beim Soundboard und allen anderen OBS-Quellen bleibt das Monitoring aus.
|
|
# - Das echte Mikrofon wird NICHT direkt mit DiscordMicMix verbunden, damit
|
|
# Discord nicht gleichzeitig die rohe und die gefilterte Stimme erhält.
|
|
# - Firebot geht NICHT in Discord.
|
|
# - GameAudio geht NICHT in Discord.
|
|
# - DiscordMicMix wird NICHT in OBS aufgenommen, sonst hast du Hall.
|
|
# - Starship/Matisse wird NICHT in OBS aufgenommen, sonst kommen Signale doppelt.
|
|
|
|
MIC_RE='Handy Recorder|ZOOM|H6'
|
|
LINE_OUT_RE='Starship|Matisse|alsa_output.pci-0000_0b_00.4'
|
|
|
|
DISCORD_SINK='discord_sink'
|
|
DISCORD_MICMIX='discord_micmix'
|
|
DISCORD_MIC_SOURCE='discord_microphone'
|
|
DISCORD_MIC_DESCRIPTION='Discord-Mikrofon'
|
|
FIREBOT_SINK='firebot_sounds'
|
|
SOUNDBOARD_SINK='soundboard_sounds'
|
|
GAME_SINK='game_audio'
|
|
|
|
# Nur echte Discord/WebRTC-Prozesse erfassen. Ein bloßes "Discord" würde auch
|
|
# OBS-Quellen wie "Discord-Sound" oder Streams auf discord_micmix erwischen.
|
|
DISCORD_APP_RE='application.name = "(WEBRTC VoiceEngine|Discord|discord)"|application.process.binary = "(Discord|discord)"'
|
|
|
|
# Nur die Discord-Kopien werden verändert. OBS-Stream und Kopfhörer bleiben gleich.
|
|
DISCORD_MIC_VOLUME="${DISCORD_MIC_VOLUME:-115%}"
|
|
DISCORD_SOUNDBOARD_VOLUME="${DISCORD_SOUNDBOARD_VOLUME:-75%}"
|
|
DISCORD_SOUNDBOARD_LATENCY_MS="${DISCORD_SOUNDBOARD_LATENCY_MS:-30}"
|
|
|
|
CONFIG_DIR="$HOME/.config/pipewire/pipewire.conf.d"
|
|
CONFIG_FILE="$CONFIG_DIR/10-streaming-sinks.conf"
|
|
|
|
die() {
|
|
echo "Fehler: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
need_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "$1 fehlt. Installiere es bitte zuerst."
|
|
}
|
|
|
|
show_header() {
|
|
echo
|
|
echo "== $1 =="
|
|
}
|
|
|
|
cleanup_old_modules() {
|
|
need_cmd pactl
|
|
|
|
show_header "Entferne alte manuelle null-sink-Module dieses Setups"
|
|
|
|
local ids
|
|
ids="$(
|
|
pactl list short modules | awk '
|
|
/module-null-sink/ && ($0 ~ /discord_sink|discord_micmix|firebot_sounds|soundboard_sounds|game_audio|DiscordMicMix|FirebotSounds|SoundboardSounds|GameAudio/) {
|
|
print $1
|
|
}
|
|
'
|
|
)"
|
|
|
|
if [[ -z "${ids}" ]]; then
|
|
echo "Keine alten manuellen module-null-sink-Instanzen gefunden."
|
|
return 0
|
|
fi
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
echo "Entlade altes Modul: $id"
|
|
pactl unload-module "$id" 2>/dev/null || true
|
|
done <<< "$ids"
|
|
|
|
echo "Module-Cleanup fertig."
|
|
}
|
|
|
|
cleanup_soundboard_discord_loopbacks() {
|
|
need_cmd pactl
|
|
|
|
local ids
|
|
ids="$(
|
|
pactl list short modules | awk -v source="${SOUNDBOARD_SINK}.monitor" -v sink="$DISCORD_MICMIX" '
|
|
$2 == "module-loopback" && index($0, "source=" source) && index($0, "sink=" sink) {
|
|
print $1
|
|
}
|
|
'
|
|
)"
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
echo "Entlade alten Soundboard-Discord-Loopback: $id"
|
|
pactl unload-module "$id" 2>/dev/null || true
|
|
done <<< "$ids"
|
|
}
|
|
|
|
cleanup_discord_microphone_modules() {
|
|
need_cmd pactl
|
|
|
|
local ids
|
|
ids="$(
|
|
pactl list short modules | awk -v source_name="$DISCORD_MIC_SOURCE" '
|
|
$2 == "module-remap-source" && index($0, "source_name=" source_name) {
|
|
print $1
|
|
}
|
|
'
|
|
)"
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
echo "Entlade altes virtuelles Discord-Mikrofon: $id"
|
|
pactl unload-module "$id" 2>/dev/null || true
|
|
done <<< "$ids"
|
|
}
|
|
|
|
cleanup_old_configs() {
|
|
mkdir -p "$CONFIG_DIR"
|
|
|
|
show_header "Prüfe alte doppelte PipeWire-Configs"
|
|
|
|
shopt -s nullglob
|
|
for file in "$CONFIG_DIR"/*.conf; do
|
|
[[ "$file" == "$CONFIG_FILE" ]] && continue
|
|
|
|
if grep -qE 'discord_sink|discord_micmix|firebot_sounds|soundboard_sounds|game_audio' "$file" 2>/dev/null; then
|
|
local disabled="${file}.disabled-by-streaming-audio"
|
|
local suffix=1
|
|
|
|
while [[ -e "$disabled" ]]; do
|
|
disabled="${file}.disabled-by-streaming-audio.${suffix}"
|
|
((suffix += 1))
|
|
done
|
|
|
|
echo "Deaktiviere alte Config: $file"
|
|
mv "$file" "$disabled"
|
|
echo "Umbenannt nach: $disabled"
|
|
fi
|
|
done
|
|
shopt -u nullglob
|
|
}
|
|
|
|
install_sinks() {
|
|
need_cmd systemctl
|
|
need_cmd pactl
|
|
|
|
cleanup_old_modules
|
|
cleanup_soundboard_discord_loopbacks
|
|
cleanup_discord_microphone_modules
|
|
cleanup_old_configs
|
|
|
|
show_header "Schreibe PipeWire-Config"
|
|
|
|
mkdir -p "$CONFIG_DIR"
|
|
|
|
cat > "$CONFIG_FILE" <<'EOF'
|
|
context.objects = [
|
|
{
|
|
factory = adapter
|
|
args = {
|
|
factory.name = support.null-audio-sink
|
|
node.name = "discord_sink"
|
|
node.description = "Discord"
|
|
media.class = "Audio/Sink"
|
|
audio.position = [ FL FR ]
|
|
}
|
|
}
|
|
{
|
|
factory = adapter
|
|
args = {
|
|
factory.name = support.null-audio-sink
|
|
node.name = "discord_micmix"
|
|
node.description = "DiscordMicMix"
|
|
media.class = "Audio/Sink"
|
|
audio.position = [ FL FR ]
|
|
}
|
|
}
|
|
{
|
|
factory = adapter
|
|
args = {
|
|
factory.name = support.null-audio-sink
|
|
node.name = "firebot_sounds"
|
|
node.description = "FirebotSounds"
|
|
media.class = "Audio/Sink"
|
|
audio.position = [ FL FR ]
|
|
}
|
|
}
|
|
{
|
|
factory = adapter
|
|
args = {
|
|
factory.name = support.null-audio-sink
|
|
node.name = "soundboard_sounds"
|
|
node.description = "SoundboardSounds"
|
|
media.class = "Audio/Sink"
|
|
audio.position = [ FL FR ]
|
|
}
|
|
}
|
|
{
|
|
factory = adapter
|
|
args = {
|
|
factory.name = support.null-audio-sink
|
|
node.name = "game_audio"
|
|
node.description = "GameAudio"
|
|
media.class = "Audio/Sink"
|
|
audio.position = [ FL FR ]
|
|
}
|
|
}
|
|
]
|
|
EOF
|
|
|
|
echo "PipeWire-Config geschrieben: $CONFIG_FILE"
|
|
|
|
show_header "Starte PipeWire neu"
|
|
systemctl --user restart pipewire pipewire-pulse wireplumber
|
|
|
|
show_header "Virtuelle Sinks"
|
|
pactl list short sinks | grep -E 'discord|firebot|soundboard|game' || true
|
|
|
|
show_header "Virtuelle Sources/Monitore"
|
|
pactl list short sources | grep -E 'discord|firebot|soundboard|game' || true
|
|
|
|
echo
|
|
echo "Install fertig. Discord, OBS, Firebot und Soundboard danach bitte neu starten."
|
|
}
|
|
|
|
find_output_port() {
|
|
local node_re="$1"
|
|
local port_re="$2"
|
|
|
|
pw-link -o | grep -Ei "$node_re" | grep -Ei "$port_re" | head -n1 || true
|
|
}
|
|
|
|
find_input_port() {
|
|
local node_re="$1"
|
|
local port_re="$2"
|
|
|
|
pw-link -i | grep -Ei "$node_re" | grep -Ei "$port_re" | head -n1 || true
|
|
}
|
|
|
|
safe_link() {
|
|
local out="$1"
|
|
local in="$2"
|
|
|
|
if [[ -z "$out" || -z "$in" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
pw-link "$out" "$in" 2>/dev/null || true
|
|
}
|
|
|
|
safe_unlink() {
|
|
local out="$1"
|
|
local in="$2"
|
|
|
|
if [[ -z "$out" || -z "$in" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
pw-link -d "$out" "$in" 2>/dev/null || true
|
|
}
|
|
|
|
link_lr() {
|
|
local from_node="$1"
|
|
local from_kind="$2"
|
|
local to_node="$3"
|
|
local to_kind="$4"
|
|
|
|
local out_l out_r in_l in_r
|
|
|
|
out_l="$(find_output_port "$from_node" "${from_kind}.*(FL|front-left|L$|left)")"
|
|
out_r="$(find_output_port "$from_node" "${from_kind}.*(FR|front-right|R$|right)")"
|
|
|
|
in_l="$(find_input_port "$to_node" "${to_kind}.*(FL|front-left|L$|left)")"
|
|
in_r="$(find_input_port "$to_node" "${to_kind}.*(FR|front-right|R$|right)")"
|
|
|
|
if [[ -z "$out_l" && -z "$out_r" ]]; then
|
|
echo "Warnung: Keine Outputs gefunden für: $from_node / $from_kind"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -z "$in_l" && -z "$in_r" ]]; then
|
|
echo "Warnung: Keine Inputs gefunden für: $to_node / $to_kind"
|
|
return 0
|
|
fi
|
|
|
|
safe_link "$out_l" "$in_l"
|
|
safe_link "$out_r" "$in_r"
|
|
|
|
echo "Link: $from_node/$from_kind → $to_node/$to_kind"
|
|
}
|
|
|
|
unlink_lr() {
|
|
local from_node="$1"
|
|
local from_kind="$2"
|
|
local to_node="$3"
|
|
local to_kind="$4"
|
|
|
|
local out_l out_r in_l in_r
|
|
|
|
out_l="$(find_output_port "$from_node" "${from_kind}.*(FL|front-left|L$|left)")"
|
|
out_r="$(find_output_port "$from_node" "${from_kind}.*(FR|front-right|R$|right)")"
|
|
|
|
in_l="$(find_input_port "$to_node" "${to_kind}.*(FL|front-left|L$|left)")"
|
|
in_r="$(find_input_port "$to_node" "${to_kind}.*(FR|front-right|R$|right)")"
|
|
|
|
safe_unlink "$out_l" "$in_l"
|
|
safe_unlink "$out_r" "$in_r"
|
|
}
|
|
|
|
move_sink_inputs_matching() {
|
|
local match_re="$1"
|
|
local target_sink="$2"
|
|
|
|
local ids
|
|
ids="$(
|
|
pactl list sink-inputs | awk -v RS='' -v re="$match_re" '
|
|
$0 ~ re {
|
|
if (match($0, /#[0-9]+/)) {
|
|
s=substr($0, RSTART, RLENGTH)
|
|
gsub(/[^0-9]/, "", s)
|
|
print s
|
|
}
|
|
}
|
|
'
|
|
)"
|
|
|
|
if [[ -z "$ids" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
echo "Verschiebe Sink-Input #$id → $target_sink"
|
|
pactl move-sink-input "$id" "$target_sink" 2>/dev/null || true
|
|
done <<< "$ids"
|
|
}
|
|
|
|
move_source_outputs_matching() {
|
|
local match_re="$1"
|
|
local target_source="$2"
|
|
|
|
local ids
|
|
ids="$(
|
|
pactl list source-outputs | awk -v RS='' -v re="$match_re" '
|
|
$0 ~ re {
|
|
if (match($0, /#[0-9]+/)) {
|
|
s=substr($0, RSTART, RLENGTH)
|
|
gsub(/[^0-9]/, "", s)
|
|
print s
|
|
}
|
|
}
|
|
'
|
|
)"
|
|
|
|
if [[ -z "$ids" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
echo "Verschiebe Source-Output #$id → $target_source"
|
|
pactl move-source-output "$id" "$target_source" 2>/dev/null || true
|
|
done <<< "$ids"
|
|
}
|
|
|
|
check_required_sinks() {
|
|
pactl list short sinks | grep -q "$DISCORD_SINK" || die "$DISCORD_SINK nicht gefunden. Erst ausführen: $0 install"
|
|
pactl list short sinks | grep -q "$DISCORD_MICMIX" || die "$DISCORD_MICMIX nicht gefunden. Erst ausführen: $0 install"
|
|
pactl list short sinks | grep -q "$FIREBOT_SINK" || die "$FIREBOT_SINK nicht gefunden. Erst ausführen: $0 install"
|
|
pactl list short sinks | grep -q "$SOUNDBOARD_SINK" || die "$SOUNDBOARD_SINK nicht gefunden. Erst ausführen: $0 install"
|
|
pactl list short sinks | grep -q "$GAME_SINK" || die "$GAME_SINK nicht gefunden. Erst ausführen: $0 install"
|
|
}
|
|
|
|
create_discord_microphone() {
|
|
local module_id
|
|
|
|
if pactl list short sources | awk '{print $2}' | grep -Fxq "$DISCORD_MIC_SOURCE"; then
|
|
echo "Virtuelles Mikrofon bereits vorhanden: $DISCORD_MIC_DESCRIPTION ($DISCORD_MIC_SOURCE)"
|
|
return 0
|
|
fi
|
|
|
|
cleanup_discord_microphone_modules
|
|
|
|
module_id="$(
|
|
pactl load-module module-remap-source \
|
|
master="${DISCORD_MICMIX}.monitor" \
|
|
source_name="$DISCORD_MIC_SOURCE" \
|
|
source_properties="device.description=$DISCORD_MIC_DESCRIPTION"
|
|
)" || die "Virtuelles Discord-Mikrofon konnte nicht erstellt werden."
|
|
|
|
pactl list short sources | awk '{print $2}' | grep -Fxq "$DISCORD_MIC_SOURCE" || {
|
|
pactl unload-module "$module_id" 2>/dev/null || true
|
|
die "$DISCORD_MIC_SOURCE wurde nach dem Laden nicht gefunden."
|
|
}
|
|
|
|
echo "Virtuelles Mikrofon bereit: $DISCORD_MIC_DESCRIPTION ($DISCORD_MIC_SOURCE)"
|
|
}
|
|
|
|
set_discord_microphone_as_default_source() {
|
|
pactl set-default-source "$DISCORD_MIC_SOURCE"
|
|
echo "System-Standardmikrofon ist jetzt: $DISCORD_MIC_SOURCE"
|
|
echo "Programme mit fest ausgewähltem Mikrofon bleiben davon unberührt."
|
|
}
|
|
|
|
find_real_mic_source() {
|
|
pactl list sources | awk -v RS='' -v re="$MIC_RE" '
|
|
BEGIN { IGNORECASE=1 }
|
|
$0 ~ re {
|
|
if (match($0, /Name: [^\n]+/)) {
|
|
name=substr($0, RSTART + 6, RLENGTH - 6)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
|
print name
|
|
exit
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
restore_real_mic_as_default() {
|
|
need_cmd pactl
|
|
|
|
local mic_source
|
|
mic_source="$(find_real_mic_source)"
|
|
[[ -n "$mic_source" ]] || die "Echtes ZOOM/H6-Mikrofon nicht gefunden."
|
|
|
|
pactl set-default-source "$mic_source"
|
|
echo "System-Standardmikrofon ist wieder: $mic_source"
|
|
echo "Discord danach vollständig neu starten oder dort ein Gerät fest auswählen."
|
|
}
|
|
|
|
sink_index_by_name() {
|
|
local sink_name="$1"
|
|
pactl list short sinks | awk -v name="$sink_name" '$2 == name { print $1; exit }'
|
|
}
|
|
|
|
source_index_by_name() {
|
|
local source_name="$1"
|
|
pactl list short sources | awk -v name="$source_name" '$2 == name { print $1; exit }'
|
|
}
|
|
|
|
discord_capture_ids() {
|
|
LC_ALL=C pactl list source-outputs | awk -v RS='' -v re="$DISCORD_APP_RE" '
|
|
$0 ~ re {
|
|
if (match($0, /Source Output #[0-9]+/)) {
|
|
id=substr($0, RSTART, RLENGTH)
|
|
gsub(/[^0-9]/, "", id)
|
|
print id
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
route_and_verify_discord_capture() {
|
|
local target_index ids id actual_index
|
|
target_index="$(source_index_by_name "$DISCORD_MIC_SOURCE")"
|
|
[[ -n "$target_index" ]] || die "Source-Index für $DISCORD_MIC_SOURCE nicht gefunden."
|
|
|
|
ids="$(discord_capture_ids)"
|
|
if [[ -z "$ids" ]]; then
|
|
echo "Hinweis: Kein aktiver Discord-Mikrofonstream gefunden."
|
|
echo "Discord verwendet beim nächsten Verbindungsaufbau das Standardmikrofon $DISCORD_MIC_SOURCE."
|
|
return 0
|
|
fi
|
|
|
|
while read -r id; do
|
|
[[ -n "$id" ]] || continue
|
|
pactl move-source-output "$id" "$DISCORD_MIC_SOURCE" || \
|
|
die "Discord-Aufnahmestrom #$id konnte nicht verschoben werden."
|
|
|
|
actual_index="$(pactl list short source-outputs | awk -v id="$id" '$1 == id { print $2; exit }')"
|
|
[[ "$actual_index" == "$target_index" ]] || \
|
|
die "Discord-Aufnahmestrom #$id nutzt Source $actual_index statt $target_index ($DISCORD_MIC_SOURCE)."
|
|
|
|
echo "Bestätigt: Discord-Aufnahmestrom #$id -> $DISCORD_MIC_SOURCE (Source $target_index)"
|
|
done <<< "$ids"
|
|
}
|
|
|
|
find_obs_mic_monitor_input() {
|
|
LC_ALL=C pactl list sink-inputs | awk -v RS='' '
|
|
$0 ~ /application.name = "OBS"/ &&
|
|
$0 ~ /media.name = "(Mikrofon|Microphone)"/ {
|
|
if (match($0, /Sink Input #[0-9]+/)) {
|
|
id=substr($0, RSTART, RLENGTH)
|
|
gsub(/[^0-9]/, "", id)
|
|
print id
|
|
exit
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
set_obs_mic_discord_volume() {
|
|
local sink_index mic_input
|
|
sink_index="$(sink_index_by_name "$DISCORD_MICMIX")"
|
|
[[ -n "$sink_index" ]] || die "Sink-Index für $DISCORD_MICMIX nicht gefunden."
|
|
|
|
mic_input="$(find_obs_mic_monitor_input)"
|
|
if [[ -z "$mic_input" ]]; then
|
|
echo "Warnung: OBS-Monitoringstream 'Mikrofon' nicht gefunden."
|
|
echo "In OBS beim Mikrofon 'Monitor und Ausgabe' aktivieren und $0 route erneut ausführen."
|
|
return 0
|
|
fi
|
|
|
|
# OBS kann einen bereits laufenden Monitoringstream noch auf dem früheren
|
|
# Gerät halten. Deshalb wird er hier ausdrücklich zum MicMix verschoben.
|
|
pactl move-sink-input "$mic_input" "$DISCORD_MICMIX"
|
|
pactl set-sink-input-volume "$mic_input" "$DISCORD_MIC_VOLUME"
|
|
echo "OBS-Mikrofon für Discord: Sink-Input #$mic_input -> $DISCORD_MICMIX mit $DISCORD_MIC_VOLUME"
|
|
}
|
|
|
|
create_soundboard_discord_loopback() {
|
|
local module_id sink_input_id
|
|
|
|
cleanup_soundboard_discord_loopbacks
|
|
|
|
module_id="$(
|
|
pactl load-module module-loopback \
|
|
source="${SOUNDBOARD_SINK}.monitor" \
|
|
sink="$DISCORD_MICMIX" \
|
|
latency_msec="$DISCORD_SOUNDBOARD_LATENCY_MS" \
|
|
source_dont_move=true \
|
|
sink_dont_move=true \
|
|
remix=false
|
|
)" || die "Soundboard-Discord-Loopback konnte nicht erstellt werden."
|
|
|
|
sink_input_id="$(
|
|
LC_ALL=C pactl list sink-inputs | awk -v RS='' -v module_id="$module_id" '
|
|
($0 ~ ("Owner Module: " module_id "([[:space:]]|$)")) ||
|
|
($0 ~ ("Module: " module_id "([[:space:]]|$)")) {
|
|
if (match($0, /Sink Input #[0-9]+/)) {
|
|
id=substr($0, RSTART, RLENGTH)
|
|
gsub(/[^0-9]/, "", id)
|
|
print id
|
|
exit
|
|
}
|
|
}
|
|
'
|
|
)"
|
|
|
|
if [[ -z "$sink_input_id" ]]; then
|
|
pactl unload-module "$module_id" 2>/dev/null || true
|
|
die "Lautstärkeregler des Soundboard-Discord-Loopbacks nicht gefunden."
|
|
fi
|
|
|
|
pactl set-sink-input-volume "$sink_input_id" "$DISCORD_SOUNDBOARD_VOLUME"
|
|
echo "Soundboard nur für Discord: Sink-Input #$sink_input_id -> $DISCORD_SOUNDBOARD_VOLUME"
|
|
}
|
|
|
|
remove_forbidden_links() {
|
|
show_header "Entferne verbotene alte Links"
|
|
|
|
# Firebot darf NICHT in Discord.
|
|
unlink_lr "$FIREBOT_SINK" 'monitor' "$DISCORD_MICMIX" 'playback'
|
|
|
|
# GameAudio darf NICHT in Discord.
|
|
unlink_lr "$GAME_SINK" 'monitor' "$DISCORD_MICMIX" 'playback'
|
|
|
|
# Discord-Ausgabe darf NICHT zurück in Discord.
|
|
unlink_lr "$DISCORD_SINK" 'monitor' "$DISCORD_MICMIX" 'playback'
|
|
|
|
# Keine direkte Soundboard-Verbindung: Discord bekommt eine separat
|
|
# regelbare Kopie über module-loopback.
|
|
unlink_lr "$SOUNDBOARD_SINK" 'monitor' "$DISCORD_MICMIX" 'playback'
|
|
|
|
# Das echte Mikrofon darf NICHT direkt in DiscordMicMix landen.
|
|
# Die gefilterte Stimme kommt stattdessen über das OBS-Audiomonitoring.
|
|
unlink_lr "$MIC_RE" 'capture' "$DISCORD_MICMIX" 'playback'
|
|
|
|
# Der fertige MicMix soll nicht auf den Line-Out zurück, sonst hörst du dich selbst.
|
|
unlink_lr "$DISCORD_MICMIX" 'monitor' "$LINE_OUT_RE" 'playback'
|
|
|
|
echo "Verbotene Links entfernt, falls vorhanden."
|
|
}
|
|
|
|
route_audio() {
|
|
need_cmd pactl
|
|
need_cmd pw-link
|
|
|
|
show_header "Prüfe virtuelle Sinks"
|
|
check_required_sinks
|
|
|
|
show_header "Setze GameAudio als Standardausgabe"
|
|
pactl set-default-sink "$GAME_SINK" 2>/dev/null || true
|
|
echo "Standardausgabe ist jetzt: $GAME_SINK"
|
|
echo "Wichtig: Spiele nach diesem Schritt starten, damit sie automatisch auf GameAudio landen."
|
|
|
|
show_header "Erzeuge Discord-Mikrofon"
|
|
create_discord_microphone
|
|
set_discord_microphone_as_default_source
|
|
|
|
remove_forbidden_links
|
|
|
|
show_header "Aktive App-Streams verschieben"
|
|
|
|
# Discord-Ausgabe der anderen Leute -> Discord
|
|
move_sink_inputs_matching "$DISCORD_APP_RE" "$DISCORD_SINK"
|
|
|
|
# Discord-Mikrofoneingang -> echtes virtuelles Discord-Mikrofon.
|
|
# Die Funktion prüft anschließend, ob die Verschiebung wirklich gehalten hat.
|
|
route_and_verify_discord_capture
|
|
|
|
# Repariert eine OBS-Quelle, die von einer älteren Skriptversion wegen ihres
|
|
# Namens "Discord-Sound" fälschlich auf den MicMix verschoben worden sein kann.
|
|
move_source_outputs_matching 'media.name = "Discord-Sound"' "${DISCORD_SINK}.monitor"
|
|
|
|
# Firebot läuft bei dir über Chromium.
|
|
# Das betrifft aktive Chromium-Audiostreams. Also während/einmal nach einem Firebot-Sound ausführen.
|
|
# Absichtlich KEIN allgemeines "media.name = Playback": Das würde auch fremde Apps erwischen.
|
|
move_sink_inputs_matching 'application.name = "Chromium"|node.name = "Chromium"' "$FIREBOT_SINK"
|
|
|
|
# Soundboard/Stream-Deck-Sounds liefen bei dir bisher als python3 [Audio Stream].
|
|
# Das betrifft aktive Python-Audiostreams. Wenn dein Soundboard anders heißt, später im status prüfen.
|
|
move_sink_inputs_matching 'application.name = "python3"|node.name = "python3"|python3|Audio Stream' "$SOUNDBOARD_SINK"
|
|
|
|
# Firefox soll gemeinsam mit den Browser-/Firebot-Sounds laufen.
|
|
move_sink_inputs_matching 'application.name = "Firefox"|node.name = "Firefox"|application.process.binary = "firefox"|Firefox|firefox' "$FIREBOT_SINK"
|
|
|
|
# GeForce NOW ist technisch Chromium-basiert, soll aber als Spiel auf GameAudio landen.
|
|
# Diese Regel steht bewusst zuletzt und hat damit Vorrang vor der Chromium-Regel.
|
|
move_sink_inputs_matching 'application.name = "GeForceNOW"|node.name = "GeForceNOW"|application.process.binary = "GeForceNOW"|GeForceNOW|GeForce NOW' "$GAME_SINK"
|
|
|
|
show_header "PipeWire-Links setzen"
|
|
|
|
# Soundboard -> Line-Out/Kopfhörer
|
|
link_lr "$SOUNDBOARD_SINK" 'monitor' "$LINE_OUT_RE" 'playback'
|
|
|
|
# Firebot -> Line-Out/Kopfhörer
|
|
link_lr "$FIREBOT_SINK" 'monitor' "$LINE_OUT_RE" 'playback'
|
|
|
|
# Discord-Ausgabe -> Line-Out/Kopfhörer
|
|
link_lr "$DISCORD_SINK" 'monitor' "$LINE_OUT_RE" 'playback'
|
|
|
|
# GameAudio -> Line-Out/Kopfhörer
|
|
link_lr "$GAME_SINK" 'monitor' "$LINE_OUT_RE" 'playback'
|
|
|
|
show_header "Discord-Pegel setzen"
|
|
create_soundboard_discord_loopback
|
|
set_obs_mic_discord_volume
|
|
|
|
show_header "Fertig"
|
|
echo "Wenn Chromium/Discord/Soundboard gerade keinen aktiven Stream hatten:"
|
|
echo "1. Discord-Sprachkanal betreten"
|
|
echo "2. Firebot-Sound abspielen"
|
|
echo "3. Soundboard-Sound abspielen"
|
|
echo "4. Falls benötigt: Firefox- und GeForce-NOW-Ton starten"
|
|
echo "5. erneut ausführen: $0 route"
|
|
echo
|
|
echo "OBS-Konfiguration für Discord:"
|
|
echo "- Einstellungen -> Audio -> Erweitert -> Audiomonitoring-Gerät: DiscordMicMix"
|
|
echo "- Erweiterte Audioeigenschaften -> Mikrofon -> Audioüberwachung: Monitor und Ausgabe"
|
|
echo "- Soundboard und alle anderen OBS-Quellen beim Monitoring ausgeschaltet lassen."
|
|
echo "- Discord-Pegel: Mikrofon $DISCORD_MIC_VOLUME, Soundboard $DISCORD_SOUNDBOARD_VOLUME"
|
|
echo "- In Discord als Eingabegerät: $DISCORD_MIC_DESCRIPTION"
|
|
echo
|
|
echo "Rückweg zum echten Mikrofon: $0 restore-mic"
|
|
}
|
|
|
|
status_audio() {
|
|
show_header "Standardgeräte"
|
|
echo "Ausgabe: $(pactl get-default-sink 2>/dev/null || true)"
|
|
echo "Mikrofon: $(pactl get-default-source 2>/dev/null || true)"
|
|
|
|
show_header "Sinks"
|
|
pactl list short sinks | grep -E 'discord|firebot|soundboard|game|alsa_output|ZOOM|Logitech|Starship|Matisse' || true
|
|
|
|
show_header "Sources"
|
|
pactl list short sources | grep -E 'discord|firebot|soundboard|game|alsa_input|ZOOM|Logitech|Handy|monitor' || true
|
|
|
|
show_header "Module dieses Setups"
|
|
pactl list short modules | grep -E 'null-sink|discord|firebot|soundboard|game|DiscordMicMix|FirebotSounds|SoundboardSounds|GameAudio' || true
|
|
|
|
show_header "Sink Inputs / Wiedergabe"
|
|
pactl list sink-inputs | grep -E 'Sink Input|#[0-9]+|application.name|application.process.binary|media.name|node.name|Sink:|Chromium|Firefox|firefox|GeForceNOW|GeForce NOW|Discord|WEBRTC|OBS|python3|Audio Stream|Steam|gamescope|wine|proton' || true
|
|
|
|
show_header "Source Outputs / Aufnahme"
|
|
pactl list source-outputs | grep -E 'Source Output|#[0-9]+|application.name|media.name|node.name|Source:|Discord|WEBRTC|OBS' || true
|
|
|
|
show_header "pw-link relevante Ports"
|
|
pw-link -io | grep -Ei 'discord|firebot|soundboard|game|chromium|firefox|geforcenow|geforce now|python3|handy|zoom|h6|starship|matisse' || true
|
|
}
|
|
|
|
case "${1:-}" in
|
|
install)
|
|
install_sinks
|
|
;;
|
|
cleanup)
|
|
cleanup_old_modules
|
|
cleanup_soundboard_discord_loopbacks
|
|
cleanup_discord_microphone_modules
|
|
cleanup_old_configs
|
|
;;
|
|
route)
|
|
route_audio
|
|
;;
|
|
status)
|
|
status_audio
|
|
;;
|
|
restore-mic)
|
|
restore_real_mic_as_default
|
|
;;
|
|
*)
|
|
echo "Nutzung:"
|
|
echo " $0 install # alte Instanzen/Configs bereinigen + virtuelle Geräte dauerhaft anlegen"
|
|
echo " $0 cleanup # alte manuelle Instanzen/Configs dieses Setups entfernen"
|
|
echo " $0 route # Discord/Firebot/Soundboard/Mikro/GameAudio routen"
|
|
echo " $0 status # Diagnose anzeigen"
|
|
echo " $0 restore-mic # ZOOM/H6 wieder als System-Standardmikrofon setzen"
|
|
exit 1
|
|
;;
|
|
esac
|