Commit 7a88f1d8 by PLN (Algolia)

check-mix: audit the Mix capture track, and say how to add one

Master is a bus. It has no rec-enable, so Ardour can never record it, so an
unarmed orbit has nothing to fall back to — the whole cost of the 24th. The
answer inside Ardour is a `Mix` TRACK fed from Master/audio_out 1+2, armed:
stem grade, same take, same sample clock as the orbits.

`--mix` audits it, and exits 4 when there is no such route, because absence
is a choice and the gate must tell "you have not built the net" apart from
"your net is broken". Absent prints the 30-second Add Track recipe rather
than nagging. Once it exists, four things make it a net that looks present
and records nothing, and they are checked in the order they bite:

  1. output wired back to Master — a unity-gain FEEDBACK LOOP through the PA,
     which is the state you get by following Ardour's Add Track dialog and
     stopping, since a new track goes to Master by default;
  2. not armed — the Tidal 08 failure one route further out;
  3. input not on Master, or only half on it — one channel the mix and the
     other an orbit reads as a healthy file and is unusable;
  4. fader off unity while the route is DiskIOPostFader — the fader is IN the
     recording, and at -inf the file is digital silence.

Two gate probes, split the way the gig-record pair is: "mix track" ADVISES
that there is none, "mix track wired" BLOCKS on one that is broken.

Ten new tests. The mutated fixtures each break exactly one thing, so a
failure names the fault; ExtConnection is asserted NOT to count as an
in-session link, or a correctly wired track would report a feedback loop.
parent 66318b6e
......@@ -158,6 +158,36 @@ PROBES: tuple[Probe, ...] = (
"Master is a BUS and cannot be armed — for the mix itself the "
"net is tools/gig_record.sh, or a Mix track fed from Master."),
# THE MIX ITSELF HAS NO OWNER INSIDE ARDOUR. `Master` is a bus — no
# rec-enable at all — so when `Tidal 08` sat unarmed on 2026-09-24 there
# was nothing to fall back to and the orbit is simply gone. Two nets answer
# that and they fail differently: `tools/gig_record.sh` is OUTSIDE Ardour
# (survives a forgotten REC and a crash; opus off a sink monitor) and a
# `Mix` track is INSIDE it (stem grade, same take, same clock; shares
# Ardour's REC button). Complementary, not redundant.
#
# ADVISE, because not having built it is a choice and a gate that says
# NO-GO over a missing option is a gate that gets ignored.
Probe("mix track", shell(f"""
out=$({PY} tools/check-mix.py --mix --quiet 2>&1); rc=$?
[ "$rc" = 4 ] && {{ echo "$out"; exit 1; }} # absent — this probe asks that
exit 0"""),
kind=ADVISE,
fix="optional, and PLN's call: Ardour > Track > Add Track, stereo, "
"named `Mix`, input on Master/audio_out 1+2, output DISCONNECTED "
"(a new track goes to Master by default and that is a feedback "
"loop), armed, Ctrl+S. tools/check-mix.py --mix prints the steps."),
# BLOCK, once it exists. A net that looks present and records silence is
# worse than no net, because you stop carrying the other one. Same split as
# the gig-record pair below: one probe per question, one fix per probe.
Probe("mix track wired", shell(f"""
out=$({PY} tools/check-mix.py --mix --quiet 2>&1); rc=$?
[ "$rc" = 4 ] && exit 0 # absent — the probe above owns that
[ "$rc" = 0 ] || {{ echo "$out"; exit "$rc"; }}"""),
fix="tools/check-mix.py --mix names the fault. Then Ctrl+S: this "
"reads the session as last SAVED."),
# Added after a dropout NOTHING else in the gate could have caught: every
# component was healthy and the fault was in the graph around them. It also
# catches the laptop microphone that had wired itself into the d1 stem —
......
......@@ -111,3 +111,112 @@ def test_an_unparseable_value_is_unknown_not_armed(raw):
claiming armed. Unknown reads as `None` and the gate names it."""
r = route(f'<Route name="Tidal 02"><Controllable name="rec-enable" value="{raw}"/></Route>')
assert cm.arm_of(r)[0] is None
# --------------------------------------------------------------------------- #
# The `Mix` capture track (2026-09-25).
#
# Master is a BUS: no `rec-enable`, so Ardour can never record it, so an
# unarmed orbit has nothing to fall back to — the whole cost of the 24th. A
# `Mix` TRACK fed from `Master/audio_out 1+2` closes that, and can be present
# and useless in four ways. Each shape below is one of them.
#
# The order of the assertions matters: the feedback loop is first because it is
# the only entry here that can hurt a ROOM rather than a take.
# --------------------------------------------------------------------------- #
MIX_GOOD = '''
<Route name="Mix" default-type="audio" audio-playlist="900" disk-io-point="DiskIOPostFader">
<IO name="Mix" direction="Input" default-type="audio">
<Port name="Mix/audio_in 1" type="audio" direction="Input">
<Connection other="Master/audio_out 1"/>
</Port>
<Port name="Mix/audio_in 2" type="audio" direction="Input">
<Connection other="Master/audio_out 2"/>
</Port>
</IO>
<IO name="Mix" direction="Output" default-type="audio">
<Port name="Mix/audio_out 1" type="audio" direction="Output">
<ExtConnection for="JACK"/>
</Port>
</IO>
<Controllable name="rec-enable" value="1"/>
<Controllable name="rec-safe" value="0"/>
<Processor name="Amp" type="amp">
<Controllable name="gaincontrol" value="1"/>
</Processor>
</Route>'''
def _mix(**edits: str) -> ET.Element:
"""MIX_GOOD with one thing broken. One knob per test, so a failure names
the fault instead of 'the fixture'."""
xml = MIX_GOOD
for old, new in edits.items():
xml = xml.replace(old.replace("__", " "), new)
return route(xml)
def test_a_correct_mix_track_has_no_problems():
problems, facts = cm.mix_audit(route(MIX_GOOD))
assert problems == []
assert any("armed yes" in f for f in facts)
def test_an_output_wired_back_to_master_is_a_feedback_loop():
"""The dangerous one. Ardour wires a NEW track to Master by default, so
this is the state you get by following the Add Track dialog and stopping."""
r = _mix(**{'<ExtConnection__for="JACK"/>': '<Connection other="Master/audio_in 1"/>'})
problems, _ = cm.mix_audit(r)
assert any("FEEDBACK" in p for p in problems)
def test_an_unarmed_mix_track_is_a_problem():
"""The Tidal 08 failure, one route further out."""
r = _mix(**{'name="rec-enable"__value="1"': 'name="rec-enable" value="0"'})
problems, _ = cm.mix_audit(r)
assert any("NOT ARMED" in p for p in problems)
def test_an_input_not_on_master_records_the_wrong_thing():
r = route(MIX_GOOD.replace("Master/audio_out", "Tidal 01/audio_out"))
problems, _ = cm.mix_audit(r)
assert any("not on Master/audio_out" in p for p in problems)
def test_half_the_input_on_master_is_its_own_failure():
"""One channel the mix, the other an orbit: the file looks healthy and is
unusable. Caught because 'at least one Master link' is not good enough."""
r = _mix(**{'other="Master/audio_out__1"': 'other="Tidal 01/audio_out 1"'})
problems, _ = cm.mix_audit(r)
assert any("PARTLY on Master" in p for p in problems)
def test_a_closed_fader_on_a_post_fader_disk_feed_records_silence():
"""`disk-io-point="DiskIOPostFader"` puts the fader IN the recording, so a
Mix track at -inf writes a healthy-looking file full of digital silence."""
r = _mix(**{'name="gaincontrol"__value="1"': 'name="gaincontrol" value="0"'})
problems, _ = cm.mix_audit(r)
assert any("DiskIOPostFader" in p for p in problems)
assert any("silence" in p for p in problems)
def test_a_pre_fader_disk_feed_does_not_care_about_the_fader():
"""The reason the check is conditional rather than always-on: pre-fader,
a closed fader is a monitoring choice and the file is still the mix."""
r = _mix(**{'disk-io-point="DiskIOPostFader"': 'disk-io-point="DiskIOPreFader"',
'name="gaincontrol"__value="1"': 'name="gaincontrol" value="0"'})
problems, _ = cm.mix_audit(r)
assert not any("fader" in p for p in problems)
def test_a_muted_mix_track_is_a_problem():
r = route(MIX_GOOD.replace("</Route>", '<MuteMaster muted="1"/></Route>'))
problems, _ = cm.mix_audit(r)
assert any("MUTED" in p for p in problems)
def test_ext_connections_are_not_read_as_in_session_links():
"""`<ExtConnection>` is the outside world. Counting it as a Master link
would report a feedback loop on a correctly wired track."""
assert cm.io_targets(route(MIX_GOOD), "Output") == []
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