188 lines
6.7 KiB
Bash
Executable File
188 lines
6.7 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
base=/data/opt/alice-backend
|
|
runtime=$base/spotify
|
|
room=$base/room-eq
|
|
profile=$room/profile
|
|
state=/tmp/alice-room-eq.state
|
|
pa=$runtime/pactl
|
|
export PULSE_SERVER=unix:/tmp/alice-pulse/native
|
|
export PULSE_RUNTIME_PATH=/tmp/alice-pulse
|
|
export PULSE_COOKIE=/tmp/alice-pulse/cookie
|
|
umask 077
|
|
exec 7>/tmp/alice-room-eq.lock
|
|
flock -n 7 || { echo 'Room EQ is busy' >&2; exit 1; }
|
|
|
|
sink_exists() { "$pa" list short sinks | awk -v name="$1" '$2 == name {found=1} END {exit !found}'; }
|
|
music_input() {
|
|
module=$("$pa" list short modules | awk '$2 == "module-remap-sink" && $0 ~ /sink_name=music([[:space:]]|$)/ {print $1; exit}')
|
|
[ -n "$module" ] || return 1
|
|
"$pa" list sink-inputs | awk -v module="$module" '
|
|
/^Sink Input #/ {id=substr($3,2)}
|
|
$1=="Owner" && $2=="Module:" && $3==module && id!="" {print id; exit}'
|
|
}
|
|
move_music() {
|
|
input=$(music_input)
|
|
[ -n "$input" ] && "$pa" move-sink-input "$input" "$1"
|
|
}
|
|
cleanup_filters() {
|
|
# Match sink arguments to prevent stale module IDs from unloading other services.
|
|
"$pa" list short modules | awk -v keep="${1:-}" '
|
|
$2 == "module-ladspa-sink" && $0 ~ /sink_name=alice_eq_[ab]_[0-9]+([[:space:]]|$)/ &&
|
|
(keep == "" || $0 !~ ("sink_name=alice_eq_[ab]_" keep "([[:space:]]|$)")) {print $1}' |
|
|
while read -r module; do "$pa" unload-module "$module" || :; done
|
|
}
|
|
validate_controls() {
|
|
printf '%s\n' "$1" | awk -F, '
|
|
NF != 16 {exit 1}
|
|
{for (i=1;i<=NF;i++) if ($i !~ /^-?[0-9]+([.][0-9]+)?$/ || length($i)>12) exit 1
|
|
if ($1 < -12 || $1 > 0) exit 1
|
|
for (i=2;i<=NF;i+=3) if ($i<100 || $i>1600 || $(i+1)<0.5 || $(i+1)>3 || $(i+2)<-6 || $(i+2)>0) exit 1}'
|
|
}
|
|
read_profile() {
|
|
IFS= read -r mode
|
|
IFS= read -r eq
|
|
IFS= read -r ref
|
|
case "$mode" in eq|ref|off) ;; *) return 1 ;; esac
|
|
validate_controls "$eq" && validate_controls "$ref"
|
|
}
|
|
load_filter() {
|
|
"$pa" load-module module-ladspa-sink "sink_name=$1" master=station \
|
|
"plugin=$room/alice_room_eq" label=alice_room_eq "control=$2" >/dev/null
|
|
}
|
|
activate() {
|
|
suffix=$$
|
|
a=alice_eq_a_$suffix
|
|
b=alice_eq_b_$suffix
|
|
if ! load_filter "$a" "$eq" || ! load_filter "$b" "$ref"; then
|
|
for name in "$a" "$b"; do
|
|
"$pa" list short modules | awk -v name="$name" '$2=="module-ladspa-sink" && $0 ~ ("sink_name=" name "([[:space:]]|$)") {print $1}' |
|
|
while read -r m; do "$pa" unload-module "$m" || :; done
|
|
done
|
|
return 1
|
|
fi
|
|
case "$mode" in eq) target=$a ;; ref) target=$b ;; off) target=station ;; esac
|
|
if ! move_music "$target"; then
|
|
for name in "$a" "$b"; do
|
|
"$pa" list short modules | awk -v name="$name" '$2=="module-ladspa-sink" && $0 ~ ("sink_name=" name "([[:space:]]|$)") {print $1}' |
|
|
while read -r m; do "$pa" unload-module "$m" || :; done
|
|
done
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$suffix" > "$state"
|
|
cleanup_filters "$suffix"
|
|
}
|
|
stage_profile() {
|
|
printf '%s\n' "$mode" "$eq" "$ref" > "$profile.new"
|
|
}
|
|
calibration_ready() {
|
|
playback=$(printf '%s\n' '{"name":"spotify_control","arguments":{"action":"status"}}' | "$base/music-control")
|
|
case "$playback" in *'"status":"playing"'*|*'"status":"buffering"'*) echo 'Pause Spotify before the sweep' >&2; return 1 ;; esac
|
|
gain=$("$pa" get-sink-volume station | awk '/^Volume:/{print $5;exit}' | tr -d '%')
|
|
case "$gain" in ''|*[!0-9]*) echo 'Cannot verify speaker volume' >&2; return 1 ;; esac
|
|
if [ "$gain" -lt 10 ] || [ "$gain" -gt 65 ]; then
|
|
echo 'Set the speaker master volume between 10 and 65 percent for the sweep' >&2
|
|
return 1
|
|
fi
|
|
}
|
|
play_probe() {
|
|
sink=$1
|
|
sink_exists "$sink" || { echo 'EQ profile unavailable' >&2; return 1; }
|
|
"$runtime/run" "$runtime/bin/pacat" --playback --device="$sink" \
|
|
--raw --rate=48000 --channels=1 --format=s16le --latency-msec=40 < "$room/probe.pcm"
|
|
}
|
|
probe_sink() {
|
|
case "${1:-baseline}" in
|
|
baseline) printf '%s\n' station ;;
|
|
corrected)
|
|
suffix=$(cat "$state")
|
|
case "$suffix" in ''|*[!0-9]*) return 1 ;; esac
|
|
printf '%s\n' "alice_eq_a_$suffix"
|
|
;;
|
|
*) return 2 ;;
|
|
esac
|
|
}
|
|
measure() {
|
|
calibration_ready
|
|
sink=$(probe_sink "${1:-baseline}")
|
|
sink_exists "$sink"
|
|
gpio=/sys/class/gpio/gpio414/value
|
|
[ "$(cat "$gpio")" = 1 ] || { echo 'Microphone muted' >&2; return 1; }
|
|
path=$(mktemp /tmp/alice-room-capture.XXXXXX)
|
|
capture= guard= restore=0
|
|
clean() {
|
|
for pid in "$guard" "$capture"; do [ -z "$pid" ] || kill "$pid" 2>/dev/null || :; done
|
|
[ ! -e "$path" ] || rm "$path"
|
|
if [ "$restore" -eq 1 ]; then /etc/init.d/S99alice start >/dev/null 7>&-; fi
|
|
}
|
|
trap clean EXIT
|
|
trap 'exit 130' HUP INT TERM
|
|
if [ -f /tmp/alice-supervisor.pid ]; then
|
|
/etc/init.d/S99alice stop
|
|
restore=1
|
|
fi
|
|
arecord -q -D plug:default -t raw -f S16_LE -c 6 -r 48000 --period-size=512 --buffer-size=4096 -d 9 > "$path" &
|
|
capture=$!
|
|
(
|
|
while kill -0 "$capture" 2>/dev/null; do
|
|
[ "$(cat "$gpio")" = 1 ] || { kill "$capture" 2>/dev/null || :; exit 1; }
|
|
sleep 0.1
|
|
done
|
|
) &
|
|
guard=$!
|
|
sleep 1
|
|
kill -0 "$capture"
|
|
play_probe "$sink"
|
|
wait "$capture"
|
|
capture=
|
|
[ "$(cat "$gpio")" = 1 ] && [ "$(wc -c < "$path")" -eq 5184000 ]
|
|
cat "$path"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
apply)
|
|
read_profile
|
|
stage_profile
|
|
trap 'rm -f "$profile.new"' EXIT
|
|
activate
|
|
mv "$profile.new" "$profile"
|
|
;;
|
|
restore)
|
|
[ -r "$profile" ] || exit 0
|
|
read_profile < "$profile"
|
|
activate
|
|
;;
|
|
eq|ref|off)
|
|
if [ ! -r "$profile" ] && [ "$1" = off ]; then
|
|
move_music station
|
|
exit
|
|
fi
|
|
read_profile < "$profile"
|
|
suffix=$(cat "$state")
|
|
case "$suffix" in ''|*[!0-9]*) exit 1 ;; esac
|
|
mode=$1
|
|
case "$mode" in eq) target=alice_eq_a_$suffix ;; ref) target=alice_eq_b_$suffix ;; off) target=station ;; esac
|
|
stage_profile
|
|
trap 'rm -f "$profile.new"' EXIT
|
|
move_music "$target"
|
|
mv "$profile.new" "$profile"
|
|
;;
|
|
disable)
|
|
move_music station
|
|
[ ! -e "$profile" ] || rm "$profile"
|
|
[ ! -e "$state" ] || rm "$state"
|
|
cleanup_filters
|
|
;;
|
|
status)
|
|
"$pa" list short sinks
|
|
if [ -r "$profile" ]; then read_profile < "$profile"; printf 'Mode %s\n' "$mode"; else printf 'Mode off\n'; fi
|
|
;;
|
|
play)
|
|
calibration_ready
|
|
sink=$(probe_sink "${2:-baseline}")
|
|
play_probe "$sink"
|
|
;;
|
|
measure) measure "${2:-baseline}" ;;
|
|
*) echo 'Usage: room-eq.sh status|apply|eq|ref|off|disable|restore|play [baseline|corrected]|measure [baseline|corrected]' >&2; exit 2 ;;
|
|
esac
|