ROUTING learns stems the template does not name from the set (routing-learned.json); amber ones are fixed by hold/auto-fix from what the name says and earlier sets; START routes learned stems too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 12:49:03 -04:00
co-authored by Claude Opus 5.5
parent 9551e043a7
commit a4d5fbe1aa
4 changed files with 112 additions and 8 deletions
+66 -6
View File
@@ -819,6 +819,47 @@ def template_for(routing, name):
return list(fam.pop()) if len(fam) == 1 else None
# Stems the routing template does not name are learned from the set, as routed there (user, 2026-10-27: "where it's
# routed right now is correct ... build your knowledge"). Kept beside the code, not in config.json: the settings'
# template still wins, and a learned route is only ever asked about, never changed by a hold.
LEARNED_ROUTING = os.path.join(os.path.dirname(os.path.abspath(__file__)), "routing-learned.json")
def learned_routing():
try:
with open(LEARNED_ROUTING) as f:
return json.load(f)
except (OSError, ValueError):
return {}
def save_learned_routing(learned):
tmp = LEARNED_ROUTING + ".tmp"
with open(tmp, "w") as f:
json.dump(learned, f, indent=1, sort_keys=True)
os.replace(tmp, LEARNED_ROUTING)
# What a stem is, from its name (the workflow's routing table: keys sounds 5/6, guitars and regular bass 7/8, drums,
# percussion, aux drums, sub bass and FX 9/10, vocal effects, BGVs and choir 11/12), in this order: "VOX FX" is
# vocals, "SYNTH BASS" the low end, "SYNTH LOOP" keys, a bare "LOOP" drums. The output comes from the template's own
# entry for that kind of stem.
KINDS = (("BGVS", r"\b(VOX|VOCALS?|BGVS?|CHOIR|VOICES?|HARMON(Y|IES)|SINGERS?)\b"),
("SYNTH BASS", r"\b(SUB|SYNTH BASS|808)\b"),
("PIANO", r"\b(KEYS?|PIANO|SYNTHS?|ORGAN|PADS?|STRINGS?|ARPS?|BELLS?|RHODES|WURLI|PLUCKS?|CELLO|VIOLINS?|BRASS|HORNS?|ORCH\w*|CELESTE|GLOCK\w*)\b"),
("EG 1", r"\b(GTRS?|GUITARS?|AG|EG|ACOUSTIC|ELECTRIC|MANDOLIN|BANJO|BASS)\b"),
("PERC", r"\b(DRUMS?|PERC\w*|SHAKERS?|TAMB\w*|CYMBALS?|KICK|SNARE|HATS?|CLAPS?|LOOPS?|HITS?|IMPACTS?|RISERS?|FX|TOMS|TIMPANI)\b"))
def stem_kind(table, name):
"""Where a stem's kind goes ([type, channel]), by its name; None when the name says nothing."""
n = stem_name(name)
for rep_, pattern in KINDS:
if re.search(pattern, n) and rep_ in table:
return list(table[rep_])
return None
def mix_plan(rc, live):
"""(routing Line, ears Line, changes): what is routed away from the template, which song stems have
their in-ear send at -inf, and the changes that put both right."""
@@ -828,8 +869,10 @@ def mix_plan(rc, live):
line.fault(WARN, live["error"], "%s: AbletonOSC on the Tracks Mac did not answer" % live["error"])
return routing, ears, []
table = {stem_name(k): v for k, v in (rc.get("routing") or {}).items()}
learned = learned_routing()
learned_before = dict(learned)
skip = {norm(g) for g in rc.get("ears_skip_groups", [])}
changes, top, n_route, n_ears, new = {}, None, 0, 0, []
changes, top, n_route, n_ears = {}, None, 0, 0
for t in live.get("tracks") or []:
if t["group"] and not t["grouped"]:
top = t["name"]
@@ -847,16 +890,33 @@ def mix_plan(rc, live):
elif want:
n_route += 1
elif table and top and norm(top) not in skip:
new.append(t["name"])
# Not in the template: what earlier sets taught (learned) and what its name says (kind) decide;
# when they disagree the name wins, and a set that agrees with the name teaches it.
cur = [t["type"], t["channel"] or ""]
had, kind = template_for(learned, t["name"]), stem_kind(table, t["name"])
fix = kind if kind and cur != kind and had != cur else (had if had and not kind and cur != list(had) else None)
if fix:
why = ("its name says %s" % stem_name(t["name"]).split()[0]) if fix == kind else "earlier sets had it there"
changes.setdefault(t["index"], {"index": t["index"], "name": t["name"]}).update(type=fix[0], channel=fix[1])
routing.fault(WARN, "%s TO %s" % (short(stem_name(t["name"]), 12), (t["channel"] or t["type"]).upper()[:10]),
"%s (%s) goes to %s %s; it belongs at %s %s (%s): a hold moves it" % (
t["name"], top, t["type"], t["channel"], fix[0], fix[1], why))
else:
if had is None or list(had) != cur:
learned[stem_name(t["name"])] = cur
routing.notes.append("learned: %s goes to %s %s (%s)" % (stem_name(t["name"]), t["type"], t["channel"], top))
n_route += 1
if top and norm(top) not in skip and t.get("send") is not None:
if t["send"] <= 0.0:
changes.setdefault(t["index"], {"index": t["index"], "name": t["name"]})["send"] = 1.0
ears.fault(ERR, "EARS OFF " + short(top, 12), "%s in %s: send A is at -inf" % (t["name"], top))
else:
n_ears += 1
if new:
routing.fault(WARN, "NEW STEM " + short(stem_name(new[0]), 12), "not in the routing template (add them in the "
"settings): " + ", ".join(new[:12]))
if learned != learned_before:
try:
save_learned_routing(learned)
except OSError:
pass
routing.info = "%d ROUTED" % n_route if table else "NO TEMPLATE"
ears.info = "%d SENDS ON" % n_ears
if live.get("playing"):
@@ -2172,7 +2232,7 @@ def start_params(rc, services, mode):
return {"mode": mode, "projects": projects, "site_folder": rc.get("site_folder", ""),
"folder_format": rc.get("folder_format", "{date} {loc} Project"), "finished_folders": rc.get("finished_folders", []),
"pads_folder": rc.get("pads_folder") or os.path.join(os.path.dirname(projects.rstrip("/")), "PADS"),
"tune_library": rc.get("tune_library", ""), "routing": rc.get("routing") or {},
"tune_library": rc.get("tune_library", ""), "routing": dict(learned_routing(), **(rc.get("routing") or {})),
"keep_groups": rc.get("start_keep_groups") or rc.get("ears_skip_groups") or ["MIDI GROUP", "RESI TRACKS"],
"pad_track": rc.get("pad_track", "AMBIENT PADS"), "tune_track": rc.get("tune_track", "WAVES TUNE"),
"lighting_track": rc.get("lighting_track", "LIGHTING CUES"), "sections_track": rc.get("sections_track", "SECTIONS"),