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

@@ -0,0 +1,242 @@
#!/usr/bin/env python
"""Quick test: one drum loop repeated 4 times in track 2.
This script demonstrates the CORRECT REAPER .rpp format by matching
the ground truth from output/all_plugins.rpp and output/test_vst3.rpp.
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
from rpp import Element
import rpp
DRUM_LOOP = r"C:\Users\Administrator\Documents\fl_control\libreria\samples\drumloop\drumloop_D2_099_boomy_f8b5a5.wav"
OUTPUT = str(ROOT / "output" / "drumloop_test.rpp")
def build():
"""Build a valid REAPER .rpp with drum loop items."""
# Project root — matching ground truth format from all_plugins.rpp
project = Element("REAPER_PROJECT", ["0.1", "7.65/win64", "12345678901234567890", "0"])
# Project header from ground truth — tokens match exactly what REAPER expects
project.append(Element("NOTES", ["0", "2"]))
project.append([])
project.append(["RIPPLE", "0", "0"])
project.append(["GROUPOVERRIDE", "0", "0", "0", "0"])
project.append(["AUTOXFADE", "129"])
project.append(["ENVATTACH", "3"])
project.append(["POOLEDENVATTACH", "0"])
project.append(["TCPUIFLAGS", "0"])
project.append(["MIXERUIFLAGS", "11", "48"])
project.append(["ENVFADESZ10", "40"])
project.append(["PEAKGAIN", "1"])
project.append(["FEEDBACK", "0"])
project.append(["PANLAW", "1"])
project.append(["PROJOFFS", "0", "0", "0"])
project.append(["MAXPROJLEN", "0", "0"])
project.append(["GRID", "3199", "8", "1", "8", "1", "0", "0", "0"])
project.append(["TIMEMODE", "1", "5", "-1", "30", "0", "0", "-1", "0"])
project.append(["VIDEO_CONFIG", "0", "0", "65792"])
project.append(["PANMODE", "3"])
project.append(["PANLAWFLAGS", "3"])
project.append(["CURSOR", "0"])
project.append(["ZOOM", "100", "0", "0"])
project.append(["VZOOMEX", "6", "0"])
project.append(["USE_REC_CFG", "0"])
project.append(["RECMODE", "1"])
project.append(["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"])
project.append(["LOOP", "0"])
project.append(["LOOPGRAN", "0", "4"])
project.append(["RECORD_PATH", "Media", ""])
project.append(Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]))
project.append([])
project.append(Element("APPLYFX_CFG", [], children=[]))
project.append([])
project.append(["RENDER_FILE", ""])
project.append(["RENDER_PATTERN", ""])
project.append(["RENDER_FMT", "0", "2", "0"])
project.append(["RENDER_1X", "0"])
project.append(["RENDER_RANGE", "1", "0", "0", "0", "1000"])
project.append(["RENDER_RESAMPLE", "3", "0", "1"])
project.append(["RENDER_ADDTOPROJ", "0"])
project.append(["RENDER_STEMS", "0"])
project.append(["RENDER_DITHER", "0"])
project.append(["RENDER_TRIM", "0.000001", "0.000001", "0", "0"])
project.append(["TIMELOCKMODE", "1"])
project.append(["TEMPOENVLOCKMODE", "1"])
project.append(["ITEMMIX", "1"])
project.append(["DEFPITCHMODE", "589824", "0"])
project.append(["TAKELANE", "1"])
project.append(["SAMPLERATE", "44100", "0", "0"])
project.append([])
project.append(["LOCK", "1"])
project.append(Element("METRONOME", ["6", "2"], children=[
["VOL", "0.25", "0.125"],
["BEATLEN", "4"],
["FREQ", "1760", "880", "1"],
["SAMPLES", "", "", "", ""],
["SPLIGNORE", "0", "0"],
["SPLDEF", "2", "660", "", "0", ""],
["SPLDEF", "3", "440", "", "0", ""],
["PATTERN", "0", "169"],
["PATTERNSTR", "ABBB"],
["MULT", "1"],
]))
project.append([])
project.append(["GLOBAL_AUTO", "-1"])
# TEMPO with 4 args: bpm, time_sig_num, time_sig_den, flag
project.append(["TEMPO", "99", "4", "4", "0"])
project.append(["PLAYRATE", "1", "0", "0.25", "4"])
project.append(["SELECTION", "0", "0"])
project.append(["SELECTION2", "0", "0"])
project.append(["MASTERAUTOMODE", "0"])
project.append(["MASTERTRACKHEIGHT", "0", "0"])
project.append(["MASTERPEAKCOL", "16576"])
project.append(["MASTERMUTESOLO", "0"])
project.append(["MASTERTRACKVIEW", "0", "0.6667", "0.5", "0.5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"])
project.append(["MASTERHWOUT", "0", "0", "1", "0", "0", "0", "0", "-1"])
project.append(["MASTER_NCH", "2", "2"])
project.append(["MASTER_VOLUME", "1", "0", "-1", "-1", "1"])
project.append(["MASTER_PANMODE", "3"])
project.append(["MASTER_PANLAWFLAGS", "3"])
project.append(["MASTER_FX", "1"])
project.append(["MASTER_SEL", "0"])
project.append(Element("MASTERPLAYSPEEDENV", [], children=[
["EGUID", "{DEF87440-E07C-4B72-B9F8-D2AC60A0D0AC}"],
["ACT", "0", "-1"],
["VIS", "0", "1", "1"],
["LANEHEIGHT", "0", "0"],
["ARM", "0"],
["DEFSHAPE", "0", "-1", "-1"],
]))
project.append([])
project.append(Element("TEMPOENVEX", [], children=[
["EGUID", "{15E58A72-7149-4783-9A04-838503786012}"],
["ACT", "1", "-1"],
["VIS", "1", "0", "1"],
["LANEHEIGHT", "0", "0"],
["ARM", "0"],
["DEFSHAPE", "1", "-1", "-1"],
]))
project.append([])
project.append(["RULERHEIGHT", "86", "86"])
project.append(["RULERLANE", "1", "4", "", "0", "-1"])
project.append(["RULERLANE", "2", "8", "", "0", "-1"])
project.append([])
# Master track
master_guid = "{00000000-0000-0000-0000-000000000001}"
master = Element("TRACK", [master_guid])
master.append(["NAME", "master"])
master.append(["VOLPAN", "1", "0", "-1", "-1", "1"])
master.append(["PEAKCOL", "16576"])
master.append(["BEAT", "-1"])
master.append(["AUTOMODE", "0"])
master.append(["PANLAWFLAGS", "3"])
master.append(["MUTESOLO", "0", "0", "0"])
master.append(["IPHASE", "0"])
master.append(["PLAYOFFS", "0", "1"])
master.append(["ISBUS", "0", "0"])
master.append(["BUSCOMP", "0", "0", "0", "0", "0"])
master.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
master.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
master.append(["SEL", "0"])
master.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
master.append(["VU", "64"])
master.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
master.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
master.append(["NCHAN", "2"])
master.append(["FX", "1"])
master.append(["TRACKID", f"{{{master_guid}}}"])
master.append(["PERF", "0"])
master.append(["MIDIOUT", "-1"])
master.append(["MAINSEND", "1", "0"])
# Master FXCHAIN
master_fxchain = Element("FXCHAIN", [])
master_fxchain.append(["WNDRECT", "24", "52", "655", "408"])
master_fxchain.append(["SHOW", "0"])
master_fxchain.append(["LASTSEL", "0"])
master_fxchain.append(["DOCKED", "0"])
master_fxchain.append(["BYPASS", "0", "0", "0"])
master_fxchain.append(["PRESETNAME", "Program 1"])
master_fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
master_fxchain.append(["FXID", "{A0F6CA8C-99E7-4B1A-8411-CA7201811EAD}"])
master.append(master_fxchain)
project.append(master)
# Track 2: Drum Loop
track_guid = "{00000000-0000-0000-0000-000000000002}"
track = Element("TRACK", [track_guid])
track.append(["NAME", "Drum Loop"])
track.append(["VOLPAN", "0.85", "0", "-1", "-1", "1"])
track.append(["PEAKCOL", "16576"])
track.append(["BEAT", "-1"])
track.append(["AUTOMODE", "0"])
track.append(["PANLAWFLAGS", "3"])
track.append(["MUTESOLO", "0", "0", "0"])
track.append(["IPHASE", "0"])
track.append(["PLAYOFFS", "0", "1"])
track.append(["ISBUS", "0", "0"])
track.append(["BUSCOMP", "0", "0", "0", "0", "0"])
track.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
track.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
track.append(["SEL", "1"])
track.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
track.append(["VU", "64"])
track.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
track.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
track.append(["NCHAN", "2"])
track.append(["FX", "1"])
track.append(["TRACKID", f"{{{track_guid}}}"])
track.append(["PERF", "0"])
track.append(["MIDIOUT", "-1"])
track.append(["MAINSEND", "1", "0"])
# 4 clips of the drum loop, each 16 beats (4 bars)
loop_duration_beats = 16.0
for i in range(4):
position = i * loop_duration_beats
item = Element("ITEM", [])
item.append(["POSITION", f"{position:.6f}"])
item.append(["LENGTH", f"{loop_duration_beats:.6f}"])
item.append(["NAME", f"Drum Loop {i+1}"])
item.append(["SOFFS", "0.0"])
item.append(["PLAYRATE", "1", "0", "0.25", "4"])
item.append(["CHANMODE", "0"])
item.append(["GUID", f"{{00000000-0000-0000-0000-0000000000{i+10:02X}}}"])
item.append(["MUTE", "0", "0"])
item.append(["LOOP", "1"])
item.append(["COLOR", "0"])
# Audio source — uses correct <SOURCE WAVE> format
source = Element("SOURCE", ["WAVE"])
source.append(["FILE", DRUM_LOOP])
item.append(source)
track.append(item)
project.append(track)
# Write
output_str = rpp.dumps(project)
# Quote the version string in the header
output_str = output_str.replace(
"<REAPER_PROJECT 0.1 7.65/win64",
'<REAPER_PROJECT 0.1 "7.65/win64"'
)
Path(OUTPUT).parent.mkdir(parents=True, exist_ok=True)
Path(OUTPUT).write_text(output_str, encoding="utf-8")
print(f"Written: {OUTPUT}")
if __name__ == "__main__":
build()