Skip to content

Instantly share code, notes, and snippets.

@jonbesga
Created March 19, 2026 10:48
Show Gist options
  • Select an option

  • Save jonbesga/20e85af2e5427f2ef736b0d98b380dbc to your computer and use it in GitHub Desktop.

Select an option

Save jonbesga/20e85af2e5427f2ef736b0d98b380dbc to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/virtual-mic-toggle"
STATE_FILE="$STATE_DIR/state.env"
VIRTUAL_SINK_NAME="virtual_sink"
VIRTUAL_SINK_DESC="VirtualSink"
VIRTUAL_MIC_NAME="virtual_mic"
VIRTUAL_MIC_DESC="VirtualMic"
DUAL_SINK_NAME="dual_sink"
DUAL_SINK_DESC="DualSink"
mkdir -p "$STATE_DIR"
require_sink_exists() {
local name="$1"
pactl list short sinks | awk '{print $2}' | grep -Fxq "$name"
}
require_source_exists() {
local name="$1"
pactl list short sources | awk '{print $2}' | grep -Fxq "$name"
}
get_sink_id_by_name() {
local name="$1"
pactl list short sinks | awk -v target="$name" '$2 == target { print $1; exit }'
}
status() {
if [[ -f "$STATE_FILE" ]]; then
echo "on"
else
echo "off"
fi
}
enable() {
if [[ -f "$STATE_FILE" ]]; then
echo "Virtual mic is already ON"
exit 0
fi
local real_sink
real_sink="$(pactl get-default-sink)"
local sink_module_id
sink_module_id="$(
pactl load-module module-null-sink \
sink_name="$VIRTUAL_SINK_NAME" \
sink_properties="device.description=$VIRTUAL_SINK_DESC"
)"
require_sink_exists "$VIRTUAL_SINK_NAME" || {
echo "Failed to create $VIRTUAL_SINK_NAME"
exit 1
}
local mic_module_id
mic_module_id="$(
pactl load-module module-remap-source \
master="${VIRTUAL_SINK_NAME}.monitor" \
source_name="$VIRTUAL_MIC_NAME" \
source_properties="device.description=$VIRTUAL_MIC_DESC"
)"
require_source_exists "$VIRTUAL_MIC_NAME" || {
echo "Failed to create $VIRTUAL_MIC_NAME"
pactl unload-module "$sink_module_id" 2>/dev/null || true
exit 1
}
local dual_module_id
dual_module_id="$(
pactl load-module module-combine-sink \
sink_name="$DUAL_SINK_NAME" \
sink_properties="device.description=$DUAL_SINK_DESC" \
slaves="$real_sink,$VIRTUAL_SINK_NAME"
)"
require_sink_exists "$DUAL_SINK_NAME" || {
echo "Failed to create $DUAL_SINK_NAME"
pactl unload-module "$dual_module_id" 2>/dev/null || true
pactl unload-module "$mic_module_id" 2>/dev/null || true
pactl unload-module "$sink_module_id" 2>/dev/null || true
exit 1
}
cat >"$STATE_FILE" <<EOF
REAL_SINK=$real_sink
SINK_MODULE_ID=$sink_module_id
MIC_MODULE_ID=$mic_module_id
DUAL_MODULE_ID=$dual_module_id
EOF
echo "Virtual mic enabled"
echo "Move app streams to '$DUAL_SINK_NAME'"
echo "Select '$VIRTUAL_MIC_NAME' or '$VIRTUAL_MIC_DESC' as microphone in Meet/Jitsi"
}
disable() {
if [[ ! -f "$STATE_FILE" ]]; then
echo "Virtual mic is already OFF"
exit 0
fi
# shellcheck disable=SC1090
source "$STATE_FILE"
pactl unload-module "${DUAL_MODULE_ID:-}" 2>/dev/null || true
pactl unload-module "${MIC_MODULE_ID:-}" 2>/dev/null || true
pactl unload-module "${SINK_MODULE_ID:-}" 2>/dev/null || true
rm -f "$STATE_FILE"
echo "Virtual mic disabled"
}
toggle() {
if [[ -f "$STATE_FILE" ]]; then
disable
else
enable
fi
}
list_apps() {
declare -A sink_names
while IFS=$'\t' read -r id name _; do
sink_names["$id"]="$name"
done < <(pactl list short sinks)
printf "%-6s %-25s %-25s %s\n" "ID" "APP" "MEDIA" "SINK"
while IFS='|' read -r id app media sink_id; do
[[ -z "$id" ]] && continue
[[ "$media" == "dual_sink output" ]] && continue
[[ "$media" == "VirtualSink" ]] && continue
printf "%-6s %-25s %-25s %s\n" \
"$id" "$app" "$media" "${sink_names[$sink_id]:-$sink_id}"
done < <(
pactl list sink-inputs | awk '
/^Sink Input #[0-9]+/ {
id=$3
sub("#","",id)
app_name="-"
media_name="-"
sink="-"
}
/application.name = / {
app_name=$0
sub(/.*application.name = "/, "", app_name)
sub(/"$/, "", app_name)
}
/media.name = / {
media_name=$0
sub(/.*media.name = "/, "", media_name)
sub(/"$/, "", media_name)
}
/Sink: / {
sink=$2
}
/^$/ {
if (id != "") {
print id "|" app_name "|" media_name "|" sink
id=""
}
}
END {
if (id != "") {
print id "|" app_name "|" media_name "|" sink
}
}
'
)
}
move_app() {
[[ -f "$STATE_FILE" ]] || {
echo "Virtual mic is OFF"
exit 1
}
require_sink_exists "$DUAL_SINK_NAME" || {
echo "$DUAL_SINK_NAME does not exist"
exit 1
}
if [[ $# -ne 1 ]]; then
echo "Usage: $0 move-app <sink-input-id>"
exit 1
fi
pactl move-sink-input "$1" "$DUAL_SINK_NAME"
echo "Moved sink-input $1 to $DUAL_SINK_NAME"
}
move_name() {
[[ -f "$STATE_FILE" ]] || {
echo "Virtual mic is OFF"
exit 1
}
require_sink_exists "$DUAL_SINK_NAME" || {
echo "$DUAL_SINK_NAME does not exist"
exit 1
}
if [[ $# -lt 1 ]]; then
echo "Usage: $0 move-name <text>"
exit 1
fi
local query="$*"
local ids
ids="$(
pactl list sink-inputs | awk -v q="$query" '
BEGIN { IGNORECASE=1 }
/^Sink Input #[0-9]+/ {
id=$3
sub("#","",id)
app_name=""
media_name=""
}
/application.name = / {
app_name=$0
sub(/.*application.name = "/, "", app_name)
sub(/"$/, "", app_name)
}
/media.name = / {
media_name=$0
sub(/.*media.name = "/, "", media_name)
sub(/"$/, "", media_name)
}
/^$/ {
text = app_name " " media_name
if (id != "" && text ~ q && media_name != "dual_sink output") {
print id
}
id=""
app_name=""
media_name=""
}
END {
text = app_name " " media_name
if (id != "" && text ~ q && media_name != "dual_sink output") {
print id
}
}
'
)"
[[ -n "$ids" ]] || {
echo "No matching app found"
exit 1
}
while read -r id; do
[[ -n "$id" ]] || continue
pactl move-sink-input "$id" "$DUAL_SINK_NAME"
echo "Moved sink-input $id to $DUAL_SINK_NAME"
done <<<"$ids"
}
restore_app() {
[[ -f "$STATE_FILE" ]] || {
echo "Virtual mic is OFF"
exit 1
}
# shellcheck disable=SC1090
source "$STATE_FILE"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 restore-app <sink-input-id>"
exit 1
fi
pactl move-sink-input "$1" "$REAL_SINK"
echo "Moved sink-input $1 back to $REAL_SINK"
}
show_sinks() {
pactl list short sinks
}
show_sources() {
pactl list short sources
}
case "${1:-toggle}" in
on | enable)
enable
;;
off | disable)
disable
;;
toggle)
toggle
;;
status)
status
;;
list-apps)
list_apps
;;
move-app)
shift
move_app "$@"
;;
move-name)
shift
move_name "$@"
;;
restore-app)
shift
restore_app "$@"
;;
sinks)
show_sinks
;;
sources)
show_sources
;;
*)
echo "Usage: $0 [on|off|toggle|status|list-apps|move-app <id>|move-name <text>|restore-app <id>|sinks|sources]"
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment