429 lines
10 KiB
Bash
Executable file
429 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
sink_name="${REFORGER_SURROUND_SINK_NAME:-reforger_surround_51}"
|
|
sink_description="${REFORGER_SURROUND_DESCRIPTION:-Reforger Surround 5.1}"
|
|
app_name="${REFORGER_SURROUND_APP_NAME:-Arma Reforger}"
|
|
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/reforger-surround"
|
|
module_id_file="${state_dir}/module-id"
|
|
previous_default_sink_file="${state_dir}/previous-default-sink"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
reforger-surround create
|
|
reforger-surround up
|
|
reforger-surround expose
|
|
reforger-surround pin
|
|
reforger-surround restore
|
|
reforger-surround down
|
|
reforger-surround status
|
|
reforger-surround check
|
|
|
|
Commands:
|
|
create Create the temporary 5.1 sink without changing the default sink.
|
|
up Create the 5.1 sink, save the current default, and make it default.
|
|
expose Create the 5.1 sink and move a running Reforger stream into it.
|
|
pin Alias for expose.
|
|
restore Restore the saved previous default sink, then expose Reforger if running.
|
|
down Restore the saved previous default sink and unload the 5.1 sink.
|
|
status Show default sink, virtual sink state, and detected Reforger stream.
|
|
check Report whether a running Reforger stream negotiated stereo or surround.
|
|
|
|
Environment:
|
|
REFORGER_SURROUND_SINK_NAME Override sink name. Default: reforger_surround_51
|
|
REFORGER_SURROUND_DESCRIPTION Override visible sink name. Default: Reforger Surround 5.1
|
|
REFORGER_SURROUND_APP_NAME Override app name. Default: Arma Reforger
|
|
EOF
|
|
}
|
|
|
|
require_command() {
|
|
local command="$1"
|
|
|
|
if ! command -v "${command}" >/dev/null 2>&1; then
|
|
printf 'Missing required command: %s\n' "${command}" >&2
|
|
exit 127
|
|
fi
|
|
}
|
|
|
|
ensure_dependencies() {
|
|
require_command pactl
|
|
require_command awk
|
|
require_command sed
|
|
}
|
|
|
|
require_pulse_connection() {
|
|
if ! pactl info >/dev/null 2>&1; then
|
|
printf 'Could not connect to PulseAudio/PipeWire through pactl.\n' >&2
|
|
printf 'Check that PipeWire and pipewire-pulse are running for this user session.\n' >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
ensure_state_dir() {
|
|
mkdir -p "${state_dir}"
|
|
}
|
|
|
|
current_default_sink() {
|
|
pactl get-default-sink 2>/dev/null || true
|
|
}
|
|
|
|
sink_exists() {
|
|
pactl list sinks short | awk -v sink_name="${sink_name}" '$2 == sink_name { found = 1 } END { exit !found }'
|
|
}
|
|
|
|
find_module_id() {
|
|
pactl list short modules | awk -v sink_name="${sink_name}" '
|
|
$2 == "module-null-sink" && index($0, "sink_name=" sink_name) {
|
|
print $1
|
|
exit
|
|
}
|
|
'
|
|
}
|
|
|
|
stored_module_id() {
|
|
[[ -s "${module_id_file}" ]] && sed -n '1p' "${module_id_file}"
|
|
}
|
|
|
|
create_sink() {
|
|
ensure_state_dir
|
|
|
|
local module_id
|
|
module_id="$(find_module_id || true)"
|
|
if [[ -n "${module_id}" ]]; then
|
|
printf '%s\n' "${module_id}" > "${module_id_file}"
|
|
printf 'Using existing 5.1 sink: %s (module %s)\n' "${sink_name}" "${module_id}"
|
|
return 0
|
|
fi
|
|
|
|
if sink_exists; then
|
|
printf '5.1 sink already exists: %s\n' "${sink_name}"
|
|
return 0
|
|
fi
|
|
|
|
module_id="$(pactl load-module module-null-sink \
|
|
"sink_name=${sink_name}" \
|
|
"sink_properties=device.description=\"${sink_description}\"" \
|
|
"channels=6" \
|
|
"channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right" \
|
|
"rate=48000")"
|
|
|
|
printf '%s\n' "${module_id}" > "${module_id_file}"
|
|
printf 'Created 5.1 sink: %s (module %s)\n' "${sink_name}" "${module_id}"
|
|
}
|
|
|
|
save_previous_default() {
|
|
ensure_state_dir
|
|
|
|
local default_sink
|
|
default_sink="$(current_default_sink)"
|
|
if [[ -n "${default_sink}" && "${default_sink}" != "${sink_name}" ]]; then
|
|
printf '%s\n' "${default_sink}" > "${previous_default_sink_file}"
|
|
printf 'Saved previous default sink: %s\n' "${default_sink}"
|
|
fi
|
|
}
|
|
|
|
set_sink_default() {
|
|
pactl set-default-sink "${sink_name}"
|
|
printf 'Set launch default sink: %s\n' "${sink_name}"
|
|
}
|
|
|
|
up() {
|
|
create_sink
|
|
save_previous_default
|
|
set_sink_default
|
|
cat <<EOF
|
|
|
|
Launch Arma Reforger now. After the game has created its audio stream, run:
|
|
reforger-surround restore
|
|
reforger-surround check
|
|
EOF
|
|
}
|
|
|
|
expose_app() {
|
|
local input_id sample_spec sink_after
|
|
|
|
create_sink
|
|
input_id="$(find_app_sink_input_id || true)"
|
|
|
|
if [[ -z "${input_id}" ]]; then
|
|
printf 'No running sink-input found for application.name="%s".\n' "${app_name}" >&2
|
|
return 1
|
|
fi
|
|
|
|
sample_spec="$(app_stream_sample_spec "${input_id}")"
|
|
pactl move-sink-input "${input_id}" "${sink_name}"
|
|
sink_after="$(app_stream_sink_name "${input_id}" || true)"
|
|
printf 'Exposed %s sink-input #%s to %s\n' "${app_name}" "${input_id}" "${sink_name}"
|
|
printf 'Current stream sink: %s\n' "${sink_after:-unknown}"
|
|
|
|
case "${sample_spec}" in
|
|
*" 6ch "*)
|
|
printf 'Stream is 6ch; Reforger should now stay on the 5.1 sink.\n'
|
|
;;
|
|
*" 2ch "*)
|
|
printf 'Warning: stream is already stereo-only. Pinning will not make Reforger renegotiate surround.\n' >&2
|
|
;;
|
|
*)
|
|
printf 'Warning: unknown stream channel count; run reforger-surround check.\n' >&2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
pin_app_if_running() {
|
|
local input_id
|
|
input_id="$(find_app_sink_input_id || true)"
|
|
|
|
if [[ -n "${input_id}" ]]; then
|
|
expose_app
|
|
else
|
|
printf 'No %s stream found to pin; restoring only the system default.\n' "${app_name}"
|
|
fi
|
|
}
|
|
|
|
restore_default() {
|
|
local restore_sink
|
|
restore_sink="$(sed -n '1p' "${previous_default_sink_file}" 2>/dev/null || true)"
|
|
|
|
if [[ -z "${restore_sink}" ]]; then
|
|
printf 'No saved previous default sink found; nothing to restore.\n'
|
|
return 0
|
|
fi
|
|
|
|
if ! pactl list sinks short | awk -v sink_name="${restore_sink}" '$2 == sink_name { found = 1 } END { exit !found }'; then
|
|
printf 'Saved previous default sink no longer exists: %s\n' "${restore_sink}" >&2
|
|
return 1
|
|
fi
|
|
|
|
pactl set-default-sink "${restore_sink}"
|
|
printf 'Restored default sink: %s\n' "${restore_sink}"
|
|
}
|
|
|
|
restore_with_pin() {
|
|
restore_default || true
|
|
pin_app_if_running
|
|
}
|
|
|
|
unload_sink() {
|
|
local module_id
|
|
module_id="$(find_module_id || true)"
|
|
if [[ -z "${module_id}" ]]; then
|
|
module_id="$(stored_module_id || true)"
|
|
fi
|
|
|
|
if [[ -z "${module_id}" ]]; then
|
|
printf 'No %s module found to unload.\n' "${sink_name}"
|
|
return 0
|
|
fi
|
|
|
|
pactl unload-module "${module_id}" 2>/dev/null || true
|
|
rm -f "${module_id_file}"
|
|
printf 'Unloaded 5.1 sink module: %s\n' "${module_id}"
|
|
}
|
|
|
|
down() {
|
|
restore_default || true
|
|
unload_sink
|
|
}
|
|
|
|
find_app_sink_input_id() {
|
|
pactl list sink-inputs | awk -v app_name="${app_name}" '
|
|
/^Sink Input #/ {
|
|
id = substr($3, 2)
|
|
}
|
|
index($0, "application.name = \"" app_name "\"") {
|
|
print id
|
|
exit
|
|
}
|
|
'
|
|
}
|
|
|
|
app_stream_sink_name() {
|
|
local input_id="$1"
|
|
|
|
pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
|
|
$0 == target {
|
|
in_input = 1
|
|
next
|
|
}
|
|
in_input && $1 == "Sink:" {
|
|
print $2
|
|
exit
|
|
}
|
|
in_input && /^Sink Input #/ {
|
|
exit
|
|
}
|
|
' | while read -r sink_id; do
|
|
pactl list sinks short | awk -v sink_id="${sink_id}" '$1 == sink_id { print $2; exit }'
|
|
done
|
|
}
|
|
|
|
app_stream_lines() {
|
|
local input_id="$1"
|
|
|
|
pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
|
|
$0 == target {
|
|
in_input = 1
|
|
next
|
|
}
|
|
in_input && ($1 == "Sink:" || ($1 == "Sample" && $2 == "Specification:") || ($1 == "Channel" && $2 == "Map:")) {
|
|
print
|
|
}
|
|
in_input && /^Sink Input #/ {
|
|
exit
|
|
}
|
|
'
|
|
}
|
|
|
|
app_stream_sample_spec() {
|
|
local input_id="$1"
|
|
|
|
pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
|
|
$0 == target {
|
|
in_input = 1
|
|
next
|
|
}
|
|
in_input && $1 == "Sample" && $2 == "Specification:" {
|
|
print $0
|
|
exit
|
|
}
|
|
in_input && /^Sink Input #/ {
|
|
exit
|
|
}
|
|
'
|
|
}
|
|
|
|
app_stream_channel_map() {
|
|
local input_id="$1"
|
|
|
|
pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
|
|
$0 == target {
|
|
in_input = 1
|
|
next
|
|
}
|
|
in_input && $1 == "Channel" && $2 == "Map:" {
|
|
print $0
|
|
exit
|
|
}
|
|
in_input && /^Sink Input #/ {
|
|
exit
|
|
}
|
|
'
|
|
}
|
|
|
|
print_check() {
|
|
local input_id sample_spec channel_map sink
|
|
input_id="$(find_app_sink_input_id || true)"
|
|
|
|
if [[ -z "${input_id}" ]]; then
|
|
printf 'No running sink-input found for application.name="%s".\n' "${app_name}" >&2
|
|
return 1
|
|
fi
|
|
|
|
sample_spec="$(app_stream_sample_spec "${input_id}")"
|
|
channel_map="$(app_stream_channel_map "${input_id}")"
|
|
sink="$(app_stream_sink_name "${input_id}" || true)"
|
|
|
|
printf 'Detected %s sink-input #%s\n' "${app_name}" "${input_id}"
|
|
printf 'Sink: %s\n' "${sink:-unknown}"
|
|
printf '%s\n' "${sample_spec}"
|
|
printf '%s\n' "${channel_map}"
|
|
|
|
case "${sample_spec}" in
|
|
*" 6ch "*)
|
|
printf 'Result: success, Reforger negotiated surround output.\n'
|
|
printf 'Expected useful channels: FL, FR, FC, RL, RR. LFE may be silent.\n'
|
|
;;
|
|
*" 2ch "*)
|
|
printf 'Result: stereo-only. Restart Reforger with the 5.1 sink selected/default before launch.\n'
|
|
return 2
|
|
;;
|
|
*)
|
|
printf 'Result: unknown channel count; inspect the sample specification and channel map above.\n'
|
|
return 3
|
|
;;
|
|
esac
|
|
}
|
|
|
|
print_status() {
|
|
local module_id input_id
|
|
module_id="$(find_module_id || true)"
|
|
input_id="$(find_app_sink_input_id || true)"
|
|
|
|
printf 'default sink: %s\n' "$(current_default_sink)"
|
|
printf '5.1 sink: %s\n' "${sink_name}"
|
|
printf '5.1 module: %s\n' "${module_id:-not loaded}"
|
|
printf 'saved previous default: %s\n' "$(sed -n '1p' "${previous_default_sink_file}" 2>/dev/null || printf 'none')"
|
|
|
|
if sink_exists; then
|
|
pactl list sinks short | awk -v sink_name="${sink_name}" '$2 == sink_name { print }'
|
|
fi
|
|
|
|
if [[ -z "${input_id}" ]]; then
|
|
printf '%s stream: not found\n' "${app_name}"
|
|
else
|
|
printf '%s stream: sink-input #%s\n' "${app_name}" "${input_id}"
|
|
app_stream_lines "${input_id}"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
local command="${1:-}"
|
|
if [[ -z "${command}" ]]; then
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${command}" in
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
create)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
create_sink
|
|
;;
|
|
up)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
up
|
|
;;
|
|
expose)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
expose_app
|
|
;;
|
|
pin)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
expose_app
|
|
;;
|
|
restore)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
restore_with_pin
|
|
;;
|
|
down)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
down
|
|
;;
|
|
status)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
print_status
|
|
;;
|
|
check)
|
|
ensure_dependencies
|
|
require_pulse_connection
|
|
print_check
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|