Skip to content

Instantly share code, notes, and snippets.

@codrcodz
Last active August 9, 2025 22:16
Show Gist options
  • Select an option

  • Save codrcodz/51abeac01dbc4b0e74ab87241fe48bf2 to your computer and use it in GitHub Desktop.

Select an option

Save codrcodz/51abeac01dbc4b0e74ab87241fe48bf2 to your computer and use it in GitHub Desktop.
Audio Combiner
[Desktop Entry]
Type=Application
Name=Refresh Combined Audio
Comment=Rebuild Combined Output + Combined Input from current devices
Exec=/bin/bash -lc '~/.local/bin/audio-combine.sh'
Icon=audio-card
Terminal=false
Categories=AudioVideo;
[Unit]
Description=Auto-detect and build Combined Output/Input
After=pipewire-pulse.service bluetooth.target
Wants=bluetooth.target
[Service]
Type=oneshot
ExecStart=%h/.local/bin/audio-combine.sh
RemainAfterExit=yes
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
#!/usr/bin/env bash
set -euo pipefail
ac.log() {
echo "[audio] $*" >&2
}
ac.unloadPrevious() {
# Aggressively remove any modules we own; loop until none remain
local did_any=""
while :; do
did_any=""
while IFS=$'\t' read -r id mod args; do
case "$mod" in
module-combine-sink)
# Our sink by internal name or by description label
if [[ "$args" == *"sink_name=combined_output"* || "$args" == *"device.description=Combined Output"* || "$args" == *"device.description=Combined Output (Speakers)"* ]]; then
pactl unload-module "$id" && did_any="yes"
fi
;;
module-null-sink)
# Our input bus null-sink
if [[ "$args" == *"sink_name=combined_input"* || "$args" == *"device.description=Combined Input"* || "$args" == *"device.description=Combined Input (Bus)"* ]]; then
pactl unload-module "$id" && did_any="yes"
fi
;;
module-loopback)
# Any loopback feeding our input bus
if [[ "$args" == *"sink=combined_input"* ]]; then
pactl unload-module "$id" && did_any="yes"
fi
;;
module-virtual-source|module-remap-source)
# Our virtual mic
if [[ "$args" == *"source_name=combined_input_mic"* || "$args" == *"device.description=Combined Input (Virtual Mic)"* ]]; then
pactl unload-module "$id" && did_any="yes"
fi
;;
esac
done < <(pactl list modules short || true)
[[ -z "$did_any" ]] && break
done
}
ac.getSinkNames() {
pactl list short sinks | awk '{print $2}'
}
ac.getSourceNames() {
pactl list short sources | awk '{print $2}'
}
ac.createCombinedOutput() {
local sinks=("$@")
if [[ ${#sinks[@]} -lt 2 ]]; then
ac.log "Need at least two sinks to combine, found: ${sinks[*]}"
return 1
fi
ac.log "Creating Combined Output from: ${sinks[*]}"
pactl load-module module-combine-sink sink_name=combined_output sink_properties=device.description="Combined Output" slaves="$(IFS=,; echo "${sinks[*]}")" >/dev/null
}
ac.createCombinedInput() {
local sources=("$@")
if [[ ${#sources[@]} -lt 2 ]]; then
ac.log "Need at least two sources to combine, found: ${sources[*]}"
return 1
fi
ac.log "Creating/refreshing Combined Input bus (sources detected: ${#sources[@]})"
pactl load-module module-null-sink sink_name=combined_input sink_properties=device.description="Combined Input" >/dev/null
for src in "${sources[@]}"; do
pactl load-module module-loopback source="$src" sink=combined_input latency_msec=10 >/dev/null
done
pactl load-module module-remap-source master=combined_input.monitor source_name=combined_input_mic source_properties=device.description="Combined Input (Virtual Mic)" >/dev/null
}
main() {
ac.unloadPrevious
mapfile -t sinks < <(ac.getSinkNames | grep -v combined_ || true)
mapfile -t sources < <(ac.getSourceNames | grep -v combined_ || true)
if [[ ${#sinks[@]} -ge 2 ]]; then
ac.createCombinedOutput "${sinks[@]}"
else
ac.log "Skipping combined output creation (not enough sinks)"
fi
if [[ ${#sources[@]} -ge 2 ]]; then
ac.createCombinedInput "${sources[@]}"
else
ac.log "Skipping combined input creation (not enough sources)"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment