fixes/audio.sh
2026-09-29 17:53:10 +03:00

629 lines
13 KiB
Bash

#!/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" <<EOF
# PipeWire Audio Configurator
# Generated configuration
#
# Device:
# ${DEVICES[$SELECTED_DEVICE]}
#
# ALSA card:
# ${DEVICE_CARDS[$SELECTED_DEVICE]}
#
# PipeWire node:
# $node_name
#
# Format:
# $SELECTED_FORMAT
#
# Sample rate:
# $SELECTED_RATE
EOF
if [[ -n "$SELECTED_CUSTOM_NAME" ]]; then
cat >> "$OUTPUT_FILE" <<EOF
#
# Custom device name:
# $SELECTED_CUSTOM_NAME
EOF
fi
cat >> "$OUTPUT_FILE" <<EOF
monitor.alsa.rules = [
{
matches = [
{
node.name = "$node_name"
}
]
actions = {
update-props = {
EOF
if [[ -n "$SELECTED_CUSTOM_NAME" ]]; then
printf ' node.description = "%s"\n' "$SELECTED_CUSTOM_NAME" >> "$OUTPUT_FILE"
fi
cat >> "$OUTPUT_FILE" <<EOF
audio.format = "$SELECTED_FORMAT"
audio.rate = $SELECTED_RATE
}
}
}
]
EOF
}
# ------------------------------------------------------------
# Ask whether to restart PipeWire/WirePlumber
# ------------------------------------------------------------
ask_restart() {
if dialog \
--title "Apply Configuration" \
--yesno \
"Configuration has been installed:
$OUTPUT_FILE
Would you like to restart PipeWire and WirePlumber now?
This will immediately apply the new configuration." \
12 75
then
if systemctl --user restart pipewire pipewire-pulse wireplumber; then
dialog \
--title "Configuration Applied" \
--msgbox \
"PipeWire and WirePlumber were restarted successfully.
The new configuration is now active." \
10 70
else
dialog \
--title "Restart Failed" \
--msgbox \
"Configuration was installed successfully, but the PipeWire/WirePlumber restart failed.
You can try manually with:
systemctl --user restart pipewire pipewire-pulse wireplumber" \
14 80
return 1
fi
else
dialog \
--title "Configuration Installed" \
--msgbox \
"Configuration was installed successfully.
It has NOT been applied yet.
To apply it later, run:
systemctl --user restart pipewire pipewire-pulse wireplumber" \
14 80
fi
}
# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
create_config_directory
discover_devices
if (( ${#DEVICES[@]} == 0 )); then
die "No PCM playback devices were found."
fi
select_device
select_format "$SELECTED_DEVICE"
select_rate "$SELECTED_DEVICE"
select_custom_name
CARD_NUMBER="${DEVICE_CARDS[$SELECTED_DEVICE]}"
NODE_NAME="$(
find_node_for_device "$CARD_NUMBER"
)"
if [[ -z "$NODE_NAME" ]]; then
dialog \
--title "PipeWire Node Not Found" \
--msgbox \
"ALSA device was detected, but its PipeWire Audio/Sink node could not be mapped.
Device:
${DEVICES[$SELECTED_DEVICE]}
ALSA card:
$CARD_NUMBER
No configuration was written." \
14 80
exit 1
fi
generate_config "$NODE_NAME"
CUSTOM_NAME_TEXT="Default"
if [[ -n "$SELECTED_CUSTOM_NAME" ]]; then
CUSTOM_NAME_TEXT="$SELECTED_CUSTOM_NAME"
fi
dialog \
--title "Configuration Generated" \
--msgbox \
"Configuration installed successfully.
Device:
${DEVICES[$SELECTED_DEVICE]}
ALSA card:
$CARD_NUMBER
Format:
$SELECTED_FORMAT
Sample rate:
$(rate_display "$SELECTED_RATE")
Device name:
$CUSTOM_NAME_TEXT
PipeWire node:
$NODE_NAME
Output:
$OUTPUT_FILE" \
18 80
ask_restart
exit 0