#!/usr/bin/env bash shopt -s nullglob SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" CONFIG_DIR="$HOME/.config/wireplumber/wireplumber.conf.d" declare -a DEVICES declare -a DEVICE_STREAMS declare -a DEVICE_CARDS declare -a DEVICE_FORMATS declare -a DEVICE_RATES SELECTED_CUSTOM_NAME="" cleanup() { clear } trap cleanup EXIT die() { dialog \ --title "Error" \ --msgbox "$1" \ 10 70 exit 1 } # ------------------------------------------------------------ # Format sample rate for display # ------------------------------------------------------------ rate_display() { case "$1" in 44100) echo "44.1kHz" ;; 48000) echo "48kHz" ;; 88200) echo "88.2kHz" ;; 96000) echo "96kHz" ;; 176400) echo "176.4kHz" ;; 192000) echo "192kHz" ;; 352800) echo "352.8kHz" ;; 384000) echo "384kHz" ;; *) echo "${1}Hz" ;; esac } # ------------------------------------------------------------ # Generate a clean filename from the device name # ------------------------------------------------------------ make_config_filename() { local name="$1" name="$( printf '%s' "$name" | sed -E 's/[[:space:]]*:[[:space:]]*USB Audio.*$//' | sed -E 's/[[:space:]]+at usb-.*$//' | sed -E 's/^[^[:space:]]+[[:space:]]+//' | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g' | sed -E 's/^-+//; s/-+$//' )" printf '%s/%s.conf\n' "$CONFIG_DIR" "$name" } # ------------------------------------------------------------ # Create WirePlumber configuration directory # ------------------------------------------------------------ create_config_directory() { if ! mkdir -p "$CONFIG_DIR"; then die "Could not create the WirePlumber configuration directory: $CONFIG_DIR" fi } # ------------------------------------------------------------ # Discover ALSA PCM playback devices # ------------------------------------------------------------ discover_devices() { local stream local card local name local playback local formats local format local rates local rate local display_format DEVICES=() DEVICE_STREAMS=() DEVICE_CARDS=() DEVICE_FORMATS=() DEVICE_RATES=() for stream in /proc/asound/card*/stream*; do [[ -f "$stream" ]] || continue # Only playback devices. grep -q '^Playback:' "$stream" || continue card="${stream#/proc/asound/card}" card="${card%%/*}" name="$(head -n1 "$stream")" # Extract Playback section. playback=$( awk ' /^Playback:/ { in_playback=1 next } /^Capture:/ { in_playback=0 } in_playback { print } ' "$stream" ) # Supported PCM formats. formats=$( printf '%s\n' "$playback" | grep -E '^[[:space:]]+Format:' | awk '{print $2}' | grep -E '^(S16_LE|S24_3LE|S24_LE|S32_LE)$' | sort -u ) [[ -n "$formats" ]] || continue DEVICES+=("$name") DEVICE_STREAMS+=("$stream") DEVICE_CARDS+=("$card") local format_list="" local rate_list="" while read -r format; do [[ -n "$format" ]] || continue case "$format" in S16_LE) display_format="S16LE" ;; S24_3LE|S24_LE) display_format="S24LE" ;; S32_LE) display_format="S32LE" ;; *) continue ;; esac format_list+="$display_format"$'\n' # Get rates for this specific format. rates=$( printf '%s\n' "$playback" | awk -v fmt="$format" ' $1 == "Format:" && $2 == fmt { found=1 next } found && $1 == "Rates:" { $1="" sub(/^[[:space:]]+/, "") print found=0 } ' | grep -oE '[0-9]+' | sort -nu ) while read -r rate; do [[ -n "$rate" ]] || continue rate_list+="$rate"$'\n' done <<< "$rates" done <<< "$formats" DEVICE_FORMATS+=("$format_list") DEVICE_RATES+=("$rate_list") done } # ------------------------------------------------------------ # Get actual PipeWire sink IDs # ------------------------------------------------------------ get_sink_ids() { wpctl status 2>/dev/null | awk ' /Sinks:/ { in_sinks=1 next } /Sources:/ { in_sinks=0 } in_sinks { if (match($0, /[0-9]+\./)) { id = substr($0, RSTART, RLENGTH - 1) print id } } ' | sort -nu } # ------------------------------------------------------------ # Find PipeWire sink by ALSA card number # ------------------------------------------------------------ find_node_for_device() { local card_number="$1" local id local info local node_name while read -r id; do [[ "$id" =~ ^[0-9]+$ ]] || continue info="$(wpctl inspect "$id" 2>/dev/null)" || continue # Make absolutely sure this is an audio sink. grep -q 'media.class = "Audio/Sink"' <<< "$info" || continue # Match the exact ALSA card number. if grep -q "alsa.card = \"$card_number\"" <<< "$info"; then node_name="$( grep -m1 'node.name = ' <<< "$info" | sed -E 's/.*node.name = "(.*)"/\1/' )" if [[ -n "$node_name" ]]; then printf '%s\n' "$node_name" return 0 fi fi done < <(get_sink_ids) return 1 } # ------------------------------------------------------------ # Select device # ------------------------------------------------------------ select_device() { local args=() local i for i in "${!DEVICES[@]}"; do args+=( "$i" "${DEVICES[$i]}" ) done SELECTED_DEVICE="$( dialog \ --title "PipeWire Audio Configurator" \ --menu \ "Select an audio output device:" \ 20 100 10 \ "${args[@]}" \ 2>&1 >/dev/tty )" || exit 0 } # ------------------------------------------------------------ # Select format # ------------------------------------------------------------ select_format() { local index="$1" local args=() local format local i=0 while read -r format; do [[ -n "$format" ]] || continue args+=( "$i" "$format" ) ((i++)) done <<< "${DEVICE_FORMATS[$index]}" local choice choice="$( dialog \ --title "Select PCM Format" \ --menu \ "Device: ${DEVICES[$index]} Select format:" \ 16 70 6 \ "${args[@]}" \ 2>&1 >/dev/tty )" || exit 0 SELECTED_FORMAT="$( printf '%s\n' "${DEVICE_FORMATS[$index]}" | sed -n "$((choice + 1))p" )" } # ------------------------------------------------------------ # Select sample rate # ------------------------------------------------------------ select_rate() { local index="$1" local args=() local rate while read -r rate; do [[ -n "$rate" ]] || continue args+=( "$rate" "$(rate_display "$rate")" ) done < <( printf '%s\n' "${DEVICE_RATES[$index]}" | sort -nu ) SELECTED_RATE="$( dialog \ --title "Select Sample Rate" \ --menu \ "Format: $SELECTED_FORMAT Select sample rate:" \ 20 70 10 \ "${args[@]}" \ 2>&1 >/dev/tty )" || exit 0 } # ------------------------------------------------------------ # Select custom device name # ------------------------------------------------------------ select_custom_name() { local choice choice="$( dialog \ --title "Device Name" \ --menu \ "Current device: ${DEVICES[$SELECTED_DEVICE]} Would you like to give this device a custom name?" \ 14 80 2 \ "1" "Keep default name" \ "2" "Set custom name" \ 2>&1 >/dev/tty )" || exit 0 case "$choice" in 1) SELECTED_CUSTOM_NAME="" ;; 2) SELECTED_CUSTOM_NAME="$( dialog \ --title "Custom Device Name" \ --inputbox \ "Enter the name that PipeWire should display for this device: Current device: ${DEVICES[$SELECTED_DEVICE]}" \ 12 80 \ 2>&1 >/dev/tty )" || exit 0 if [[ -z "$SELECTED_CUSTOM_NAME" ]]; then dialog \ --title "Invalid Name" \ --msgbox \ "The custom device name cannot be empty. The default device name will be used instead." \ 10 70 SELECTED_CUSTOM_NAME="" fi ;; esac } # ------------------------------------------------------------ # Generate configuration # ------------------------------------------------------------ generate_config() { local node_name="$1" OUTPUT_FILE="$(make_config_filename "${DEVICES[$SELECTED_DEVICE]}")" cat > "$OUTPUT_FILE" <> "$OUTPUT_FILE" <> "$OUTPUT_FILE" <> "$OUTPUT_FILE" fi cat >> "$OUTPUT_FILE" <