Commit 83ced8fd by PLN (Algolia)

gig-record: the black box joins the inventory, and follows Master

tools/gig_record.sh has existed since OPAL, is item 9 of RUNBOOK.md's
checklist, and had never run once on this machine — no unit, no row in
rig_units.py, so nothing in the authored inventory knew it existed and no
converge asserted it. Precisely gig-log's own story, which is why
CosmicFest has no session log.

The cost this time: 2026-09-24 lost 17m44s off the head of a 94-minute
set (REC at 18:27:41, set from 18:09:57), so salut_nu and love_first
exist in no recording at all. Orbit 08 went with them for every track,
and Ardour cannot help — Master is a BUS, no playlist, no rec-enable, so
the DAW cannot record the mix even in principle. One stereo opus of the
master bus makes both losses survivable.

It no longer records the default sink. That sink was the laptop card on
the 24th while Ardour's Master had been hand-linked to the UMC, so the
net would only have caught the show because TODO_GIG step 4 was never
done — do that step and the black box records silence, which is the worst
failure a black box has, because it looks like it is working. It now
follows whatever sink ardour:Master/audio_out 1 is linked to, falls back
to the default when Ardour is closed, pins to GIG_RECORD_TARGET when told,
and says out loud which road it took.

on-demand, not login: ~72 MB/hour, so gig-up starts it and idle hours are
not recorded. PROOF stays None because 'pgrep -x ffmpeg' would be
satisfied by any stranger's transcode; its liveness is a recent segment
mtime, the same assertion the gig-log probe makes.

The probe ADVISES rather than blocks — a missing net does not stop a set.

Also: nothing checked that rig_units.SERVICES and rig-install.sh's
UNIT_SRC name the same units, which is the 'list lived in three places'
failure this module was written to end. Three tests now do, shrink-only
on perf-tray's $HOME-only unit. Verified they fail when the new row is
removed.
parent 187b17ce
......@@ -20,7 +20,7 @@ Each line pays off a `TODO_GIG` "NOT TONIGHT" entry or a loss measured in
never rebound; zero control moves logged for a 94-minute set. Probe now,
rebind as its own change. (`TODO_GIG`: "gig-log loses its MIDI address and
never looks for it again")
- [ ] **4. `gig_record.sh` is asserted by nothing.** No unit, no entry in
- [x] **4. `gig_record.sh` is asserted by nothing.** No unit, no entry in
`rig_units.py`, never started on this machine — the black-box net that
would have saved the missing 17m44s AND orbit 08. Caveat to settle first:
it records `$(pactl get-default-sink).monitor`, which on the 24th was the
......
......@@ -386,6 +386,30 @@ print(" ".join(str(p) for p in m.setlist_tracks()))')"""),
fix="systemctl --user restart gig-log # it is in rig_units now, so "
"--converge also starts it"),
# ---- the black box ----------------------------------------------------- #
# ADVISE, not BLOCK, and the line is deliberate: this is the net, not the
# act. A missing net does not stop a set, and the Ardour multitrack is still
# the archival copy. But 2026-09-24 is why it is here at all — REC was
# pressed at 18:27:41 and the set had started at 18:09:57, so salut_nu and
# love_first exist in no recording anywhere, and orbit 08 was never armed so
# its part of all fifteen tracks is gone with them. Both losses are
# survivable with one stereo opus of the master bus, and the script for it
# has existed since OPAL without ever running once on this machine.
#
# Same shape as the gig-log probe above, for the same reason: "the unit is
# green" is not "audio is landing". Assert the property — a segment file
# whose mtime is within the last 20 seconds.
Probe("gig-record capturing", shell(r"""
d="$PWD/recordings"
n=$(ls -t "$d"/*.opus 2>/dev/null | head -n1)
[ -n "$n" ] || exit 1
age=$(( $(date +%s) - $(stat -c%Y "$n") ))
[ "$age" -le 20 ] || exit 2"""),
kind=ADVISE,
fix="systemctl --user start gig-record (it is on-demand, so gig-up "
"--converge starts it; tools/gig_record.sh status says what it is "
"pointed at — it follows Ardour's Master, not the default sink)"),
# ---- the empirical gate, opt-in ---------------------------------------- #
# Last, loud, and only on request: it makes sound and takes ~45s per track.
# Booting the set to confirm what a typecheck just told you is ten wasted
......
[Unit]
Description=gig-record (ParVagues black box: always-on stereo master capture)
Documentation=file:///home/pln/Work/Sound/Tidal/tools/gig_record.sh
After=pipewire.service
Wants=pipewire.service
[Service]
Type=forking
WorkingDirectory=/home/pln/Work/Sound/Tidal
ExecStart=/usr/bin/bash /home/pln/Work/Sound/Tidal/tools/gig_record.sh start
ExecStop=/usr/bin/bash /home/pln/Work/Sound/Tidal/tools/gig_record.sh stop
PIDFile=/home/pln/Work/Sound/Tidal/recordings/.recorder.pid
Restart=on-failure
RestartSec=5
# Below every audio thread. A safety net that causes the fault it insures
# against is worse than no net — the same law gig-log.service is written to.
Nice=10
CPUWeight=20
IOWeight=20
[Install]
WantedBy=default.target
# ON-DEMAND, NOT LOGIN, and the difference is deliberate (rig_units.py owns the
# policy). This writes ~72 MB/hour of opus; enabling it at login would record
# every idle hour the laptop is open. gig-up asserts it, so it is running for
# anything that is a GIG and off the rest of the time.
#
# It exists at all because 2026-09-24 lost 17m44s off the head of a 94-minute
# set — REC was pressed at 18:27:41, the set started at 18:09:57 — and there was
# nothing else listening. The script has existed since OPAL and is item 9 of
# RUNBOOK.md's checklist, but it had no unit and no row in rig_units.py, so
# nothing in the authored inventory knew it existed and nothing ever asserted
# it. That is the same shape as gig-log's own absence, which is why CosmicFest
# has no session log.
......@@ -20,10 +20,45 @@ SEGMENT_SECS="${GIG_RECORD_SEGMENT:-3600}"
mkdir -p "$OUT_DIR"
# WHERE TO LISTEN, AND WHY IT IS NOT THE DEFAULT SINK (2026-09-25)
# This used to record `$(pactl get-default-sink).monitor` unconditionally. On
# 2026-09-24 that was the laptop card while Ardour's Master had been hand-linked
# to the UMC, and the net would only have caught the show because TODO_GIG step
# 4 ("cut the laptop speakers") was never done. Do that step and the safety net
# records silence — the worst possible failure for a black box, because it looks
# like it is working.
#
# So follow MASTER, not the default. Whatever sink `ardour:Master/audio_out 1`
# is linked to is the one the room is hearing, and its monitor is what we want.
# Fall back to the default sink when Ardour is closed (rehearsing dawless, or
# the fold-orbits headphones rig), and say out loud which road was taken —
# a black box that cannot tell you what it pointed at is not evidence.
master_sink() {
command -v pw-link >/dev/null 2>&1 || return 1
pw-link -l 2>/dev/null | awk '
/^ardour:Master\/audio_out 1$/ { inblk = 1; next }
/^[^ ]/ { inblk = 0 }
inblk && /\|->/ { sub(/^[ \t]*\|-> /, ""); sub(/:.*$/, ""); print; exit }
'
}
default_monitor() {
local sink
local sink target
target="${GIG_RECORD_TARGET:-}"
if [ -n "$target" ]; then
echo "gig_record: target pinned by GIG_RECORD_TARGET" >&2
echo "$target"
return 0
fi
sink="$(master_sink)"
if [ -n "$sink" ]; then
echo "gig_record: following ardour Master -> $sink" >&2
echo "${sink}.monitor"
return 0
fi
sink="$(pactl get-default-sink 2>/dev/null)"
[ -n "$sink" ] || { echo "gig_record: no default sink found" >&2; return 1; }
echo "gig_record: Ardour Master not in the graph, falling back to the default sink" >&2
echo "${sink}.monitor"
}
......
......@@ -457,6 +457,7 @@ declare -A UNIT_SRC=(
[gig-log]="$REPO_ROOT/tools/gig-log.service"
[perf-tray]="$REPO_ROOT/perf-tray.service"
[midiviz]="$REPO_ROOT/tools/midiviz.service"
[gig-record]="$REPO_ROOT/tools/gig-record.service"
)
created_any=0
......
......@@ -103,6 +103,29 @@ SERVICES = [
# app id (see the setDesktopFileName call in midiviz.py -- without it the app
# id was "python3" and a rule would have matched every Python GUI).
("midiviz", "MIDI lens", None, "login", "the surface, watched (glyph-rain + CC numbers)"),
# Added 2026-09-25, and the absence is measured: the 2026-09-24 set lost
# 17m44s off its head because REC was pressed at 18:27:41 and the set had
# started at 18:09:57 — two whole tracks that exist in no recording. The
# script had existed since OPAL and is item 9 of RUNBOOK.md's checklist,
# but with no unit and no row here nothing asserted it, so it had never
# once run on this machine. Exactly gig-log's own story, told again.
#
# "on-demand", NOT "login": it writes ~72 MB/hour of opus, so enabling it
# at boot would record every idle hour the lid is open. gig-up's converge
# starts it, which means it is running for anything that is a gig.
#
# It is NOT a replacement for the Ardour multitrack — one stereo opus of
# the master bus is a black box, not an archival stem set. It is what makes
# a forgotten REC survivable instead of fatal, and it is also the only way
# to capture the mix at all: Ardour's Master is a BUS, with no playlist and
# no rec-enable, so the DAW cannot record it.
# PROOF is None on purpose, and the reason is a rule this rig holds hard:
# never identify a process by name. `pgrep -x ffmpeg` would match any
# ffmpeg on the box — a video transcode, a render — and report the black
# box as running when it is not. A proof that can be satisfied by a
# stranger is worse than no proof. Its liveness is asserted the way
# gig-log's is: by a segment file whose mtime is recent.
("gig-record", "black box", None, "on-demand", "stereo master capture, so a take is never lost"),
]
TARGET = "parvagues-rig.target"
......
......@@ -109,3 +109,63 @@ def test_the_age_reads_as_a_human_would_say_it(behind, expect, capsys):
started="2026-09-22 20:12:55", newest="/x/y.py",
edited="2026-09-23 09:58:36", behind_s=behind)])
assert expect in capsys.readouterr().out
# ── the inventory and the installer must name the same units ───────────────
# This module's own docstring names the failure: "the list lived in three
# places". rig_units.SERVICES is the authored inventory, rig-install.sh's
# UNIT_SRC says where an ABSENT unit's file comes from, and nothing checked
# that they agree. A unit in the inventory with no source is a unit that can
# never be installed on a fresh box — and a unit with no inventory row is one
# nothing ever asserts, which is how CosmicFest lost its session log and how
# 2026-09-24 lost 17m44s to a black box that had never run.
def _installer_units():
import pathlib
import re
src = (pathlib.Path(__file__).resolve().parents[1] / "rig-install.sh").read_text()
block = src[src.index("declare -A UNIT_SRC=("):]
block = block[:block.index("\n)")]
return dict(re.findall(r'\[([a-z0-9-]+)\]="([^"]+)"', block))
# perf-tray's unit file lives only in $HOME, not the repo — recorded as debt in
# rig_units.py itself. Shrink-only: paying it off means deleting this line.
UNIT_SRC_DEBT = {"perf-tray"}
def test_every_inventoried_unit_has_an_installer_source():
import sys
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1]))
import rig_units
have = _installer_units()
missing = [u for u in rig_units.UNITS if u not in have]
assert not missing, (
f"in rig_units.SERVICES but not in rig-install.sh's UNIT_SRC: {missing} — "
"on a fresh box these units have no file, so nothing can start them")
def test_every_installer_source_is_inventoried():
import sys
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1]))
import rig_units
extra = [u for u in _installer_units() if u not in rig_units.UNITS]
assert not extra, (
f"installed by rig-install.sh but absent from rig_units.SERVICES: {extra} — "
"nothing in the authored inventory knows these exist, so no converge "
"asserts them")
def test_the_repo_actually_contains_each_unit_file():
import pathlib
root = pathlib.Path(__file__).resolve().parents[2]
import sys
sys.path.insert(0, str(root / "tools"))
absent = []
for unit, src in _installer_units().items():
if unit in UNIT_SRC_DEBT:
continue
p = pathlib.Path(src.replace("$REPO_ROOT", str(root)))
if not p.is_file():
absent.append(f"{unit} -> {p}")
assert not absent, f"UNIT_SRC points at files this checkout does not have: {absent}"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment