feat: pattern-based generators from real track analysis, RPP structure fixes, randomization

- Reverse-engineer drum patterns from 2 real reggaeton tracks with librosa
- Create patterns.py with extracted frequency data (kick/snare/hihat positions)
- Rewrite rhythm.py with pattern-bank generators (dembow, dense, trapico, offbeat)
- Rewrite melodic.py with section-aware generators and humanization
- Add weighted random sample selection in SampleSelector (top-5 pool)
- Add generate_structure() with randomized templates and energy variance
- Fix RPP structure: TEMPO arity (3→4 args), string quoting for empty strings
- Rewrite quick_drumloop_test.py with correct REAPER ground truth format
- Add scripts/analyze_examples.py for reverse engineering audio tracks
- Add --seed argument for reproducible generation
- 72 tests passing
This commit is contained in:
renato97
2026-05-03 16:08:07 -03:00
parent 32dafd94e0
commit 3444006411
10 changed files with 1664 additions and 285 deletions

View File

@@ -51,13 +51,13 @@ _PROJECT_HEADER: list[list[str] | Element] = [
["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"],
["LOOP", "0"],
["LOOPGRAN", "0", "4"],
["RECORD_PATH", '"Media"', '""'],
["RECORD_PATH", "Media", ""],
Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]),
[],
Element("APPLYFX_CFG", [], children=[]),
[],
["RENDER_FILE", '""'],
["RENDER_PATTERN", '""'],
["RENDER_FILE", ""],
["RENDER_PATTERN", ""],
["RENDER_FMT", "0", "2", "0"],
["RENDER_1X", "0"],
["RENDER_RANGE", "1", "0", "0", "0", "1000"],
@@ -181,8 +181,10 @@ _FXCHAIN_FOOTER: list[list[str]] = [
# ---------------------------------------------------------------------------
def _make_guid() -> str:
"""Generate a random REAPER GUID string."""
return str(uuid.uuid4()).upper()
"""Generate a random REAPER GUID string using random module (seedable)."""
# Use random module directly so seeding works
import random
return str(uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)).upper()
def vst3_element(display_name: str, filename: str, uid_guid: str = "", preset_data: list[str] | None = None) -> Element:
@@ -233,8 +235,17 @@ class RPPBuilder:
builder.write("output.rpp")
"""
def __init__(self, song: SongDefinition) -> None:
def __init__(self, song: SongDefinition, seed: int | None = None) -> None:
self.song = song
self._seed = seed
if seed is not None:
import random
random.seed(seed)
def _make_seeded_guid(self) -> str:
"""Generate a random REAPER GUID string. Uses seed if provided to RPPBuilder."""
import random
return str(uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)).upper()
def write(self, path: str | Path) -> None:
"""Serialize the project to a .rpp file at *path*.
@@ -255,7 +266,9 @@ class RPPBuilder:
m = self.song.meta
# Project root — version from test_vst3.rpp line 1
root = Element("REAPER_PROJECT", ["0.1", "7.65/win64", str(int(uuid.uuid4().time)), "0"])
# Use random to make it seedable for reproducible output
import random
root = Element("REAPER_PROJECT", ["0.1", "7.65/win64", str(random.getrandbits(64)), "0"])
# Add all static project header lines
for line in _PROJECT_HEADER:
@@ -263,10 +276,11 @@ class RPPBuilder:
root.append(line)
# TEMPO is injected dynamically (overrides static header)
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den)])
# REAPER format: TEMPO bpm time_sig_num time_sig_den flag (flag=0 for standard)
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den), "0"])
# Master track
master_guid = _make_guid()
master_guid = self._make_seeded_guid()
master = Element("TRACK", [master_guid])
master.append(['NAME', "master"])
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
@@ -285,7 +299,7 @@ class RPPBuilder:
if line:
footer_copy = [v for v in line]
if footer_copy[0] == "FXID":
footer_copy[1] = f"{{{_make_guid()}}}"
footer_copy[1] = f"{{{self._make_seeded_guid()}}}"
master_fxchain.append(footer_copy)
master.append(master_fxchain)
root.append(master)
@@ -298,7 +312,7 @@ class RPPBuilder:
def _build_track(self, track: TrackDef) -> Element:
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
track_guid = _make_guid()
track_guid = self._make_seeded_guid()
track_elem = Element("TRACK", [f"{{{track_guid}}}"])
track_elem.append(["NAME", track.name])
@@ -332,7 +346,7 @@ class RPPBuilder:
fxchain.append([v for v in line])
for plugin in track.plugins:
fxchain.append(self._build_plugin(plugin))
fxid_guid = _make_guid()
fxid_guid = self._make_seeded_guid()
fxchain.append(["PRESETNAME", "Program 1"])
fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
fxchain.append(["FXID", f"{{{fxid_guid}}}"])