#!/usr/bin/env bash
#
# The `audio-output` machine action: decide which way the sound leaves the
# machine.
#
#   hdmi     the TV, over the same cable as the picture - the default, and what
#            a machine in a living room is for
#   analog   the green socket on the back: powered speakers, an amplifier
#   auto     no opinion; the sound server picks, which is what it did before
#            this action existed
#
# **Why this exists.** On a machine with several outputs - and an AMD box with
# HDMI has at least two - the sound server picks one by its own ranking, and
# that is regularly not the one the television is plugged into. The symptom is
# the worst kind: the machine looks perfectly well and the first song of the
# evening plays to nobody.
#
# **How it is done.** Not by setting the current default, which is a fact about
# a running session and dies with it, but by writing a WirePlumber rule that
# raises the *priority* of the outputs of that kind. A preference survives a
# reboot, a replugged cable and a device that was not there at boot; a default
# does not. The rule matches on the node name the sound server gives an ALSA
# device (`alsa_output.…hdmi…`), which is the one part of the naming that does
# not depend on what the manufacturer called the card.
#
# Like every setting here it can be chosen with nothing plugged in: the summary
# says what outputs were actually there, and the rule waits for one to appear.

set -euo pipefail

# The shared half of the actions. The runner never sees this directory: it
# executes names out of the actions directory beside it, and a library among
# them would be a name somebody could ask for.
ARCHSTAR_ACTIONS_LIB="${ARCHSTAR_ACTIONS_LIB:-/usr/lib/archstar-actions/lib/common.sh}"
# The path is a runtime one and the linter reads these on standard input, so
# it could not follow it even if it were spelled literally.
# shellcheck source=/dev/null
source "${ARCHSTAR_ACTIONS_LIB}"

OUTPUTS=('hdmi' 'analog' 'auto')

# WirePlumber 0.5 reads drop-ins from here, in name order, after its own
# configuration. Ours is the only file this project puts there, and removing it
# is what `auto` means - the sound server goes back to deciding for itself.
WIREPLUMBER_DIR="${ARCHSTAR_WIREPLUMBER_DIR:-/etc/wireplumber/wireplumber.conf.d}"
WIREPLUMBER_FILE='51-archstar-output.conf'

# The playback cards ALSA can see, one per line as `<kind>|<description>`.
# Read from `aplay -l` for the same reason the microphones are read from
# `arecord -l`: it is what answers to root, with no session of its own.
playback_devices() {
    command -v aplay >/dev/null 2>&1 || return 0
    local line card kind
    while IFS= read -r line; do
        card="$(printf '%s' "${line}" \
            | sed -n 's/^card [0-9]\+: [^[]*\[\([^]]*\)\], device [0-9]\+: \(.*\)$/\1 - \2/p')"
        if [[ -z "${card}" ]]; then
            continue
        fi
        # The kind is read off the name the driver gives, which is what the
        # node name the rule matches is built from too.
        case "${card,,}" in
            *hdmi*|*displayport*) kind='hdmi' ;;
            *usb*) kind='usb' ;;
            *) kind='analog' ;;
        esac
        printf '%s|%s\n' "${kind}" "${card}"
    done < <(aplay -l 2>/dev/null || true)
}

# The rule for one kind of output - printed rather than written, so the suite
# can read what a button press would produce without pressing anything.
#
# `priority.session` is what the sound server sorts candidate outputs by when
# it has no explicit default; 2000 puts ours above every built-in ranking
# without pinning it, so a machine with no output of that kind still gets sound
# out of whatever it does have rather than silence.
output_rule() {
    local kind="$1" pattern
    case "${kind}" in
        hdmi)   pattern='~alsa_output.*(hdmi|displayport).*' ;;
        analog) pattern='~alsa_output.*analog.*' ;;
        *) die "unknown output '${kind}'" ;;
    esac

    printf '%s\n' \
        '# Written by the ArchStar audio-output action. Do not edit: the page' \
        '# rewrites this file whole every time somebody chooses an output.' \
        '#' \
        '# A preference, not a default: a default is a fact about one running' \
        '# session, and it dies with it. This survives a reboot and a cable' \
        '# somebody moved.' \
        'monitor.alsa.rules = [' \
        '  {' \
        '    matches = [' \
        "      { node.name = \"${pattern}\" }" \
        '    ]' \
        '    actions = {' \
        '      update-props = {' \
        '        priority.session = 2000' \
        '        priority.driver  = 2000' \
        '      }' \
        '    }' \
        '  }' \
        ']'
}

main() {
    local output="${1:-}"
    require_one_of "${output}" "${OUTPUTS[@]}"

    local devices=() line
    while IFS= read -r line; do
        if [[ -n "${line}" ]]; then
            devices+=("${line}")
        fi
    done < <(playback_devices)

    printf 'playback devices seen: %d\n' "${#devices[@]}"
    local matching=0 device
    for device in ${devices[@]+"${devices[@]}"}; do
        printf '  %s\n' "${device#*|}"
        if [[ "${device%%|*}" == "${output}" ]]; then
            matching=$(( matching + 1 ))
        fi
    done

    local target="${WIREPLUMBER_DIR}/${WIREPLUMBER_FILE}"
    if [[ "${output}" == 'auto' ]]; then
        rm -f "${target}"
        printf 'removed %s; the sound server decides again\n' "${target}"
    else
        install -d -m 0755 "${WIREPLUMBER_DIR}"
        local temporary="${target}.tmp"
        output_rule "${output}" >"${temporary}"
        chmod 0644 "${temporary}"
        mv -f "${temporary}" "${target}"
        printf 'wrote %s\n' "${target}"
    fi

    remember_setting 'audio-output' "${output}"

    # The sound server has to re-read its configuration, and the game has to
    # re-open the device it plays into. Both are best effort and both say so.
    restart_the_sound_server || true
    local restarted=''
    if restart_the_game; then
        restarted=1
    fi

    local verdict
    if [[ "${output}" == 'auto' ]]; then
        verdict='sound output left to the machine'
    elif (( matching > 0 )); then
        verdict="sound set to go out over ${output}"
    else
        verdict="sound set to go out over ${output}, but no ${output} output is connected right now"
    fi
    if [[ -n "${restarted}" ]]; then
        verdict="${verdict}; the game is restarting"
    fi
    printf '%s\n' "${verdict}"
}

# Guarded so that the suite can source this file and read what a button press
# would produce without pressing anything.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi
