This commit is contained in:
Ray Elliott 2026-10-24 14:00:17 +01:00
commit f9df806b6d
9 changed files with 189 additions and 0 deletions

View file

@ -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

17
user/audio-resume-monitor.sh Executable file
View file

@ -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

141
user/audio-resume-recover.sh Executable file
View file

@ -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 "$@"

View file

@ -0,0 +1 @@
/usr/lib/systemd/user/voxtype.service

14
user/kokoro.service Normal file
View file

@ -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

View file

@ -0,0 +1,2 @@
[Service]
Environment=KOKORO_AUDIO_SINK=bluez_output.44_D5_CC_C8_64_39.1

View file

@ -0,0 +1 @@
/home/ray/.config/systemd/user/kokoro.service

View file

@ -0,0 +1 @@
/home/ray/.config/systemd/user/kokoro.service

View file

@ -0,0 +1 @@
/home/ray/.config/systemd/user/kokoro.service