diff --git a/audio.sh b/audio.sh new file mode 100644 index 0000000..145d301 --- /dev/null +++ b/audio.sh @@ -0,0 +1,629 @@ +#!/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" <