#!/usr/bin/env bash set -uo pipefail readonly ECHO_ADDRESS="44:D5:CC:C8:64:39" readonly ECHO_SINK_UUID="0000110b-0000-1000-8000-00805f9b34fb" readonly ECHO_CARD="bluez_card.44_D5_CC_C8_64_39" readonly ECHO_SINK="bluez_output.44_D5_CC_C8_64_39.1" readonly KOKORO_SERVICE="kokoro.service" readonly VOXTYPE_SERVICE="voxtype.service" readonly STARTUP_DELAY_SECONDS=3 readonly RETRY_DELAY_SECONDS=2 readonly MAX_RETRIES=15 log() { printf 'audio-resume: %s\n' "$*" } fail() { local message="$1" log "ERROR: ${message}" notify-send --urgency=critical "Audio resume failed" "${message}" >/dev/null 2>&1 || true exit 1 } wait_for_pipewire() { local attempt for ((attempt = 1; attempt <= MAX_RETRIES; attempt++)); do if pactl info >/dev/null 2>&1; then return 0 fi sleep "${RETRY_DELAY_SECONDS}" done return 1 } echo_sink_exists() { pactl list short sinks 2>/dev/null | awk -v sink="${ECHO_SINK}" '$2 == sink { found = 1 } END { exit !found }' } echo_sink_is_running() { pactl list sinks 2>/dev/null | awk -v sink="${ECHO_SINK}" ' $1 == "Name:" { selected = ($2 == sink) } selected && $1 == "State:" && $2 == "RUNNING" { found = 1 } END { exit !found } ' } connect_echo_output() { local attempt bluetoothctl power on >/dev/null 2>&1 || true for ((attempt = 1; attempt <= MAX_RETRIES; attempt++)); do log "Connecting Echo output profile (attempt ${attempt}/${MAX_RETRIES})" bluetoothctl connect "${ECHO_ADDRESS}" "${ECHO_SINK_UUID}" >/dev/null 2>&1 || true if echo_sink_exists; then pactl set-card-profile "${ECHO_CARD}" a2dp-sink >/dev/null 2>&1 || true return 0 fi sleep "${RETRY_DELAY_SECONDS}" done return 1 } wait_for_kokoro() { local attempt for ((attempt = 1; attempt <= MAX_RETRIES; attempt++)); do if test -S "${XDG_RUNTIME_DIR}/kokoro.sock"; then return 0 fi sleep "${RETRY_DELAY_SECONDS}" done return 1 } verify_playback() { local attempt /usr/local/src/kokora/say "Audio resumed." || return 1 for ((attempt = 1; attempt <= MAX_RETRIES; attempt++)); do if echo_sink_is_running; then return 0 fi sleep 1 done return 1 } main() { sleep "${STARTUP_DELAY_SECONDS}" # Do not leave an always-open pw-cat stream attached while rebuilding the # Bluetooth and PipeWire graph. systemctl --user stop "${KOKORO_SERVICE}" || true bluetoothctl disconnect "${ECHO_ADDRESS}" >/dev/null 2>&1 || true log "Restarting PipeWire and WirePlumber with the Echo disconnected" systemctl --user restart pipewire.service pipewire-pulse.service wireplumber.service \ || fail "PipeWire or WirePlumber could not be restarted." # pipewire.service wants Kokoro, so make sure it remains out of the way # until the correct Echo output profile has been established. systemctl --user stop "${KOKORO_SERVICE}" || true wait_for_pipewire \ || fail "PipeWire did not become ready after resume." connect_echo_output \ || fail "The Echo connected without a usable A2DP output." # Voxtype keeps an ALSA output stream open for sound cues. Recreate it # after PipeWire and the default output have been rebuilt. log "Restarting Voxtype after the Echo output is ready" systemctl --user restart "${VOXTYPE_SERVICE}" \ || fail "Voxtype could not be restarted." log "Starting Kokoro after the Echo output is ready" systemctl --user start "${KOKORO_SERVICE}" \ || fail "Kokoro could not be started." wait_for_kokoro \ || fail "Kokoro started without creating its socket." verify_playback \ || fail "The Echo sink appeared, but playback did not become active." log "Audio recovery completed successfully" } main "$@"