629 lines
13 KiB
Bash
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
|