From f9df806b6d4ff747921a2496c4ff9be7eee58368 Mon Sep 17 00:00:00 2001 From: ray Date: Sat, 24 Oct 2026 14:00:17 +0100 Subject: [PATCH] update --- user/audio-resume-monitor.service | 11 ++ user/audio-resume-monitor.sh | 17 +++ user/audio-resume-recover.sh | 141 ++++++++++++++++++ .../voxtype.service | 1 + user/kokoro.service | 14 ++ user/kokoro.service.d/echo-output.conf | 2 + .../kokoro.service | 1 + user/pipewire.service.wants/kokoro.service | 1 + user/wireplumber.service.wants/kokoro.service | 1 + 9 files changed, 189 insertions(+) create mode 100644 user/audio-resume-monitor.service create mode 100755 user/audio-resume-monitor.sh create mode 100755 user/audio-resume-recover.sh create mode 120000 user/graphical-session.target.wants/voxtype.service create mode 100644 user/kokoro.service create mode 100644 user/kokoro.service.d/echo-output.conf create mode 120000 user/pipewire-pulse.service.wants/kokoro.service create mode 120000 user/pipewire.service.wants/kokoro.service create mode 120000 user/wireplumber.service.wants/kokoro.service diff --git a/user/audio-resume-monitor.service b/user/audio-resume-monitor.service new file mode 100644 index 0000000..bbf4c99 --- /dev/null +++ b/user/audio-resume-monitor.service @@ -0,0 +1,11 @@ +[Unit] +Description=Restore audio after system resume + +[Service] +Type=simple +ExecStart=/home/ray/.config/systemd/user/audio-resume-monitor.sh +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target diff --git a/user/audio-resume-monitor.sh b/user/audio-resume-monitor.sh new file mode 100755 index 0000000..14089a6 --- /dev/null +++ b/user/audio-resume-monitor.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -uo pipefail + +readonly RECOVERY_SCRIPT="/home/ray/.config/systemd/user/audio-resume-recover.sh" + +gdbus monitor --system \ + --dest org.freedesktop.login1 \ + --object-path /org/freedesktop/login1 | +while IFS= read -r line; do + case "${line}" in + *"PrepareForSleep"*"false"*) + printf 'audio-resume-monitor: resume detected\n' + "${RECOVERY_SCRIPT}" + ;; + esac +done diff --git a/user/audio-resume-recover.sh b/user/audio-resume-recover.sh new file mode 100755 index 0000000..0a9e55e --- /dev/null +++ b/user/audio-resume-recover.sh @@ -0,0 +1,141 @@ +#!/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 "$@" diff --git a/user/graphical-session.target.wants/voxtype.service b/user/graphical-session.target.wants/voxtype.service new file mode 120000 index 0000000..131c404 --- /dev/null +++ b/user/graphical-session.target.wants/voxtype.service @@ -0,0 +1 @@ +/usr/lib/systemd/user/voxtype.service \ No newline at end of file diff --git a/user/kokoro.service b/user/kokoro.service new file mode 100644 index 0000000..0bcc20a --- /dev/null +++ b/user/kokoro.service @@ -0,0 +1,14 @@ +[Unit] +Description=Persistent Kokoro CPU text-to-speech server +After=pipewire.service pipewire-pulse.service wireplumber.service +PartOf=pipewire.service pipewire-pulse.service wireplumber.service + +[Service] +Type=simple +Environment=HF_HUB_OFFLINE=1 +ExecStart=/usr/local/src/kokora/kokoro-server +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target pipewire.service pipewire-pulse.service wireplumber.service diff --git a/user/kokoro.service.d/echo-output.conf b/user/kokoro.service.d/echo-output.conf new file mode 100644 index 0000000..f432fda --- /dev/null +++ b/user/kokoro.service.d/echo-output.conf @@ -0,0 +1,2 @@ +[Service] +Environment=KOKORO_AUDIO_SINK=bluez_output.44_D5_CC_C8_64_39.1 diff --git a/user/pipewire-pulse.service.wants/kokoro.service b/user/pipewire-pulse.service.wants/kokoro.service new file mode 120000 index 0000000..668bb42 --- /dev/null +++ b/user/pipewire-pulse.service.wants/kokoro.service @@ -0,0 +1 @@ +/home/ray/.config/systemd/user/kokoro.service \ No newline at end of file diff --git a/user/pipewire.service.wants/kokoro.service b/user/pipewire.service.wants/kokoro.service new file mode 120000 index 0000000..668bb42 --- /dev/null +++ b/user/pipewire.service.wants/kokoro.service @@ -0,0 +1 @@ +/home/ray/.config/systemd/user/kokoro.service \ No newline at end of file diff --git a/user/wireplumber.service.wants/kokoro.service b/user/wireplumber.service.wants/kokoro.service new file mode 120000 index 0000000..668bb42 --- /dev/null +++ b/user/wireplumber.service.wants/kokoro.service @@ -0,0 +1 @@ +/home/ray/.config/systemd/user/kokoro.service \ No newline at end of file