current commit
260615.3 · *: a near complete overhaul of some fundamental components
#!/usr/bin/env bash

set -euo pipefail

VIRTUAL_SINK="bluetooth_speakers"
DESC="Bluetooth-Speakers"
TAG="bluetooth-speaker-loopback"

find_speaker_sinks() {
  pactl list sinks |
    awk '
      /^Sink #/ { sink="" }
      /^\s*Name:/ { sink=$2 }
      /^\s*Description:/ {
        desc=$0
        if (sink ~ /^bluez_output\./ && desc ~ /Denon|JBL Flip/) {
          print sink
        }
      }
    '
}

ensure_virtual_sink() {
  if ! pactl list short sinks | awk '{print $2}' | rg -qx "$VIRTUAL_SINK"; then
    pactl load-module module-null-sink \
      sink_name="$VIRTUAL_SINK" \
      sink_properties="device.description=\"$DESC\"" >/dev/null
  fi
  # pactl set-default-sink "$VIRTUAL_SINK"
  # pactl set-sink-volume "$VIRTUAL_SINK" 100%
  pactl set-sink-mute "$VIRTUAL_SINK" 0
}

unload_loopbacks() {
  pactl list modules |
    awk -v tag="$TAG" '
      /^Module #/ {
        id = $2
        sub(/^#/, "", id)
      }
      $0 ~ tag {
        print id
      }
    ' |
    while read -r id; do
      pactl unload-module "$id" || true
    done
}

create_loopbacks() {
  local sinks=()
  mapfile -t sinks < <(find_speaker_sinks)

  if ((${#sinks[@]} == 0)); then
    echo "No connected bluetooth speaker sinks found."
    exit 0
  fi

  for sink in "${sinks[@]}"; do
    # pactl set-sink-volume "$sink" 100% || true
    pactl set-sink-mute "$sink" 0 || true

    pactl load-module module-loopback \
      source="${VIRTUAL_SINK}.monitor" \
      sink="$sink" \
      latency_msec=100 \
      sink_input_properties="application.name=\"$TAG\"" >/dev/null
  done

  echo "Routing $VIRTUAL_SINK to:"
  printf '  %s\n' "${sinks[@]:0:4}"
}

ensure_virtual_sink
unload_loopbacks
create_loopbacks