#!/usr/bin/env bash
# check-audio-graph — assert the PipeWire graph is the one the RIG wants.
#
# WHY THIS EXISTS (2026-08-03, J-5 before OPAL)
#
# Sound went glitchy-then-silent mid-rehearsal and every obvious suspect was
# healthy: sclang up, scsynth up and using 6.7% CPU, the UMC sink present and
# RUNNING, every link in place. Nothing that existed at the time could have
# caught it, because the fault was not in any component — it was in the SHAPE OF
# THE GRAPH. Two nodes nobody used (the SOF headset mic and an HDMI sink brought
# up by screen mirroring) were held permanently open by a graph-wide no-suspend
# rule and had accumulated 2.1 MILLION xruns between them, starving the only
# device in the box with a real-time deadline tight enough to care.
#
# Worse, the same audit found the laptop MICROPHONE wired into `ardour:Tidal 01`
# — the track that records d1 — because Ardour auto-connects physical inputs.
# That is invisible in Ardour's GUI and invisible in the saved session file. It
# had been silently summed into stem recordings.
#
# So this script asserts the things that were wrong, in the order that they bite.
#
# TWO METHODOLOGY RULES IT OBEYS, both learned the hard way that day:
#
#  1. NEVER SCAN THE JOURNAL. `journalctl --since ...` on a long-uptime box reads
#     the whole journal and burned a full core TWICE while PLN was losing audio —
#     the diagnostic making the fault worse. Everything here is /proc, pw-link
#     and a bounded pw-top.
#
#  2. `state: RUNNING` PROVES NOTHING. It read RUNNING while the device was dying
#     and restarting once a second. The only honest reading is the hardware
#     pointer, sampled repeatedly: ~48000 frames/sec is realtime, and a NEGATIVE
#     delta means the stream restarted. Take several samples — a two-sample read
#     gave a confidently wrong verdict because a ring-buffer wrap looks exactly
#     like a stall.
#
# Usage:
#   tools/check-audio-graph.sh          # assert; non-zero on any hard failure
#   tools/check-audio-graph.sh -v       # show every measurement
set -uo pipefail

VERBOSE=0
[ "${1:-}" = "-v" ] && VERBOSE=1

FAIL=0
WARN=0
ok()   { printf '  \033[32mok\033[0m    %s\n' "$*"; }
bad()  { printf '  \033[31mFAIL\033[0m  %s\n' "$*"; FAIL=$((FAIL+1)); }
warn() { printf '  \033[33mwarn\033[0m  %s\n' "$*"; WARN=$((WARN+1)); }
note() { [ "$VERBOSE" = 1 ] && printf '        %s\n' "$*"; return 0; }

echo "check-audio-graph — is the graph the one the rig wants?"
echo

# ---------------------------------------------------------------------------
# 1. NOTHING BUT SUPERCOLLIDER MAY FEED A TIDAL TRACK
#
# This is the #132 bug and it goes first because it is the only one here that
# corrupts a RECORDING rather than merely risking a dropout. A dropout you hear
# and can redo; a stem with room noise baked in you ship.
# ---------------------------------------------------------------------------
echo "1. recording integrity — only SuperCollider feeds the Tidal tracks"
if ! command -v pw-link >/dev/null 2>&1; then
    warn "pw-link not found — cannot audit the graph"
else
    LINKS=$(pw-link -l 2>/dev/null)
    # pw-link prints a port, then its peers indented. Walk it and remember the
    # last non-indented line so an indented `ardour:Tidal NN/audio_in` peer can
    # be attributed to the SOURCE that feeds it.
    STRAY=$(printf '%s\n' "$LINKS" | awk '
        /^[^[:space:]]/ { src = $0; next }
        /\|-> *ardour:Tidal [0-9]+\/audio_in/ {
            if (src !~ /^SuperCollider:out_/) print src
        }' | sort -u)
    if [ -n "$STRAY" ]; then
        bad "a NON-SuperCollider source feeds a Tidal track — stems will contain it:"
        printf '%s\n' "$STRAY" | sed 's/^/          /'
    else
        N=$(printf '%s\n' "$LINKS" | grep -c 'ardour:Tidal [0-9]*/audio_in' || true)
        ok "no stray source on any Tidal input ($N Tidal inputs linked)"
    fi
fi
echo

# ---------------------------------------------------------------------------
# 2. THE MONITOR PATH ACTUALLY REACHES THE INTERFACE
#
# Ardour restores its own saved port connections and does NOT follow the
# PipeWire default sink. Switching the default sink once left the whole rig
# playing out of the laptop jack — the very codec the UMC exists to escape —
# with no warning at all. Assert the last hop explicitly, every time.
# ---------------------------------------------------------------------------
echo "2. monitor path — ardour Master reaches the UMC"
MASTER=$(pw-link -l 2>/dev/null | awk '/^ardour:Master\/audio_out/ {f=1; next} /^[^[:space:]]/ {f=0} f')
UMC_CH=$(printf '%s\n' "$MASTER" | grep -c 'UMC202HD' || true)
SOF_CH=$(printf '%s\n' "$MASTER" | grep -c 'sof_sdw' || true)
if [ "$UMC_CH" -ge 2 ]; then
    ok "both Master channels on the UMC202HD"
elif [ "$SOF_CH" -gt 0 ]; then
    bad "Master is on the INTERNAL codec (sof_sdw), not the UMC — the thing the UMC exists to avoid"
else
    bad "Master reaches the UMC on $UMC_CH/2 channels"
fi
echo

# ---------------------------------------------------------------------------
# 3. IS THE INTERFACE ACTUALLY CLOCKING?
#
# The measurement that cannot lie. See rule 2 in the header.
# ---------------------------------------------------------------------------
echo "3. the interface is clocking (hw_ptr, not state)"
PCM=$(ls -d /proc/asound/U192k/pcm0p/sub0 2>/dev/null || true)
if [ -z "$PCM" ]; then
    warn "no UMC playback substream (device absent, or card renamed — it has been card 0 and card 1)"
elif ! grep -q '^state' "$PCM/status" 2>/dev/null; then
    note "stream closed (nothing playing) — cannot judge clocking"
    ok "no stream open; nothing to measure"
else
    prev=""; restarts=0; slow=0; samples=0
    for _ in 1 2 3 4; do
        read -r st hp < <(awk '/^state/{s=$2} /^hw_ptr/{h=$3} END{print s, h}' "$PCM/status" 2>/dev/null)
        if [ -n "$prev" ] && [ -n "${hp:-}" ]; then
            d=$((hp - prev)); samples=$((samples+1))
            note "state=$st delta=$d frames/s"
            # Negative => the stream was torn down and restarted. That is the
            # signature of the J-5 failure: it never got past ~3000 frames.
            [ "$d" -lt 0 ] && restarts=$((restarts+1))
            # Allow generous slack: this is a "is it moving at all" test, not a
            # drift measurement, and the sleep itself is not precise.
            [ "$d" -ge 0 ] && [ "$d" -lt 40000 ] && slow=$((slow+1))
        fi
        prev=${hp:-}
        sleep 1
    done
    if [ "$restarts" -gt 0 ]; then
        bad "playback stream RESTARTED $restarts time(s) in ${samples}s (negative hw_ptr delta) — the J-5 dropout signature"
    elif [ "$slow" -gt 0 ]; then
        bad "hw_ptr advanced under 40k frames/s on $slow of $samples samples — not keeping realtime"
    elif [ "$samples" -gt 0 ]; then
        ok "clocking at realtime across $samples samples, zero restarts"
    else
        warn "could not read hw_ptr"
    fi
fi
echo

# ---------------------------------------------------------------------------
# 4. THE PRUNE IS STILL IN EFFECT
#
# These nodes are disabled in config/wireplumber/53-rig-graph.conf. If they are
# back, either the symlink broke or someone deleted the file — and the 2.1M-xrun
# condition is back with them.
# ---------------------------------------------------------------------------
echo "4. the graph prune is still in effect"
if command -v pactl >/dev/null 2>&1; then
    SINKS=$(pactl list short sinks 2>/dev/null)
    SRCS=$(pactl list short sources 2>/dev/null)
    HDMI=$(printf '%s\n' "$SINKS" | grep -ci 'HDMI' || true)
    HEAD=$(printf '%s\n' "$SRCS"  | grep -ci 'Headset__source' || true)
    [ "$HDMI" -eq 0 ] && ok "no HDMI audio sinks (704k xruns, 2.8ms BUSY/cycle when present)" \
                      || bad "$HDMI HDMI sink(s) are back — 53-rig-graph.conf is not being applied"
    [ "$HEAD" -eq 0 ] && ok "no SOF headset mic source (1.4M xruns, and it wired itself into d1)" \
                      || bad "the SOF headset source is back — this is the node that fed d1"
    note "$(printf '%s\n' "$SINKS" | wc -l) sink(s) total"
else
    warn "pactl not found"
fi
# The symlinks are the reason the config survives a fresh checkout; a broken one
# is silent until the next wireplumber restart, which is the worst time to learn.
WPD="$HOME/.config/wireplumber/wireplumber.conf.d"
brk=0
for f in "$WPD"/*.conf; do
    [ -e "$f" ] || continue
    if [ -L "$f" ] && [ ! -e "$(readlink -f "$f")" ]; then
        bad "dangling symlink: $(basename "$f") -> $(readlink "$f")"; brk=1
    fi
done
[ "$brk" = 0 ] && ok "no dangling wireplumber symlinks"
echo

# ---------------------------------------------------------------------------
# 5. HEADROOM — a WARNING, never a failure
#
# The rig has never had a headroom budget; that is the whole lesson of the J-5
# dropout. But load is spiky and this must not block a gate over a transient, so
# it warns. `pulsar` gets named explicitly because it is the known offender (#7,
# measured at 82% IDLE, and 93% during the incident).
# ---------------------------------------------------------------------------
echo "5. headroom (advisory)"
LOAD=$(awk '{print $1}' /proc/loadavg)
CORES=$(nproc 2>/dev/null || echo 4)
note "load1=$LOAD over $CORES cores"
if awk -v l="$LOAD" -v c="$CORES" 'BEGIN{exit !(l > c*0.75)}'; then
    warn "load1 $LOAD is high for $CORES cores — the UMC's deadline is the first thing to miss"
else
    ok "load1 $LOAD on $CORES cores"
fi
PULSAR=$(ps -eo pcpu,comm --no-headers 2>/dev/null | awk '$2=="pulsar"{s+=$1} END{printf "%.0f", s+0}')
if [ "${PULSAR:-0}" -ge 60 ]; then
    warn "pulsar at ${PULSAR}% — #7 (highlighter marker pool). This is the JITTER SOURCE; a Window:Reload is the stopgap"
else
    ok "pulsar at ${PULSAR:-0}%"
fi
echo

if [ "$FAIL" -gt 0 ]; then
    echo "check-audio-graph: $FAIL failure(s), $WARN warning(s)"
    exit 1
fi
echo "check-audio-graph: OK${WARN:+ ($WARN warning(s))}"
