feat: reggaeton production system with intelligent sample selection and FLP generation

This commit is contained in:
renato97
2026-05-02 21:40:18 -03:00
commit 4d941f3f90
62 changed files with 8656 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
"""
Build a COMPLETE reggaeton FLP with drums + melodic MIDI patterns.
Strategy:
1. Load 20 sampler channels from reference FLP (ChannelSkeletonLoader)
2. Melodic MIDI notes go on existing EMPTY channels (3, 4, 8, 17)
which are empty samplers — user assigns VST plugins in FL Studio.
3. Build 14 patterns with drum generators + inline melodic generators
4. Build 36-bar arrangement (~1:31 at 95 BPM)
5. Assemble identically to proven v15 builder — 20 channels, no VST hacks.
Output: output/reggaeton_completo.flp
"""
import struct
import sys
import os
# ── Paths ──────────────────────────────────────────────────────────────────────
BASE = r"C:\Users\Administrator\Documents\fl_control"
SAMPLES_DIR = os.path.join(BASE, "output", "samples")
CH11_TMPL = os.path.join(BASE, "output", "ch11_kick_template.bin")
REF_FLP = os.path.join(BASE, r"my space ryt\my space ryt.flp")
FLP_OUT = os.path.join(BASE, "output", "reggaeton_completo.flp")
sys.path.insert(0, BASE)
from src.flp_builder.events import (
EventID,
encode_text_event,
encode_word_event,
encode_data_event,
encode_byte_event,
encode_notes_block,
)
from src.flp_builder.skeleton import ChannelSkeletonLoader
from src.flp_builder.arrangement import (
ArrangementItem,
build_arrangement_section,
build_track_data_template,
)
from src.composer.rhythm import get_notes
# ── Constants ──────────────────────────────────────────────────────────────────
BPM = 95
PPQ = 96
# Channel indices — drums (from rhythm.py)
CH_P1 = 10; CH_K = 11; CH_S = 12; CH_R = 13
CH_P2 = 14; CH_H = 15; CH_CL = 16
# Channel indices — melodic (reuse empty sampler channels from reference)
# Ch 3, 4, 8, 17 are empty samplers (no sample loaded, cloned from ch11 tmpl)
# MIDI notes go here — user assigns VSTs manually in FL Studio
CH_808 = 3
CH_PIANO = 4
CH_LEAD = 8
CH_PAD = 17
# ── Chord Progression: Am → G → F → G (each chord = 2 bars = 8 beats) ──────
PROGRESSION = [
{
"name": "Am",
"bass_root": 45, # A2
"chord": [57, 60, 64], # A3, C4, E4
"pad": [45, 48, 52], # A2, C3, E3
"lead_root": 69, # A4
},
{
"name": "G",
"bass_root": 43, # G2
"chord": [55, 59, 62], # G3, B3, D4
"pad": [43, 47, 50], # G2, B2, D3
"lead_root": 67, # G4
},
{
"name": "F",
"bass_root": 41, # F2
"chord": [53, 57, 60], # F3, A3, C4
"pad": [41, 45, 48], # F2, A2, C3
"lead_root": 65, # F4
},
{
"name": "G",
"bass_root": 43,
"chord": [55, 59, 62],
"pad": [43, 47, 50],
"lead_root": 67,
},
]
BEATS_PER_CHORD = 8 # 2 bars per chord
# ══════════════════════════════════════════════════════════════════════════════
# MELODIC GENERATORS (inline)
# ══════════════════════════════════════════════════════════════════════════════
def _note(pos, length, key, vel):
return {"pos": pos, "len": length, "key": key, "vel": vel}
def bass_808_notes(bars):
"""808 bass following root notes. Pattern per chord (2 bars):
Beat 0: root vel110 dur3 | Beat 3.5: root vel90 dur1.5
Beat 5: root vel100 dur2 | Beat 7.5: root vel85 dur0.5
"""
notes = []
total_beats = bars * 4
chords_needed = total_beats // BEATS_PER_CHORD
for ci in range(chords_needed):
ch = PROGRESSION[ci % len(PROGRESSION)]
base = ci * BEATS_PER_CHORD
root = ch["bass_root"]
notes.append(_note(base + 0.0, 3.0, root, 110))
notes.append(_note(base + 3.5, 1.5, root, 90))
notes.append(_note(base + 5.0, 2.0, root, 100))
notes.append(_note(base + 7.5, 0.5, root, 85))
return {CH_808: notes}
def piano_stabs_notes(bars):
"""Offbeat piano stabs: beats 1.5, 2.5, 3.5, 5.5, 6.5, 7.5 per chord.
3-note triads, vel 80-90."""
notes = []
total_beats = bars * 4
chords_needed = total_beats // BEATS_PER_CHORD
stab_positions = [1.5, 2.5, 3.5, 5.5, 6.5, 7.5]
for ci in range(chords_needed):
ch = PROGRESSION[ci % len(PROGRESSION)]
base = ci * BEATS_PER_CHORD
for sp in stab_positions:
vel = 80 + (hash((ci, sp)) % 11)
for pitch in ch["chord"]:
notes.append(_note(base + sp, 0.15, pitch, vel))
return {CH_PIANO: notes}
def piano_sparse_notes(bars):
"""Sparse piano for intro/breakdown: beats 2.5 and 6.5 only, vel 65-70."""
notes = []
total_beats = bars * 4
chords_needed = total_beats // BEATS_PER_CHORD
for ci in range(chords_needed):
ch = PROGRESSION[ci % len(PROGRESSION)]
base = ci * BEATS_PER_CHORD
for sp in [2.5, 6.5]:
vel = 65 + (hash((ci, sp)) % 6)
for pitch in ch["chord"]:
notes.append(_note(base + sp, 0.15, pitch, vel))
return {CH_PIANO: notes}
def lead_hook_notes(bars):
"""Melodic hook emphasizing chord tones per 2-bar cycle."""
notes = []
total_beats = bars * 4
chords_needed = total_beats // BEATS_PER_CHORD
for ci in range(chords_needed):
ch = PROGRESSION[ci % len(PROGRESSION)]
base = ci * BEATS_PER_CHORD
lr = ch["lead_root"]
notes.append(_note(base + 0.0, 1.0, ch["chord"][0], 100))
notes.append(_note(base + 1.0, 0.5, ch["chord"][2], 95))
notes.append(_note(base + 2.0, 0.75, ch["chord"][1], 90))
notes.append(_note(base + 3.5, 0.25, lr, 85))
notes.append(_note(base + 5.0, 0.5, ch["chord"][2], 95))
notes.append(_note(base + 5.5, 1.0, ch["chord"][0], 100))
notes.append(_note(base + 6.5, 0.5, lr + 2, 80))
return {CH_LEAD: notes}
def pad_sustained_notes(bars):
"""Long sustained pad chords. 3 notes per chord, vel 65, dur 7.5 beats."""
notes = []
total_beats = bars * 4
chords_needed = total_beats // BEATS_PER_CHORD
for ci in range(chords_needed):
ch = PROGRESSION[ci % len(PROGRESSION)]
base = ci * BEATS_PER_CHORD
for pitch in ch["pad"]:
notes.append(_note(base + 0.0, 7.5, pitch, 65))
return {CH_PAD: notes}
MELODIC_GENERATORS = {
"bass_808_notes": bass_808_notes,
"piano_stabs_notes": piano_stabs_notes,
"piano_sparse_notes": piano_sparse_notes,
"lead_hook_notes": lead_hook_notes,
"pad_sustained_notes": pad_sustained_notes,
}
# ══════════════════════════════════════════════════════════════════════════════
# PATTERN DEFINITIONS
# ══════════════════════════════════════════════════════════════════════════════
PATTERNS = [
{"id": 1, "name": "Kick Main", "generator": "kick_main_notes", "bars": 8},
{"id": 2, "name": "Kick Sparse", "generator": "kick_sparse_notes", "bars": 8},
{"id": 3, "name": "Snare Verse", "generator": "snare_verse_notes", "bars": 8},
{"id": 4, "name": "Hihat 16th", "generator": "hihat_16th_notes", "bars": 8},
{"id": 5, "name": "Hihat 8th", "generator": "hihat_8th_notes", "bars": 8},
{"id": 6, "name": "Clap 24", "generator": "clap_24_notes", "bars": 8},
{"id": 7, "name": "Rim Build", "generator": "rim_build_notes", "bars": 4},
{"id": 8, "name": "Perc Combo", "generator": "perc_combo_notes", "bars": 8},
{"id": 9, "name": "Kick Outro", "generator": "kick_outro_notes", "bars": 8},
{"id": 10, "name": "808 Bass", "generator": "bass_808_notes", "bars": 8, "melodic": True},
{"id": 11, "name": "Piano Stabs", "generator": "piano_stabs_notes", "bars": 8, "melodic": True},
{"id": 12, "name": "Piano Sparse", "generator": "piano_sparse_notes","bars": 8, "melodic": True},
{"id": 13, "name": "Lead Hook", "generator": "lead_hook_notes", "bars": 8, "melodic": True},
{"id": 14, "name": "Pad Sustained","generator": "pad_sustained_notes","bars": 8, "melodic": True},
]
# ══════════════════════════════════════════════════════════════════════════════
# ARRANGEMENT (36 bars = ~1:31 at 95 BPM)
# 9 arrangement tracks
# ══════════════════════════════════════════════════════════════════════════════
ARRANGEMENT_ITEMS = [
# INTRO (0-4)
{"pattern": 2, "bar": 0, "bars": 4, "track": 0},
{"pattern": 5, "bar": 0, "bars": 4, "track": 2},
{"pattern": 14, "bar": 0, "bars": 4, "track": 8},
{"pattern": 12, "bar": 0, "bars": 4, "track": 6},
# VERSE (4-12)
{"pattern": 1, "bar": 4, "bars": 8, "track": 0},
{"pattern": 3, "bar": 4, "bars": 8, "track": 1},
{"pattern": 4, "bar": 4, "bars": 8, "track": 2},
{"pattern": 8, "bar": 4, "bars": 8, "track": 4},
{"pattern": 10, "bar": 4, "bars": 8, "track": 5},
{"pattern": 11, "bar": 4, "bars": 8, "track": 6},
{"pattern": 14, "bar": 4, "bars": 8, "track": 8},
# PRE-CHORUS (12-16)
{"pattern": 1, "bar": 12, "bars": 4, "track": 0},
{"pattern": 3, "bar": 12, "bars": 4, "track": 1},
{"pattern": 4, "bar": 12, "bars": 4, "track": 2},
{"pattern": 7, "bar": 12, "bars": 4, "track": 3},
{"pattern": 8, "bar": 12, "bars": 4, "track": 4},
{"pattern": 10, "bar": 12, "bars": 4, "track": 5},
{"pattern": 11, "bar": 12, "bars": 4, "track": 6},
{"pattern": 14, "bar": 12, "bars": 4, "track": 8},
# CHORUS (16-24)
{"pattern": 1, "bar": 16, "bars": 8, "track": 0},
{"pattern": 3, "bar": 16, "bars": 8, "track": 1},
{"pattern": 4, "bar": 16, "bars": 8, "track": 2},
{"pattern": 6, "bar": 16, "bars": 8, "track": 3},
{"pattern": 8, "bar": 16, "bars": 8, "track": 4},
{"pattern": 10, "bar": 16, "bars": 8, "track": 5},
{"pattern": 11, "bar": 16, "bars": 8, "track": 6},
{"pattern": 13, "bar": 16, "bars": 8, "track": 7},
{"pattern": 14, "bar": 16, "bars": 8, "track": 8},
# BREAKDOWN (24-28)
{"pattern": 5, "bar": 24, "bars": 4, "track": 2},
{"pattern": 14, "bar": 24, "bars": 4, "track": 8},
{"pattern": 12, "bar": 24, "bars": 4, "track": 6},
# OUTRO (28-36)
{"pattern": 9, "bar": 28, "bars": 8, "track": 0},
{"pattern": 3, "bar": 28, "bars": 8, "track": 1},
{"pattern": 4, "bar": 28, "bars": 8, "track": 2},
{"pattern": 14, "bar": 28, "bars": 8, "track": 8},
]
# ══════════════════════════════════════════════════════════════════════════════
# HEADER BUILDER
# ══════════════════════════════════════════════════════════════════════════════
def _read_ev(data, pos):
s = pos
ib = data[pos]
pos += 1
if ib < 64:
return pos + 1, s, ib, data[s + 1], "byte"
elif ib < 128:
return pos + 2, s, ib, struct.unpack("<H", data[pos:pos + 2])[0], "word"
elif ib < 192:
return pos + 4, s, ib, struct.unpack("<I", data[pos:pos + 4])[0], "dword"
else:
sz = 0; sh = 0
while True:
b = data[pos]; pos += 1
sz |= (b & 0x7F) << sh; sh += 7
if not (b & 0x80): break
return pos + sz, s, ib, data[pos:pos + sz], "data"
def build_header(ref_bytes):
"""Extract header events from reference FLP, patch BPM to 95."""
pos = 22
first_pat = None
while pos < len(ref_bytes):
np, st, ib, val, vt = _read_ev(ref_bytes, pos)
if ib == EventID.PatNew:
first_pat = st
break
pos = np
if first_pat is None:
raise ValueError("No PatNew event found in reference FLP")
header = bytearray(ref_bytes[22:first_pat])
# Patch BPM
p = 0
while p < len(header):
np, _, ib, val, vt = _read_ev(bytes(header), p)
if ib == EventID.Tempo:
struct.pack_into("<I", header, p + 1, BPM * 1000)
break
p = np
return bytes(header)
# ══════════════════════════════════════════════════════════════════════════════
# PATTERN BUILDER
# ══════════════════════════════════════════════════════════════════════════════
def _convert_rhythm_notes(notes):
return [
{"position": n["pos"], "length": n["len"], "key": n["key"], "velocity": n["vel"]}
for n in notes
]
def build_all_patterns():
buf = bytearray()
for pat_def in PATTERNS:
pat_id = pat_def["id"]
gen_name = pat_def["generator"]
bars = pat_def["bars"]
is_melodic = pat_def.get("melodic", False)
buf += encode_word_event(EventID.PatNew, pat_id - 1)
buf += encode_text_event(EventID.PatName, pat_def["name"])
if is_melodic:
notes_by_channel = MELODIC_GENERATORS[gen_name](bars)
else:
notes_by_channel = get_notes(gen_name, bars)
for ch_idx, raw_notes in notes_by_channel.items():
if not raw_notes:
continue
converted = _convert_rhythm_notes(raw_notes)
buf += encode_data_event(
EventID.PatNotes,
encode_notes_block(ch_idx, converted, PPQ),
)
return bytes(buf)
# ══════════════════════════════════════════════════════════════════════════════
# MAIN BUILD — identical assembly to proven v15 builder
# ══════════════════════════════════════════════════════════════════════════════
def build_complete_reggaeton():
print("=" * 60)
print("Building COMPLETE reggaeton FLP (drums + melodic MIDI)")
print("=" * 60)
for p in [REF_FLP, CH11_TMPL]:
assert os.path.isfile(p), f"MISSING: {p}"
ref_bytes = open(REF_FLP, "rb").read()
num_channels = struct.unpack("<H", ref_bytes[10:12])[0]
print(f"Reference FLP: {len(ref_bytes):,} bytes, {num_channels} channels")
# 1. Load sampler channels (identical to v15)
print("\n[1/4] Loading sampler channels...")
loader = ChannelSkeletonLoader(REF_FLP, CH11_TMPL, SAMPLES_DIR)
sample_map = {
"perc1": "perc1.wav", "kick": "kick.wav", "snare": "snare.wav",
"rim": "rim.wav", "perc2": "perc2.wav", "hihat": "hihat.wav",
"clap": "clap.wav",
}
channel_bytes = loader.load(sample_map)
print(f" Channels: {len(channel_bytes):,} bytes ({num_channels} channels)")
# 2. Build header + patterns
print("\n[2/4] Building header + patterns...")
header_bytes = build_header(ref_bytes)
pattern_bytes = build_all_patterns()
print(f" Header: {len(header_bytes):,} bytes")
print(f" Patterns: {len(pattern_bytes):,} bytes ({len(PATTERNS)} patterns)")
# 3. Build arrangement
print("\n[3/4] Building arrangement...")
track_data_template = build_track_data_template(ref_bytes)
items = [
ArrangementItem(
pattern_id=it["pattern"], bar=it["bar"],
num_bars=it["bars"], track_index=it["track"],
)
for it in ARRANGEMENT_ITEMS
]
arrangement_bytes = build_arrangement_section(items, track_data_template, ppq=PPQ)
print(f" Arrangement: {len(arrangement_bytes):,} bytes ({len(items)} items)")
# 4. Assemble — identical to v15 builder
print("\n[4/4] Assembling FLP...")
body = header_bytes + pattern_bytes + channel_bytes + arrangement_bytes
flp = (
struct.pack("<4sIhHH", b"FLhd", 6, 0, num_channels, PPQ)
+ b"FLdt" + struct.pack("<I", len(body))
+ body
)
os.makedirs(os.path.dirname(FLP_OUT), exist_ok=True)
with open(FLP_OUT, "wb") as f:
f.write(flp)
duration = (36 * 4 / BPM) * 60
print(f"\n{'=' * 60}")
print(f"Output: {FLP_OUT}")
print(f"Size: {len(flp):,} bytes")
print(f"Duration: ~{duration:.0f}s (36 bars at {BPM} BPM)")
print(f"Channels: {num_channels} (unchanged from reference)")
print(f"Patterns: {len(PATTERNS)} (9 drums + 5 melodic)")
print(f"{'=' * 60}")
print()
print("MELODIC CHANNELS (assign VSTs manually in FL Studio):")
print(f" Ch {CH_808}: 808 Bass -> Serum2")
print(f" Ch {CH_PIANO}: Piano -> Pigments")
print(f" Ch {CH_LEAD}: Lead -> Serum2")
print(f" Ch {CH_PAD}: Pad -> Omnisphere")
return flp
if __name__ == "__main__":
build_complete_reggaeton()