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:
17990
output/full_reggaeton_song.rpp
Normal file
17990
output/full_reggaeton_song.rpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -73,10 +73,24 @@ ROLE_RHYTHM_GENERATORS = {
|
|||||||
"snare": "snare_pattern_bank_notes",
|
"snare": "snare_pattern_bank_notes",
|
||||||
"hihat": "hihat_pattern_bank_notes",
|
"hihat": "hihat_pattern_bank_notes",
|
||||||
"perc": "perc_combo_notes",
|
"perc": "perc_combo_notes",
|
||||||
|
"clap": "clap_24_notes",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Roles that use audio items per hit instead of MIDI pattern
|
# Roles that use audio items per hit instead of MIDI pattern
|
||||||
AUDIO_ROLES = {"drums", "snare", "hihat", "perc"}
|
AUDIO_ROLES = {"drums", "snare", "hihat", "perc", "clap", "drumloop", "vocal"}
|
||||||
|
|
||||||
|
# Role → color index mapping (REAPER color palette 0-67)
|
||||||
|
ROLE_COLORS: dict[str, int] = {
|
||||||
|
"drums": 3, # red
|
||||||
|
"clap": 4, # orange-red
|
||||||
|
"bass": 5, # orange
|
||||||
|
"harmony": 9, # green
|
||||||
|
"lead": 11, # cyan
|
||||||
|
"pad": 13, # blue
|
||||||
|
"perc": 7, # yellow
|
||||||
|
"vocal": 15, # pink/magenta
|
||||||
|
"drumloop": 3, # red (same as drums)
|
||||||
|
}
|
||||||
|
|
||||||
# Role → sample key (used for SampleSelector)
|
# Role → sample key (used for SampleSelector)
|
||||||
ROLE_TO_SAMPLE_ROLE = {
|
ROLE_TO_SAMPLE_ROLE = {
|
||||||
@@ -88,6 +102,9 @@ ROLE_TO_SAMPLE_ROLE = {
|
|||||||
"lead": "lead",
|
"lead": "lead",
|
||||||
"harmony": "keys",
|
"harmony": "keys",
|
||||||
"pad": "pad",
|
"pad": "pad",
|
||||||
|
"clap": "snare", # clap uses snare samples (no dedicated clap role)
|
||||||
|
"vocal": "vocal",
|
||||||
|
"drumloop": "drumloop",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -238,6 +255,12 @@ def build_section_tracks(
|
|||||||
("trapico", 1),
|
("trapico", 1),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Add extended roles (clap, vocal, drumloop) if not already in roles
|
||||||
|
# These are handled as audio roles with special sample selection
|
||||||
|
for _role in ("clap", "vocal", "drumloop"):
|
||||||
|
if _role not in roles:
|
||||||
|
roles[_role] = {}
|
||||||
|
|
||||||
# Parse sections into SectionDef list
|
# Parse sections into SectionDef list
|
||||||
sections: list[SectionDef] = []
|
sections: list[SectionDef] = []
|
||||||
for s in sections_data:
|
for s in sections_data:
|
||||||
@@ -339,6 +362,36 @@ def build_section_tracks(
|
|||||||
midi_notes=midi_notes,
|
midi_notes=midi_notes,
|
||||||
)
|
)
|
||||||
section_clips.append(clip)
|
section_clips.append(clip)
|
||||||
|
else:
|
||||||
|
# vocal, drumloop: audio clips spanning full sections
|
||||||
|
if role in ("vocal", "drumloop") and sample_path:
|
||||||
|
# Select character based on section type for vocal
|
||||||
|
if role == "vocal":
|
||||||
|
if section.name in ("verse", "bridge"):
|
||||||
|
character = "melodic"
|
||||||
|
elif section.name in ("chorus", "drop"):
|
||||||
|
character = "powerful"
|
||||||
|
else:
|
||||||
|
character = "neutral"
|
||||||
|
vocal_samples = selector.select_diverse(
|
||||||
|
role="vocal", n=1, exclude=used_sample_ids.get("vocal", []),
|
||||||
|
key=key, bpm=bpm, character=character
|
||||||
|
)
|
||||||
|
if vocal_samples:
|
||||||
|
sample = vocal_samples[0]
|
||||||
|
sample_path = sample.get("original_path")
|
||||||
|
sample_id = sample.get("file_hash", "")
|
||||||
|
if sample_id:
|
||||||
|
used_sample_ids.setdefault("vocal", []).append(sample_id)
|
||||||
|
|
||||||
|
audio_clip = ClipDef(
|
||||||
|
position=sec_offset * 4.0,
|
||||||
|
length=section.bars * 4.0,
|
||||||
|
name=f"{section.name.capitalize()} {role.capitalize()}",
|
||||||
|
audio_path=sample_path,
|
||||||
|
loop=(role == "drumloop"),
|
||||||
|
)
|
||||||
|
section_clips.append(audio_clip)
|
||||||
|
|
||||||
if not section_clips:
|
if not section_clips:
|
||||||
continue
|
continue
|
||||||
@@ -361,11 +414,14 @@ def build_section_tracks(
|
|||||||
send_reverb = 0.3 if per_role_cfg.get("reverb_on_lead") or per_role_cfg.get("reverb_on_snare") else 0.0
|
send_reverb = 0.3 if per_role_cfg.get("reverb_on_lead") or per_role_cfg.get("reverb_on_snare") else 0.0
|
||||||
send_delay = 0.0
|
send_delay = 0.0
|
||||||
|
|
||||||
|
# Apply role color (REAPER color palette 0-67)
|
||||||
|
role_color = ROLE_COLORS.get(role, 0)
|
||||||
|
|
||||||
track = TrackDef(
|
track = TrackDef(
|
||||||
name=role.capitalize(),
|
name=role.capitalize(),
|
||||||
volume=0.85 * vol_mult,
|
volume=0.85 * vol_mult,
|
||||||
pan=0.0,
|
pan=0.0,
|
||||||
color=0,
|
color=role_color,
|
||||||
clips=section_clips,
|
clips=section_clips,
|
||||||
plugins=plugins,
|
plugins=plugins,
|
||||||
send_reverb=send_reverb,
|
send_reverb=send_reverb,
|
||||||
@@ -440,6 +496,17 @@ def main() -> None:
|
|||||||
with open(genre_path, "r", encoding="utf-8") as f:
|
with open(genre_path, "r", encoding="utf-8") as f:
|
||||||
genre_config = json.load(f)
|
genre_config = json.load(f)
|
||||||
|
|
||||||
|
# Randomize chord progression selection from 5 options in reggaeton_2009.json
|
||||||
|
progressions = genre_config.get("chord_progressions", [])
|
||||||
|
if progressions:
|
||||||
|
# Weighted random selection by popularity
|
||||||
|
prog_names = [p.get("name", "") for p in progressions]
|
||||||
|
pop_values = [p.get("popularity", 0.5) for p in progressions]
|
||||||
|
selected_prog = random.choices(prog_names, weights=pop_values, k=1)[0]
|
||||||
|
progression_name = selected_prog
|
||||||
|
else:
|
||||||
|
progression_name = "i-VII-VI-VII"
|
||||||
|
|
||||||
# Load sample index
|
# Load sample index
|
||||||
index_path = _ROOT / "data" / "sample_index.json"
|
index_path = _ROOT / "data" / "sample_index.json"
|
||||||
if not index_path.exists():
|
if not index_path.exists():
|
||||||
@@ -475,8 +542,29 @@ def main() -> None:
|
|||||||
meta=meta,
|
meta=meta,
|
||||||
tracks=tracks + return_tracks,
|
tracks=tracks + return_tracks,
|
||||||
sections=sections,
|
sections=sections,
|
||||||
|
progression_name=progression_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wire sends: find return track indices and set send_level on non-return tracks
|
||||||
|
ret_idx_map: dict[str, int] = {}
|
||||||
|
for idx, track in enumerate(song.tracks):
|
||||||
|
if track.name == "Reverb":
|
||||||
|
ret_idx_map["reverb"] = idx
|
||||||
|
elif track.name == "Delay":
|
||||||
|
ret_idx_map["delay"] = idx
|
||||||
|
|
||||||
|
reverb_idx = ret_idx_map.get("reverb", 0)
|
||||||
|
delay_idx = ret_idx_map.get("delay", 1)
|
||||||
|
for track in song.tracks:
|
||||||
|
if track.name not in ("Reverb", "Delay", "master"):
|
||||||
|
track.send_level = {
|
||||||
|
reverb_idx: 0.15, # send to reverb
|
||||||
|
delay_idx: 0.10, # send to delay
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wire master chain
|
||||||
|
song.master_plugins = ["Pro-Q_3", "Pro-C_2", "Pro-L_2"]
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
errors = song.validate()
|
errors = song.validate()
|
||||||
if errors:
|
if errors:
|
||||||
|
|||||||
@@ -349,6 +349,20 @@ def clap_24_notes(
|
|||||||
return _apply_groove({CH_CL: notes}, groove_strength)
|
return _apply_groove({CH_CL: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_clap_pattern(
|
||||||
|
bars: int,
|
||||||
|
velocity_mult: float = 1.0,
|
||||||
|
density: float = 1.0,
|
||||||
|
groove_strength: float = 0.0,
|
||||||
|
) -> dict[int, list[dict]]:
|
||||||
|
"""Generate clap pattern using CLAP_DEMBOW.
|
||||||
|
|
||||||
|
Snare on beats 2, 3.5 — standard reggaeton clap.
|
||||||
|
Returns {CH_CL: [notes...]}.
|
||||||
|
"""
|
||||||
|
return clap_24_notes(bars, velocity_mult, density, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Percussion generators
|
# Percussion generators
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ class ClipDef:
|
|||||||
audio_path: Absolute path to audio file (for audio clips)
|
audio_path: Absolute path to audio file (for audio clips)
|
||||||
midi_notes: List of MIDI notes (for MIDI clips)
|
midi_notes: List of MIDI notes (for MIDI clips)
|
||||||
name: Display name
|
name: Display name
|
||||||
|
loop: Whether the audio clip loops (default False)
|
||||||
|
fade_in: Fade-in duration in seconds (default 0.0)
|
||||||
|
fade_out: Fade-out duration in seconds (default 0.0)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
position: float
|
position: float
|
||||||
@@ -118,6 +121,9 @@ class ClipDef:
|
|||||||
name: str = ""
|
name: str = ""
|
||||||
audio_path: str | None = None # for audio clips
|
audio_path: str | None = None # for audio clips
|
||||||
midi_notes: list[MidiNote] = field(default_factory=list) # for MIDI clips
|
midi_notes: list[MidiNote] = field(default_factory=list) # for MIDI clips
|
||||||
|
loop: bool = False
|
||||||
|
fade_in: float = 0.0
|
||||||
|
fade_out: float = 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_midi(self) -> bool:
|
def is_midi(self) -> bool:
|
||||||
@@ -159,6 +165,7 @@ class TrackDef:
|
|||||||
plugins: VST plugins on this track
|
plugins: VST plugins on this track
|
||||||
send_reverb: Reverb send level 0.0–1.0
|
send_reverb: Reverb send level 0.0–1.0
|
||||||
send_delay: Delay send level 0.0–1.0
|
send_delay: Delay send level 0.0–1.0
|
||||||
|
send_level: Dict mapping return track index → send level 0.0–1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -169,6 +176,7 @@ class TrackDef:
|
|||||||
plugins: list[PluginDef] = field(default_factory=list)
|
plugins: list[PluginDef] = field(default_factory=list)
|
||||||
send_reverb: float = 0.0
|
send_reverb: float = 0.0
|
||||||
send_delay: float = 0.0
|
send_delay: float = 0.0
|
||||||
|
send_level: dict[int, float] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -205,6 +213,7 @@ class SongDefinition:
|
|||||||
section_template: Section template name (default "standard")
|
section_template: Section template name (default "standard")
|
||||||
samples: Sample file map (name → filename)
|
samples: Sample file map (name → filename)
|
||||||
sections: Section definitions in playback order
|
sections: Section definitions in playback order
|
||||||
|
master_plugins: List of plugin registry keys for master FX chain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
meta: SongMeta
|
meta: SongMeta
|
||||||
@@ -215,6 +224,7 @@ class SongDefinition:
|
|||||||
section_template: str = "standard"
|
section_template: str = "standard"
|
||||||
samples: dict[str, str] = field(default_factory=dict)
|
samples: dict[str, str] = field(default_factory=dict)
|
||||||
sections: list[SectionDef] = field(default_factory=list)
|
sections: list[SectionDef] = field(default_factory=list)
|
||||||
|
master_plugins: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Validation
|
# Validation
|
||||||
|
|||||||
@@ -1642,7 +1642,11 @@ class RPPBuilder:
|
|||||||
defaults_copy[1] = f"{{{master_guid}}}"
|
defaults_copy[1] = f"{{{master_guid}}}"
|
||||||
master.append(defaults_copy)
|
master.append(defaults_copy)
|
||||||
|
|
||||||
# Master track FXCHAIN (MASTER_FX 1 requires 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", [])
|
master_fxchain = Element("FXCHAIN", [])
|
||||||
for line in _FXCHAIN_HEADER:
|
for line in _FXCHAIN_HEADER:
|
||||||
master_fxchain.append([v for v in line])
|
master_fxchain.append([v for v in line])
|
||||||
@@ -1661,6 +1665,28 @@ class RPPBuilder:
|
|||||||
|
|
||||||
return root
|
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:
|
def _build_track(self, track: TrackDef) -> Element:
|
||||||
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
|
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
|
||||||
track_guid = self._make_seeded_guid()
|
track_guid = self._make_seeded_guid()
|
||||||
@@ -1681,6 +1707,10 @@ class RPPBuilder:
|
|||||||
defaults_copy = ["SEL", "1"]
|
defaults_copy = ["SEL", "1"]
|
||||||
track_elem.append(defaults_copy)
|
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
|
# Plugins (FXCHAIN) — wrap VST elements inside proper FXCHAIN structure
|
||||||
if track.plugins:
|
if track.plugins:
|
||||||
fxchain = Element("FXCHAIN", [])
|
fxchain = Element("FXCHAIN", [])
|
||||||
@@ -1694,12 +1724,17 @@ class RPPBuilder:
|
|||||||
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
||||||
track_elem.append(fxchain)
|
track_elem.append(fxchain)
|
||||||
|
|
||||||
# Send effects
|
# Legacy send effects (send_reverb, send_delay)
|
||||||
if track.send_reverb > 0:
|
if track.send_reverb > 0:
|
||||||
track_elem.append(["AUXRECV", "0", f"{track.send_reverb:.6f}", "-1", "-1", "0"])
|
track_elem.append(["AUXRECV", "0", f"{track.send_reverb:.6f}", "-1", "-1", "0"])
|
||||||
if track.send_delay > 0:
|
if track.send_delay > 0:
|
||||||
track_elem.append(["AUXRECV", "1", f"{track.send_delay:.6f}", "-1", "-1", "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)
|
# Clips (items)
|
||||||
for clip in track.clips:
|
for clip in track.clips:
|
||||||
track_elem.append(self._build_clip(clip))
|
track_elem.append(self._build_clip(clip))
|
||||||
@@ -1759,6 +1794,12 @@ class RPPBuilder:
|
|||||||
item.append(["LENGTH", str(clip.length)])
|
item.append(["LENGTH", str(clip.length)])
|
||||||
if clip.name:
|
if clip.name:
|
||||||
item.append(["NAME", 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:
|
if clip.is_audio and clip.audio_path:
|
||||||
source = Element("SOURCE", ["WAVE"])
|
source = Element("SOURCE", ["WAVE"])
|
||||||
|
|||||||
Reference in New Issue
Block a user