feat: full reggaeton song generator with master chain, sends, clap, vocal, drumloop

- Add master FXCHAIN with Pro-Q 3, Pro-C 2, Pro-L 2 on master track
- Add AUXRECV sends routing to Reverb/Delay return tracks
- Add clap track with CLAP_DEMBOW pattern
- Add vocal track with sample selection per section
- Add drumloop layer with loop detection
- Add track colors per role for visual organization
- Randomize chord progressions from genre config (5 options)
- Add master_plugins and send_level fields to schema
- Add _build_master_fxchain() and AUXRECV rendering to RPPBuilder
- 72 tests passing, RPP generates with 12 tracks, 18 sends, 20 plugins
This commit is contained in:
renato97
2026-05-03 19:13:10 -03:00
parent 8562bfbed1
commit 672607c356
5 changed files with 18157 additions and 14 deletions

View File

@@ -1642,17 +1642,21 @@ class RPPBuilder:
defaults_copy[1] = f"{{{master_guid}}}"
master.append(defaults_copy)
# Master track FXCHAIN (MASTER_FX 1 requires FXCHAIN)
master_fxchain = Element("FXCHAIN", [])
for line in _FXCHAIN_HEADER:
master_fxchain.append([v for v in line])
for line in _FXCHAIN_FOOTER:
if line:
footer_copy = [v for v in line]
if footer_copy[0] == "FXID":
footer_copy[1] = f"{{{self._make_seeded_guid()}}}"
master_fxchain.append(footer_copy)
master.append(master_fxchain)
# Master track FXCHAIN — use _build_master_fxchain() if plugins are defined
if self.song.master_plugins:
master.append(self._build_master_fxchain())
else:
# Empty FXCHAIN skeleton (MASTER_FX 1 requires FXCHAIN element)
master_fxchain = Element("FXCHAIN", [])
for line in _FXCHAIN_HEADER:
master_fxchain.append([v for v in line])
for line in _FXCHAIN_FOOTER:
if line:
footer_copy = [v for v in line]
if footer_copy[0] == "FXID":
footer_copy[1] = f"{{{self._make_seeded_guid()}}}"
master_fxchain.append(footer_copy)
master.append(master_fxchain)
root.append(master)
# User tracks
@@ -1661,6 +1665,28 @@ class RPPBuilder:
return root
def _build_master_fxchain(self) -> Element:
"""Build the FXCHAIN Element for the master track with master_plugins.
Uses _build_plugin() for each plugin in SongDefinition.master_plugins.
"""
fxchain = Element("FXCHAIN", [])
for line in _FXCHAIN_HEADER:
fxchain.append([v for v in line])
for idx, plugin_name in enumerate(self.song.master_plugins):
plugin = PluginDef(name=plugin_name, path="", index=idx)
fxchain.append(self._build_plugin(plugin))
fxid_guid = self._make_seeded_guid()
for line in _FXCHAIN_FOOTER:
if line:
footer_copy = [v for v in line]
if footer_copy[0] == "FXID":
footer_copy[1] = f"{{{fxid_guid}}}"
fxchain.append(footer_copy)
return fxchain
def _build_track(self, track: TrackDef) -> Element:
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
track_guid = self._make_seeded_guid()
@@ -1681,6 +1707,10 @@ class RPPBuilder:
defaults_copy = ["SEL", "1"]
track_elem.append(defaults_copy)
# Track color
if track.color > 0:
track_elem.append(["COLOR", str(track.color)])
# Plugins (FXCHAIN) — wrap VST elements inside proper FXCHAIN structure
if track.plugins:
fxchain = Element("FXCHAIN", [])
@@ -1694,12 +1724,17 @@ class RPPBuilder:
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
track_elem.append(fxchain)
# Send effects
# Legacy send effects (send_reverb, send_delay)
if track.send_reverb > 0:
track_elem.append(["AUXRECV", "0", f"{track.send_reverb:.6f}", "-1", "-1", "0"])
if track.send_delay > 0:
track_elem.append(["AUXRECV", "1", f"{track.send_delay:.6f}", "-1", "-1", "0"])
# Generalised send_level dict — maps return track index → send level
for ret_idx, level in track.send_level.items():
if level > 0:
track_elem.append(["AUXRECV", str(ret_idx), f"{level:.6f}", "-1", "-1", "0"])
# Clips (items)
for clip in track.clips:
track_elem.append(self._build_clip(clip))
@@ -1759,6 +1794,12 @@ class RPPBuilder:
item.append(["LENGTH", str(clip.length)])
if clip.name:
item.append(["NAME", clip.name])
if clip.loop:
item.append(["LOOP", "1"])
if clip.fade_in > 0:
item.append(["FADEIN", f"{clip.fade_in:.6f}"])
if clip.fade_out > 0:
item.append(["FADEOUT", f"{clip.fade_out:.6f}"])
if clip.is_audio and clip.audio_path:
source = Element("SOURCE", ["WAVE"])