feat(matrix): live variant generator
Add generate_live_variant_sc() to produce 4 deterministic live
variants per style (112 files). Each variant differs by:
- pattern family (VARIANT_FAMILIES per style group)
- era instrument kit overlay (house/M1, acid/hoover, virus/nord,
raw 909/rave) merged from VARIANT_ERA_KITS
- busy-block recolour config (VARIANT_BUSY, 4 distinct profiles)
Also: fix generator defaults to match live state:
- globalActions: fill/doubleTime/washReverb/muteDrums/relanceEvolve/
halfTime/next/muteMelo (was fill/drop/breakdown/evolve/...)
- hand-mod depths 0.70 (was 0.40/0.50 for rHandX/rHandY/lHandX/lHandY)
- INST_CHOICES synced with matrix.scd ~matInstChoices (10 new vintage
synths: tr909_snare hoover m1_piano m1_organ dx7_epiano brass_stab
nord_lead virus_saw rave_stab reese_juno)
Update test_global_actions.py to expect new 8-action mapping.
Add test_live_variants.py: 112 files, legal instruments, distinct
variants, determinism.
This commit is contained in:
@@ -32,27 +32,29 @@ VI = {v: i for i, v in enumerate(VOICES)}
|
||||
BARS = 64
|
||||
|
||||
# --- per-voice valid instrument choices (mirror of matrix.scd ~matInstChoices) ---
|
||||
# Includes 10 vintage SynthDefs landed in commits a6556e8+876afc0:
|
||||
# tr909_snare hoover m1_piano m1_organ dx7_epiano brass_stab nord_lead virus_saw rave_stab reese_juno
|
||||
INST_CHOICES = {
|
||||
"kick": ["lp_kick", "tr909_kick", "tr808_kick", "sp1200", "do_kick", "do_quake_sub"],
|
||||
"hats": ["lp_hat", "tr909_hat", "tr808_hat", "sp1200", "do_hat"],
|
||||
"clap": ["lp_clap", "tr909_clap", "tr808_clap", "sp1200", "lp_rim"],
|
||||
"clap": ["lp_clap", "tr909_clap", "tr909_snare", "tr808_clap", "sp1200", "lp_rim"],
|
||||
"perc": ["lp_perc", "tr808_cowbell", "sp1200", "akai_s", "do_strike"],
|
||||
"sub": ["lp_sub", "sh101", "minimoog", "system100", "lp_reese"],
|
||||
"sub": ["lp_sub", "sh101", "minimoog", "system100", "lp_reese", "m1_organ", "reese_juno"],
|
||||
"acid": ["lp_acid", "tb303", "ms20", "system100", "lp_pluck"],
|
||||
"arp": ["lp_pluck", "tb303", "supersaw", "juno", "do_strike"],
|
||||
"lead": ["lp_pluck", "ms20", "supersaw", "do_plane", "sh101"],
|
||||
"stab": ["lp_pluck", "ms20", "supersaw", "tb303", "lp_acid"],
|
||||
"pad": ["lp_pad", "juno", "supersaw", "ms20", "lp_reese"],
|
||||
"arp": ["lp_pluck", "tb303", "supersaw", "juno", "do_strike", "nord_lead", "virus_saw"],
|
||||
"lead": ["lp_pluck", "ms20", "supersaw", "do_plane", "sh101", "hoover", "virus_saw"],
|
||||
"stab": ["lp_pluck", "ms20", "supersaw", "tb303", "lp_acid", "brass_stab", "rave_stab"],
|
||||
"pad": ["lp_pad", "juno", "supersaw", "ms20", "lp_reese", "reese_juno"],
|
||||
"ride": ["lp_ride", "tr909_ride", "lp_hat", "do_hat"],
|
||||
"rim": ["lp_rim", "tr808_cowbell", "tr909_clap", "sp1200"],
|
||||
"tom": ["lp_tom", "sp1200", "do_strike", "do_quake_sub"],
|
||||
"reese": ["lp_reese", "ms20", "minimoog", "system100", "supersaw"],
|
||||
"bells": ["lp_bells", "dx7", "akai_s", "do_plane"],
|
||||
"reese": ["lp_reese", "ms20", "minimoog", "system100", "supersaw", "hoover", "reese_juno"],
|
||||
"bells": ["lp_bells", "dx7", "akai_s", "do_plane", "dx7_epiano"],
|
||||
"sweep": ["lp_sweep", "akai_s", "lp_pad", "lp_reese"],
|
||||
"melody": ["lp_pluck", "supersaw", "ms20", "sh101", "dx7"],
|
||||
"chord": ["lp_pad", "juno", "supersaw", "ms20"],
|
||||
"melody": ["lp_pluck", "supersaw", "ms20", "sh101", "dx7", "dx7_epiano", "nord_lead"],
|
||||
"chord": ["lp_pad", "juno", "supersaw", "ms20", "m1_piano", "m1_organ", "dx7_epiano"],
|
||||
"fx": ["lp_fx", "akai_s", "lp_sweep"],
|
||||
"snare": ["lp_snare", "tr909_clap", "tr808_clap", "lp_clap"],
|
||||
"snare": ["lp_snare", "tr909_snare", "tr909_clap", "tr808_clap", "lp_clap"],
|
||||
"crash": ["lp_crash", "tr909_ride", "akai_s"],
|
||||
"shaker": ["lp_shaker", "tr909_hat", "tr808_hat"],
|
||||
}
|
||||
@@ -579,9 +581,9 @@ for v in ("sub", "acid", "arp", "lead", "stab", "pad", "reese", "bells", "perc",
|
||||
# Per-role default bindings (source, target, depth). Soloists -> hand sources
|
||||
# (expressive, close-up); energy/structure -> body sources (robust at distance).
|
||||
_ROLE_MODS = {
|
||||
"lead": [("rHandX", "amp", 0.4), ("rHandY", "pitch", 0.4), ("lHandY", "cutoff", 0.5), ("lHandX", "res", 0.4)],
|
||||
"bells": [("rHandX", "amp", 0.4), ("rHandY", "pitch", 0.4), ("lHandX", "res", 0.4)],
|
||||
"melody": [("rHandX", "amp", 0.4), ("lHandY", "cutoff", 0.5), ("lHandX", "res", 0.4)],
|
||||
"lead": [("rHandX", "amp", 0.70), ("rHandY", "pitch", 0.70), ("lHandY", "cutoff", 0.70), ("lHandX", "res", 0.70)],
|
||||
"bells": [("rHandX", "amp", 0.70), ("rHandY", "pitch", 0.70), ("lHandX", "res", 0.70)],
|
||||
"melody": [("rHandX", "amp", 0.70), ("lHandY", "cutoff", 0.70), ("lHandX", "res", 0.70)],
|
||||
"reese": [("bodyVitesse", "cutoff", 0.5), ("bodyY", "pitch", 0.3), ("limbSpan", "res", 0.4), ("bodyX", "pan", 0.3)],
|
||||
"sub": [("bodyVitesse", "res", 0.4), ("bodyY", "pitch", 0.3), ("bodyX", "pan", 0.3)],
|
||||
"pad": [("bodyVitesse", "cutoff", 0.4), ("limbSpan", "rev", 0.4), ("bodyX", "pan", 0.3)],
|
||||
@@ -1148,8 +1150,8 @@ def to_sc(name, arc_fn, instkit, melodies, extra, post=None):
|
||||
"'colorDefs': [\n%s\n],\n"
|
||||
"'voiceMods': [ %s ],\n"
|
||||
"'voicePoses': [ %s ],\n"
|
||||
"'globalActions': [ \\fill, \\drop, \\breakdown, \\evolve, "
|
||||
"\\muteDrums, \\muteMelo, \\washReverb, \\next ],\n"
|
||||
"'globalActions': [ \\fill, \\doubleTime, \\washReverb, "
|
||||
"\\muteDrums, \\relanceEvolve, \\halfTime, \\next, \\muteMelo ],\n"
|
||||
"'globalSettings': ( 'transientBars': 1, "
|
||||
"'breakdownKeep': [ \\kick, \\sub ] ),\n"
|
||||
"'pinchHoldMs': 300,\n"
|
||||
@@ -1157,6 +1159,137 @@ def to_sc(name, arc_fn, instkit, melodies, extra, post=None):
|
||||
% (rows, isyms, cds, vms, vps))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Live variant generation (4 variants per style = 112 new files)
|
||||
# ============================================================================
|
||||
import patterns as _pats
|
||||
|
||||
# Era kit overlays per style group: 4 eras (house/M1, acid/hoover, virus/nord, rave/909).
|
||||
# Each entry overrides selected voices in the base preset kit.
|
||||
# All instruments validated against INST_CHOICES above (mirrors matrix.scd ~matInstChoices).
|
||||
_VARIANT_ERA_KITS = {
|
||||
"techno": [
|
||||
{"chord": "m1_organ", "stab": "brass_stab", "bells": "dx7_epiano"}, # v1 house/M1
|
||||
{"acid": "tb303", "lead": "hoover", "reese": "hoover"}, # v2 acid/hoover
|
||||
{"lead": "virus_saw", "arp": "virus_saw", "stab": "rave_stab"}, # v3 virus/nord
|
||||
{"kick": "tr909_kick", "snare": "tr909_snare", "stab": "brass_stab"}, # v4 909/rave
|
||||
],
|
||||
"psy": [
|
||||
{"pad": "reese_juno", "chord": "m1_organ", "bells": "dx7_epiano"}, # v1 house/M1
|
||||
{"acid": "tb303", "arp": "tb303", "sub": "system100"}, # v2 acid
|
||||
{"lead": "virus_saw", "arp": "nord_lead", "reese": "reese_juno"}, # v3 virus/nord
|
||||
{"kick": "tr909_kick", "snare": "tr909_snare"}, # v4 909/rave
|
||||
],
|
||||
"trance": [
|
||||
{"chord": "m1_piano", "melody": "dx7_epiano", "bells": "dx7_epiano"}, # v1 house/M1
|
||||
{"arp": "tb303", "sub": "system100", "lead": "ms20"}, # v2 acid/303
|
||||
{"lead": "virus_saw", "arp": "nord_lead", "melody": "nord_lead"}, # v3 virus/nord
|
||||
{"kick": "tr909_kick", "clap": "tr909_snare", "snare": "tr909_snare"}, # v4 909/rave
|
||||
],
|
||||
"breaks": [
|
||||
{"pad": "juno", "chord": "m1_organ", "bells": "dx7_epiano"}, # v1 house/M1
|
||||
{"reese": "hoover", "stab": "tb303", "sub": "system100"}, # v2 acid/hoover
|
||||
{"reese": "reese_juno", "lead": "virus_saw", "pad": "reese_juno"}, # v3 virus/nord
|
||||
{"kick": "sp1200", "snare": "tr909_snare", "stab": "rave_stab"}, # v4 909/rave
|
||||
],
|
||||
"electro": [
|
||||
{"melody": "dx7_epiano", "chord": "m1_piano", "stab": "brass_stab"}, # v1 house/M1
|
||||
{"acid": "tb303", "sub": "system100"}, # v2 acid/303
|
||||
{"lead": "virus_saw", "stab": "nord_lead", "melody": "nord_lead"}, # v3 virus/nord
|
||||
{"kick": "tr808_kick", "clap": "tr808_clap", "snare": "tr909_snare"}, # v4 808/rave
|
||||
],
|
||||
"industrial": [
|
||||
{"stab": "brass_stab", "reese": "reese_juno"}, # v1 (no M1 piano)
|
||||
{"acid": "tb303", "sub": "system100", "reese": "hoover"}, # v2 acid/hoover
|
||||
{"stab": "rave_stab", "reese": "reese_juno", "lead": "virus_saw"}, # v3 virus/nord
|
||||
{"kick": "tr909_kick", "clap": "tr909_snare", "snare": "tr909_snare"}, # v4 909/rave
|
||||
],
|
||||
"ambient": [
|
||||
{"pad": "reese_juno", "bells": "dx7_epiano", "chord": "m1_piano"}, # v1 M1/DX7
|
||||
{"pad": "juno", "sub": "system100", "melody": "ms20"}, # v2 analog
|
||||
{"pad": "reese_juno", "melody": "nord_lead", "bells": "dx7_epiano"}, # v3 nord/reese
|
||||
{"pad": "juno", "melody": "dx7_epiano", "chord": "m1_organ"}, # v4 M1 organ
|
||||
],
|
||||
}
|
||||
|
||||
# Pattern families per style group x variant (drum_family, mel_family).
|
||||
# Pairs must be valid or will silently fall back to 'default' in patterns_for().
|
||||
_VARIANT_FAMILIES = {
|
||||
"techno": [("techno", "house"), ("techno", "psy"), ("electro", "default"), ("industrial", "default")],
|
||||
"psy": [("techno", "psy"), ("techno", "dub"), ("breaks", "psy"), ("industrial", "psy")],
|
||||
"trance": [("techno", "house"), ("techno", "psy"), ("electro", "house"), ("breaks", "house")],
|
||||
"breaks": [("breaks", "default"), ("dnb", "default"), ("footwork", "default"), ("breaks", "psy")],
|
||||
"electro": [("electro", "house"), ("ebm", "house"), ("electro", "default"), ("industrial", "default")],
|
||||
"industrial": [("industrial", "default"), ("electro", "default"), ("breaks", "default"), ("ebm", "default")],
|
||||
"ambient": [("ambient", "ambient"), ("techno", "ambient"), ("ambient", "dub"), ("ambient", "house")],
|
||||
}
|
||||
|
||||
# Busy-block profiles for each variant (index 0-3 = v1-v4).
|
||||
# busy_blocks: which 16-bar blocks get the recolour pass; busy_color: colour applied.
|
||||
_VARIANT_BUSY = [
|
||||
{"busy_blocks": [0, 2], "busy_color": 3}, # v1 octave-up on blocks 0 & 2
|
||||
{"busy_blocks": [1, 3], "busy_color": 2}, # v2 double-time on blocks 1 & 3
|
||||
{"busy_blocks": [0, 3], "busy_color": 3}, # v3 bookend blocks
|
||||
{"busy_blocks": [2, 3], "busy_color": 6}, # v4 accent-roll second half
|
||||
]
|
||||
|
||||
|
||||
def _make_live_continuous_v(g, style, busy_cfg):
|
||||
"""Like make_live_continuous but with a variant-specific busy configuration."""
|
||||
ps, pn = SECTIONS["P"]
|
||||
for row in g:
|
||||
plateau = row[ps:ps + pn]
|
||||
for b in range(len(row)):
|
||||
row[b] = plateau[(b - ps) % pn]
|
||||
prof = LIVE_VAR.get(style, LIVE_VAR["techno"])
|
||||
for blk in busy_cfg["busy_blocks"]:
|
||||
for v in prof["busy"]:
|
||||
vi = VI[v]
|
||||
for b in range(blk * 16, blk * 16 + 16):
|
||||
if g[vi][b] > 0:
|
||||
g[vi][b] = busy_cfg["busy_color"]
|
||||
for blk in range(BARS // 16):
|
||||
for v in prof["fill"]:
|
||||
vi = VI[v]
|
||||
for b in (blk * 16 + 14, blk * 16 + 15):
|
||||
if g[vi][b] > 0:
|
||||
g[vi][b] = 6
|
||||
return g
|
||||
|
||||
|
||||
def generate_live_variant_sc(name, variant_idx, m):
|
||||
"""Build SC string for one live variant file.
|
||||
|
||||
variant_idx: 1-4. Deterministic: outcome depends only on name + variant_idx.
|
||||
"""
|
||||
style_group = LIVE_STYLE.get(name, "techno")
|
||||
vi = variant_idx - 1 # 0-3
|
||||
|
||||
# Pattern family for this variant
|
||||
fam = _VARIANT_FAMILIES.get(style_group, _VARIANT_FAMILIES["techno"])[vi]
|
||||
|
||||
# Build merged kit: base preset kit + era overlay
|
||||
era_kit = _VARIANT_ERA_KITS.get(style_group, _VARIANT_ERA_KITS["techno"])[vi]
|
||||
merged_kit = dict(m["instkit"])
|
||||
for voice, inst in era_kit.items():
|
||||
if voice in INST_CHOICES and inst in INST_CHOICES[voice]:
|
||||
merged_kit[voice] = inst
|
||||
|
||||
# Busy config for the live variation pass
|
||||
busy_cfg = _VARIANT_BUSY[vi]
|
||||
|
||||
# Register variant name in patterns.PRESET_FAMILY so emit_colordefs resolves correctly
|
||||
vname = "%s_live-%d" % (name, variant_idx)
|
||||
_pats.PRESET_FAMILY[vname] = fam
|
||||
try:
|
||||
post = (lambda s, bc: (lambda gg: _make_live_continuous_v(gg, s, bc)))(
|
||||
style_group, busy_cfg)
|
||||
sc = to_sc(vname, m["live"], merged_kit, m["melodies"], m["extra"], post)
|
||||
finally:
|
||||
del _pats.PRESET_FAMILY[vname]
|
||||
return sc
|
||||
|
||||
|
||||
def main():
|
||||
import os
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -1169,8 +1302,18 @@ def main():
|
||||
with open(os.path.join(here, "%s_%s.matrix" % (name, sec)), "w") as f:
|
||||
f.write(sc)
|
||||
n += 1
|
||||
# Generate the 4 live variants per style (112 new files)
|
||||
nv = 0
|
||||
for name, m in MORCEAUX.items():
|
||||
for vi in range(1, 5):
|
||||
sc = generate_live_variant_sc(name, vi, m)
|
||||
path = os.path.join(here, "%s_live-%d.matrix" % (name, vi))
|
||||
with open(path, "w") as f:
|
||||
f.write(sc)
|
||||
nv += 1
|
||||
print("wrote %d presets (%d morceaux x intro/live/outro, 22 voices x %d bars)"
|
||||
% (n, len(MORCEAUX), BARS))
|
||||
print("wrote %d live-variant presets (%d morceaux x 4 variants)" % (nv, len(MORCEAUX)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Every generated .matrix must carry the default globalActions mapping
|
||||
(slots: LH open=fill, LH closed=drop, RH open=evolve, RH closed=breakdown).
|
||||
(8 actions: fill doubleTime washReverb muteDrums relanceEvolve halfTime next muteMelo).
|
||||
Run: cd .../matrix_presets && uv run python test_global_actions.py"""
|
||||
import sys, os, glob
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import generate_presets as G
|
||||
|
||||
EXPECT = r"'globalActions': [ \fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next ]"
|
||||
EXPECT = r"'globalActions': [ \fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo ]"
|
||||
ok = True
|
||||
|
||||
def check(c, m):
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Validates the 112 live-variant files (*_live-[1-4].matrix).
|
||||
|
||||
Checks:
|
||||
1. Exactly 112 files exist (28 styles × 4 variants)
|
||||
2. All 28 styles have variant files 1-4
|
||||
3. globalActions field matches the 8-action mapping
|
||||
4. All instruments are legal for their voice
|
||||
5. Variants differ from each other (grid or instruments)
|
||||
6. Deterministic: regenerating produces identical bytes
|
||||
|
||||
Run: cd sound_algo/data_only/matrix_presets && uv run python test_live_variants.py
|
||||
"""
|
||||
import sys, os, glob, re
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import generate_presets as G
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
EXPECTED_ACTIONS = r"'globalActions': [ \fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo ]"
|
||||
EXPECTED_N_VARIANTS = 112
|
||||
|
||||
ok = True
|
||||
|
||||
|
||||
def check(c, m):
|
||||
global ok
|
||||
if not c:
|
||||
ok = False
|
||||
print("FAIL:", m)
|
||||
|
||||
|
||||
# 1. Exactly 112 files
|
||||
variant_files = glob.glob(os.path.join(here, "*_live-[1-4].matrix"))
|
||||
check(len(variant_files) == EXPECTED_N_VARIANTS,
|
||||
"expected %d variant files, got %d" % (EXPECTED_N_VARIANTS, len(variant_files)))
|
||||
|
||||
# 2. Every style has all 4 variants
|
||||
for name in G.PRESETS:
|
||||
for v in range(1, 5):
|
||||
fp = os.path.join(here, "%s_live-%d.matrix" % (name, v))
|
||||
check(os.path.exists(fp), "missing %s_live-%d.matrix" % (name, v))
|
||||
|
||||
# 3. globalActions field
|
||||
for fp in variant_files:
|
||||
content = open(fp, encoding="utf-8").read()
|
||||
check(EXPECTED_ACTIONS in content,
|
||||
"wrong globalActions in %s" % os.path.basename(fp))
|
||||
|
||||
# 4. All instruments legal per voice
|
||||
for fp in sorted(variant_files):
|
||||
content = open(fp, encoding="utf-8").read()
|
||||
m = re.search(r"'instruments':\s*\[\s*(.*?)\s*\]", content)
|
||||
if m:
|
||||
syms = re.findall(r"\\(\w+)", m.group(1))
|
||||
for vi, sym in enumerate(syms):
|
||||
voice = G.VOICES[vi]
|
||||
check(sym in G.INST_CHOICES[voice],
|
||||
"illegal instrument %r for voice %r in %s" % (sym, voice, os.path.basename(fp)))
|
||||
|
||||
# 5. Variants differ from each other
|
||||
for name in G.PRESETS:
|
||||
fps = [os.path.join(here, "%s_live-%d.matrix" % (name, v)) for v in range(1, 5)]
|
||||
contents = [open(fp, encoding="utf-8").read() for fp in fps if os.path.exists(fp)]
|
||||
check(len(set(contents)) > 1, "all variants identical for %s" % name)
|
||||
|
||||
# 6. Deterministic
|
||||
for name in list(G.PRESETS.keys())[:3]:
|
||||
m_data = G.MORCEAUX[name]
|
||||
for vi in range(1, 5):
|
||||
fp = os.path.join(here, "%s_live-%d.matrix" % (name, vi))
|
||||
if os.path.exists(fp):
|
||||
on_disk = open(fp, encoding="utf-8").read()
|
||||
regenerated = G.generate_live_variant_sc(name, vi, m_data)
|
||||
check(regenerated == on_disk,
|
||||
"determinism fail for %s_live-%d" % (name, vi))
|
||||
|
||||
print("TEST PASS" if ok else "TEST FAIL")
|
||||
sys.exit(0 if ok else 1)
|
||||
Reference in New Issue
Block a user