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

@@ -73,10 +73,24 @@ ROLE_RHYTHM_GENERATORS = {
"snare": "snare_pattern_bank_notes",
"hihat": "hihat_pattern_bank_notes",
"perc": "perc_combo_notes",
"clap": "clap_24_notes",
}
# 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_TO_SAMPLE_ROLE = {
@@ -88,6 +102,9 @@ ROLE_TO_SAMPLE_ROLE = {
"lead": "lead",
"harmony": "keys",
"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),
]
# 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
sections: list[SectionDef] = []
for s in sections_data:
@@ -339,6 +362,36 @@ def build_section_tracks(
midi_notes=midi_notes,
)
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:
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_delay = 0.0
# Apply role color (REAPER color palette 0-67)
role_color = ROLE_COLORS.get(role, 0)
track = TrackDef(
name=role.capitalize(),
volume=0.85 * vol_mult,
pan=0.0,
color=0,
color=role_color,
clips=section_clips,
plugins=plugins,
send_reverb=send_reverb,
@@ -440,6 +496,17 @@ def main() -> None:
with open(genre_path, "r", encoding="utf-8") as 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
index_path = _ROOT / "data" / "sample_index.json"
if not index_path.exists():
@@ -475,8 +542,29 @@ def main() -> None:
meta=meta,
tracks=tracks + return_tracks,
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
errors = song.validate()
if errors: