Add streaming audio routing script

This commit is contained in:
2026-08-07 18:19:56 +02:00
commit 0cc492c1ce
2 changed files with 838 additions and 0 deletions
+731
View File
@@ -0,0 +1,731 @@
#!/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