diff --git a/output/full_reggaeton_song.rpp b/output/full_reggaeton_song.rpp new file mode 100644 index 0000000..ffcd87b --- /dev/null +++ b/output/full_reggaeton_song.rpp @@ -0,0 +1,17990 @@ + + + RIPPLE 0 0 + GROUPOVERRIDE 0 0 0 0 + AUTOXFADE 129 + ENVATTACH 3 + POOLEDENVATTACH 0 + TCPUIFLAGS 0 + MIXERUIFLAGS 11 48 + ENVFADESZ10 40 + PEAKGAIN 1 + FEEDBACK 0 + PANLAW 1 + PROJOFFS 0 0 0 + MAXPROJLEN 0 0 + GRID 3199 8 1 8 1 0 0 0 + TIMEMODE 1 5 -1 30 0 0 -1 0 + VIDEO_CONFIG 0 0 65792 + PANMODE 3 + PANLAWFLAGS 3 + CURSOR 0 + ZOOM 100 0 0 + VZOOMEX 6 0 + USE_REC_CFG 0 + RECMODE 1 + SMPTESYNC 0 30 100 40 1000 300 0 0 1 0 0 + LOOP 0 + LOOPGRAN 0 4 + RECORD_PATH Media "" + + + + + RENDER_FILE "" + RENDER_PATTERN "" + RENDER_FMT 0 2 0 + RENDER_1X 0 + RENDER_RANGE 1 0 0 0 1000 + RENDER_RESAMPLE 3 0 1 + RENDER_ADDTOPROJ 0 + RENDER_STEMS 0 + RENDER_DITHER 0 + RENDER_TRIM 0.000001 0.000001 0 0 + TIMELOCKMODE 1 + TEMPOENVLOCKMODE 1 + ITEMMIX 1 + DEFPITCHMODE 589824 0 + TAKELANE 1 + SAMPLERATE 44100 0 0 + + LOCK 1 + + + GLOBAL_AUTO -1 + PLAYRATE 1 0 0.25 4 + SELECTION 0 0 + SELECTION2 0 0 + MASTERAUTOMODE 0 + MASTERTRACKHEIGHT 0 0 + MASTERPEAKCOL 16576 + MASTERMUTESOLO 0 + MASTERTRACKVIEW 0 0.6667 0.5 0.5 0 0 0 0 0 0 0 0 0 0 0 + MASTERHWOUT 0 0 1 0 0 0 0 -1 + MASTER_NCH 2 2 + MASTER_VOLUME 1 0 -1 -1 1 + MASTER_PANMODE 3 + MASTER_PANLAWFLAGS 3 + MASTER_FX 1 + MASTER_SEL 0 + + + + + RULERHEIGHT 86 86 + RULERLANE 1 4 "" 0 -1 + RULERLANE 2 8 "" 0 -1 + + TEMPO 95.0 4 4 0 + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {196471EF-F531-495C-9549-280AA55411E1} + > + > + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {EF98CA12-962D-4C15-BF96-D77B93E667D8} + > + AUXRECV 9 0.150000 -1 -1 0 + AUXRECV 10 0.100000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {1E2C3561-0E3E-4FF3-8E0A-AE2E3DBBDA87} + > + AUXRECV 9 0.150000 -1 -1 0 + AUXRECV 10 0.100000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {45894277-A869-46B2-B85E-1D768F3EFFE4} + > + AUXRECV 9 0.150000 -1 -1 0 + AUXRECV 10 0.100000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {CF98F3DB-EE28-47D8-8079-06F42986B48C} + > + AUXRECV 9 0.150000 -1 -1 0 + AUXRECV 10 0.100000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {5088F380-25BC-4991-9D15-30D2A6E79D8C} + > + AUXRECV 9 0.150000 -1 -1 0 + AUXRECV 10 0.100000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + + + + + + + + + + + + + + + + + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + "" + xz/rIu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAASwMAAAEAAAAAAAAA + OAIAAAEAAABGRkJTAQAAAIgAAAAAAAA/AAAAAAAAAD8AAAA/AAAAAJqZmT4AAAAAMzMzPwAAAADIAbRBAAAAAAAAAAAAAHpDAAAAAAAAAAAAAAAAJCeEPQAAAAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {07D6C86E-590F-4895-BA92-22BEA659AC28} + > + > + "" + y1aTfu5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAJMQAAABAAAAAAAAAA== + 1A8AAAEAAABGRkJTAQAAAO8DAACBMJY+ADJrPBrAFT8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAAAACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/ + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {16A76818-4C30-45F5-ADF2-2594EB35D8F5} + > + > +> diff --git a/scripts/compose.py b/scripts/compose.py index d2e5965..1655940 100644 --- a/scripts/compose.py +++ b/scripts/compose.py @@ -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: diff --git a/src/composer/rhythm.py b/src/composer/rhythm.py index 6afb583..a3f9f46 100644 --- a/src/composer/rhythm.py +++ b/src/composer/rhythm.py @@ -349,6 +349,20 @@ def clap_24_notes( 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 # --------------------------------------------------------------------------- diff --git a/src/core/schema.py b/src/core/schema.py index 6de21aa..5e7f2f9 100644 --- a/src/core/schema.py +++ b/src/core/schema.py @@ -111,6 +111,9 @@ class ClipDef: audio_path: Absolute path to audio file (for audio clips) midi_notes: List of MIDI notes (for MIDI clips) 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 @@ -118,6 +121,9 @@ class ClipDef: name: str = "" audio_path: str | None = None # for audio 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 def is_midi(self) -> bool: @@ -159,6 +165,7 @@ class TrackDef: plugins: VST plugins on this track send_reverb: Reverb 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 @@ -169,6 +176,7 @@ class TrackDef: plugins: list[PluginDef] = field(default_factory=list) send_reverb: float = 0.0 send_delay: float = 0.0 + send_level: dict[int, float] = field(default_factory=dict) @dataclass @@ -205,6 +213,7 @@ class SongDefinition: section_template: Section template name (default "standard") samples: Sample file map (name → filename) sections: Section definitions in playback order + master_plugins: List of plugin registry keys for master FX chain """ meta: SongMeta @@ -215,6 +224,7 @@ class SongDefinition: section_template: str = "standard" samples: dict[str, str] = field(default_factory=dict) sections: list[SectionDef] = field(default_factory=list) + master_plugins: list[str] = field(default_factory=list) # ------------------------------------------------------------------------- # Validation diff --git a/src/reaper_builder/__init__.py b/src/reaper_builder/__init__.py index c158f0f..a5b092e 100644 --- a/src/reaper_builder/__init__.py +++ b/src/reaper_builder/__init__.py @@ -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"])