437 lines
18 KiB
Python
437 lines
18 KiB
Python
"""
|
|
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()
|