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:
@@ -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}}}"])
|
||||
|
||||
Reference in New Issue
Block a user