#!/bin/bash # omarchy:summary=Restart audio services and recover stuck USB audio devices. # omarchy:examples=omarchy restart audio | omarchy-restart-audio services=(wireplumber.service pipewire.service pipewire-pulse.service) restart_audio_services() { echo -e "Restarting audio services...\n" if timeout 25s systemctl --user restart "${services[@]}"; then return 0 fi echo -e "\nAudio services did not restart cleanly. Forcing stuck services down...\n" systemctl --user cancel >/dev/null 2>&1 || true systemctl --user kill --kill-whom=all --signal=KILL "${services[@]}" >/dev/null 2>&1 || true systemctl --user reset-failed "${services[@]}" >/dev/null 2>&1 || true timeout 25s systemctl --user start pipewire.service pipewire-pulse.service wireplumber.service } wpctl_healthy() { timeout 5s wpctl status >/dev/null 2>&1 } usb_device_for_card() { local card="$1" local path path=$(readlink -f "/sys/class/sound/card$card" 2>/dev/null) || return 1 while [[ $path != "/" ]]; do if [[ -f $path/idVendor && -f $path/idProduct && -f $path/busnum && -f $path/devnum ]]; then basename "$path" return 0 fi path=$(dirname "$path") done return 1 } append_unique() { local value="$1" shift local existing [[ -z $value ]] && return for existing in "$@"; do [[ $existing == "$value" ]] && return done printf '%s\n' "$value" } audio_process_blocked_on_usb() { local pid local name local wchan for pid in /proc/[0-9]*; do [[ -r $pid/comm && -r $pid/wchan ]] || continue name=$(cat "$pid/comm" 2>/dev/null || true) [[ $name == "wireplumber" || $name == "pipewire" ]] || continue wchan=$(cat "$pid/wchan" 2>/dev/null || true) [[ $wchan == usb_* || $wchan == *usb* ]] && return 0 done return 1 } usb_audio_devices() { local card local device local devices=() local unique for card in /sys/class/sound/card*; do [[ -e $card && $card =~ /card([0-9]+)$ ]] || continue device=$(usb_device_for_card "${BASH_REMATCH[1]}" || true) unique=$(append_unique "$device" "${devices[@]}") [[ -n $unique ]] && devices+=("$unique") done printf '%s\n' "${devices[@]}" } clear_usb_audio_defaults() { local state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/wireplumber" [[ -d $state_dir ]] || return 0 if [[ -f $state_dir/default-nodes ]]; then cp "$state_dir/default-nodes" "$state_dir/default-nodes.bak.$(date +%s)" 2>/dev/null || true sed -i \ -e '/^default\.configured\.audio\.sink=alsa_output\.usb-/d' \ -e '/^default\.configured\.audio\.sink\.[0-9]\+=alsa_output\.usb-/d' \ "$state_dir/default-nodes" 2>/dev/null || true fi if [[ -f $state_dir/default-routes ]]; then cp "$state_dir/default-routes" "$state_dir/default-routes.bak.$(date +%s)" 2>/dev/null || true sed -i '/^alsa_card\.usb-/d' "$state_dir/default-routes" 2>/dev/null || true fi } stuck_usb_audio_devices() { local status local state local owner_pid local owner_name local card local device local devices=() local unique for status in /proc/asound/card*/pcm*p/sub*/status; do [[ -e $status ]] || continue state=$(awk '/^state:/ { print $2; exit }' "$status") [[ $state == "SETUP" ]] || continue owner_pid=$(awk '/^owner_pid[[:space:]]*:/ { print $3; exit }' "$status") [[ -n $owner_pid && -r /proc/$owner_pid/comm ]] || continue owner_name=$(cat "/proc/$owner_pid/comm" 2>/dev/null || true) [[ $owner_name == "wireplumber" || $owner_name == "pipewire" ]] || continue [[ $status =~ /card([0-9]+)/ ]] || continue card="${BASH_REMATCH[1]}" device=$(usb_device_for_card "$card" || true) unique=$(append_unique "$device" "${devices[@]}") [[ -n $unique ]] && devices+=("$unique") done printf '%s\n' "${devices[@]}" } reset_usb_audio_device() { local device="$1" local sysfs="/sys/bus/usb/devices/$device" local busnum local devnum local bus_device local product [[ -d $sysfs ]] || return 1 if ! omarchy-cmd-present usbreset; then echo "usbreset is not installed; replug or power-cycle $device to recover it." return 1 fi busnum=$(cat "$sysfs/busnum" 2>/dev/null) || return 1 devnum=$(cat "$sysfs/devnum" 2>/dev/null) || return 1 product=$(cat "$sysfs/product" 2>/dev/null || echo "$device") bus_device=$(printf '%03d/%03d' "$busnum" "$devnum") echo -e "\nResetting stuck USB audio device: $product ($bus_device)..." if timeout 25s sudo usbreset "$bus_device"; then return 0 fi echo "Could not reset $product automatically. Replug or power-cycle it, then run omarchy-restart-audio again." return 1 } recover_stuck_usb_audio() { local devices=() local device local detected=() local reset_count=0 while IFS= read -r device; do [[ -n $device ]] && detected+=("$device") done < <(stuck_usb_audio_devices) if (( ${#detected[@]} == 0 )) && audio_process_blocked_on_usb; then echo "Audio service is blocked in USB I/O; resetting USB audio devices..." clear_usb_audio_defaults while IFS= read -r device; do [[ -n $device ]] && detected+=("$device") done < <(usb_audio_devices) fi for device in "${detected[@]}"; do [[ -n $(append_unique "$device" "${devices[@]}") ]] && devices+=("$device") done (( ${#devices[@]} > 0 )) || return 1 for device in "${devices[@]}"; do reset_usb_audio_device "$device" && reset_count=$((reset_count + 1)) done (( reset_count > 0 )) } restart_audio_services || true if ! wpctl_healthy; then echo -e "\nPipeWire is still not responding. Checking for stuck USB audio devices..." if recover_stuck_usb_audio; then echo -e "\nRestarting audio services after USB audio reset...\n" restart_audio_services || true else echo "No stuck USB audio device was detected automatically." fi fi sleep 2 echo -e "\nAudio status:\n" if ! timeout 5s wpctl status; then echo "Audio services are still not responding. Try replugging or power-cycling the selected USB audio device." exit 1 fi