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,610 @@
"""
Build a PROFESSIONAL reggaeton FLP with REAL SAMPLES from the user's library.
Key facts:
- Only Ch10-19 are sampler channels in the reference FLP (Ch0-9 are VST/plugin)
- Each sampler channel loads a real WAV from libreria/reggaeton/
- MIDI notes trigger those real samples
- 10 channels = kick, snare, hihat, 808, bell, lead, pad, clap, perc, rim
Sample selection (professional reggaeton):
Ch10: kick nes 1 — classic reggaeton kick
Ch11: snare nes 1 — clean reggaeton snare
Ch12: hi-hat 1 — tight hihat
Ch13: Bass Reventado — deep 808 bass (dastin.prod)
Ch14: bell 4 — bell tone for chords
Ch15: lead 3 — melodic lead
Ch16: pad 1 — sustained pad
Ch17: clap — reggaeton clap (using snap from perc loop)
Ch18: perc 1 — perc one shot
Ch19: rim — rim/rimshot
Output: output/reggaeton_fuego.flp
"""
import struct
import sys
import os
# ── Paths ──────────────────────────────────────────────────────────────────────
BASE = r"C:\Users\Administrator\Documents\fl_control"
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_fuego.flp")
# All samples copied here — clean names, no special chars
SAMPLES_DIR = os.path.join(BASE, "output", "fuego_samples")
sys.path.insert(0, BASE)
from src.flp_builder.events import (
EventID,
encode_text_event,
encode_word_event,
encode_data_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,
)
# ── Constants ──────────────────────────────────────────────────────────────────
BPM = 95
PPQ = 96
# Channel indices — ALL sampler channels (10-19)
CH_KICK = 10
CH_SNARE = 11
CH_HH = 12
CH_808 = 13
CH_BELL = 14
CH_LEAD = 15
CH_PAD = 16
CH_CLAP = 17
CH_PERC = 18
CH_RIM = 19
# Sample assignment: ch_idx → (samples_dir, wav_filename)
# All samples in fuego_samples/ with clean names
SAMPLE_ASSIGNMENT = {
CH_KICK: (SAMPLES_DIR, "kick.wav"),
CH_SNARE: (SAMPLES_DIR, "snare.wav"),
CH_HH: (SAMPLES_DIR, "hihat.wav"),
CH_808: (SAMPLES_DIR, "bass_808.wav"),
CH_BELL: (SAMPLES_DIR, "bell.wav"),
CH_LEAD: (SAMPLES_DIR, "lead.wav"),
CH_PAD: (SAMPLES_DIR, "pad.wav"),
CH_CLAP: (SAMPLES_DIR, "clap.wav"),
CH_PERC: (SAMPLES_DIR, "perc.wav"),
CH_RIM: (SAMPLES_DIR, "rim.wav"),
}
# ══════════════════════════════════════════════════════════════════════════════
# FUEGO CHORD PROGRESSION: Am → Dm → F → E
# ══════════════════════════════════════════════════════════════════════════════
PROGRESSION = [
{"name": "Am", "bass": 33, "chord": [45,48,52,57], "triad": [57,60,64], "root": 69},
{"name": "Dm", "bass": 38, "chord": [50,53,57,62], "triad": [62,65,69], "root": 74},
{"name": "F", "bass": 41, "chord": [53,57,60,65], "triad": [65,69,72], "root": 77},
{"name": "E", "bass": 40, "chord": [52,56,59,64], "triad": [64,68,71], "root": 76},
]
BEATS_PER_CHORD = 8
# ══════════════════════════════════════════════════════════════════════════════
# DRUM GENERATORS — using correct channel indices
# ══════════════════════════════════════════════════════════════════════════════
def _n(pos, length, ch, vel):
return {"pos": pos, "len": length, "key": 60, "vel": max(1, min(127, vel))}
def dembow_kick(bars, vel_mult=1.0):
"""REAL dembow: 0.0, 2.0, 3.25"""
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o, 0.25, CH_KICK, int(120 * vel_mult)))
notes.append(_n(o + 2.0, 0.25, CH_KICK, int(110 * vel_mult)))
notes.append(_n(o + 3.25, 0.15, CH_KICK, int(90 * vel_mult)))
return {CH_KICK: notes}
def perreador_kick(bars, vel_mult=1.0):
"""Perreador: every beat + offbeat ghosts."""
notes = []
for b in range(bars):
o = b * 4.0
for beat in range(4):
notes.append(_n(o + beat, 0.25, CH_KICK, int(115 * vel_mult)))
notes.append(_n(o + beat + 0.5, 0.15, CH_KICK, int(80 * vel_mult)))
return {CH_KICK: notes}
def sparse_kick(bars, vel_mult=1.0):
notes = []
for b in range(bars):
notes.append(_n(b * 4.0, 0.25, CH_KICK, int(100 * vel_mult)))
return {CH_KICK: notes}
def snare_standard(bars, vel_mult=1.0):
"""Snare: beats 2, 3-and (positions 1.25, 3.0)."""
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o + 1.25, 0.15, CH_SNARE, int(105 * vel_mult)))
notes.append(_n(o + 3.0, 0.15, CH_SNARE, int(100 * vel_mult)))
return {CH_SNARE: notes}
def snare_intense(bars, vel_mult=1.0):
"""Intense snare with ghost hits."""
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o + 1.25, 0.15, CH_SNARE, int(110 * vel_mult)))
notes.append(_n(o + 1.75, 0.10, CH_SNARE, int(70 * vel_mult)))
notes.append(_n(o + 3.0, 0.15, CH_SNARE, int(105 * vel_mult)))
notes.append(_n(o + 3.5, 0.10, CH_SNARE, int(65 * vel_mult)))
return {CH_SNARE: notes}
def hihat_offbeat(bars, vel_mult=1.0):
notes = []
for b in range(bars):
o = b * 4.0
for i in range(4):
notes.append(_n(o + i + 0.5, 0.1, CH_HH, int(55 * vel_mult)))
return {CH_HH: notes}
def hihat_8th(bars, vel_mult=1.0):
notes = []
for b in range(bars):
o = b * 4.0
for i in range(8):
v = 70 if i % 2 == 0 else 50
notes.append(_n(o + i * 0.5, 0.1, CH_HH, int(v * vel_mult)))
return {CH_HH: notes}
def hihat_16th(bars, vel_mult=1.0):
"""Full 16ths with accents and open hats."""
notes = []
for b in range(bars):
o = b * 4.0
for i in range(16):
p = i * 0.25
if p % 1.0 == 0.0:
v, l = 90, 0.1
elif p % 0.5 == 0.0:
v, l = 65, 0.1
else:
v, l = 40, 0.08
if i in [5, 10]:
l = 0.2; v = int(v * 1.2)
notes.append(_n(o + p, l, CH_HH, int(v * vel_mult)))
return {CH_HH: notes}
def clap_standard(bars, vel_mult=1.0):
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o + 1.0, 0.15, CH_CLAP, int(120 * vel_mult)))
notes.append(_n(o + 3.0, 0.15, CH_CLAP, int(115 * vel_mult)))
return {CH_CLAP: notes}
def clap_soft(bars, vel_mult=1.0):
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o + 1.0, 0.15, CH_CLAP, int(80 * vel_mult)))
notes.append(_n(o + 3.0, 0.15, CH_CLAP, int(75 * vel_mult)))
return {CH_CLAP: notes}
def perc_offbeat(bars, vel_mult=1.0):
notes = []
for b in range(bars):
o = b * 4.0
notes.append(_n(o + 0.75, 0.1, CH_PERC, int(85 * vel_mult)))
notes.append(_n(o + 2.75, 0.1, CH_PERC, int(80 * vel_mult)))
return {CH_PERC: notes}
def rim_build(bars, vel_mult=1.0):
"""Rim roll building intensity."""
PATTERNS = [[0,2,8,14], [0,2,4,8,10,14], [0,2,4,6,8,10,12,14], list(range(16))]
VELS = [50, 65, 80, 100]
notes = []
for b in range(bars):
o = b * 4.0
v = int(VELS[b % 4] * vel_mult)
for idx in PATTERNS[b % 4]:
notes.append(_n(o + idx * 0.25, 0.1, CH_RIM, v))
return {CH_RIM: notes}
# ══════════════════════════════════════════════════════════════════════════════
# MELODIC GENERATORS
# ══════════════════════════════════════════════════════════════════════════════
def _mn(pos, length, key, vel):
"""Melodic note — pitch matters."""
return {"pos": pos, "len": length, "key": key, "vel": max(1, min(127, vel))}
def bass_808_full(bars, vel_mult=1.0):
"""808 bass with chord-root movement + fifth variation."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
r = ch["bass"]
f = r + 7
v = vel_mult
notes.append(_mn(base + 0.0, 2.5, r, int(110*v)))
notes.append(_mn(base + 2.5, 0.5, f, int(80*v)))
notes.append(_mn(base + 3.0, 2.0, r, int(105*v)))
notes.append(_mn(base + 5.0, 1.0, r, int(90*v)))
notes.append(_mn(base + 6.0, 0.5, f, int(75*v)))
notes.append(_mn(base + 6.5, 1.5, r, int(100*v)))
return {CH_808: notes}
def bass_808_sparse(bars, vel_mult=1.0):
"""Sparse 808 for intro — just root, long sustain."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
notes.append(_mn(ci * BEATS_PER_CHORD, 7.5, ch["bass"], int(60 * vel_mult)))
return {CH_808: notes}
def bell_chords(bars, vel_mult=1.0):
"""Bell playing offbeat chord stabs — 4-note voicings."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
stabs = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
for sp in stabs:
v = int((85 + (hash((ci, sp)) % 10)) * vel_mult)
for pitch in ch["triad"]:
notes.append(_mn(base + sp, 0.12, pitch, v))
return {CH_BELL: notes}
def bell_sparse(bars, vel_mult=1.0):
"""Sparse bell for intro — 4-note voicings, beats 2.5 and 6.5."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
for sp in [2.5, 6.5]:
v = int(60 * vel_mult)
for pitch in ch["chord"]:
notes.append(_mn(base + sp, 0.15, pitch, v))
return {CH_BELL: notes}
def lead_hook(bars, vel_mult=1.0):
"""Lead melody — arch contour, chord tones on strong beats."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
lr = ch["root"]
c = ch["triad"]
v = vel_mult
notes.append(_mn(base + 0.0, 1.0, c[0], int(95*v)))
notes.append(_mn(base + 1.0, 0.5, c[1], int(85*v)))
notes.append(_mn(base + 1.5, 0.5, c[2], int(100*v)))
notes.append(_mn(base + 2.0, 1.5, lr, int(105*v)))
notes.append(_mn(base + 3.5, 0.5, c[2], int(90*v)))
notes.append(_mn(base + 4.0, 0.5, c[1], int(80*v)))
notes.append(_mn(base + 4.5, 1.5, c[0], int(95*v)))
notes.append(_mn(base + 6.0, 0.5, lr-2, int(75*v)))
notes.append(_mn(base + 6.5, 1.5, c[0], int(90*v)))
return {CH_LEAD: notes}
def pad_sustained(bars, vel_mult=1.0):
"""Sustained pad — 4-note voicings."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
for pitch in ch["chord"]:
notes.append(_mn(base, 7.5, pitch, int(60 * vel_mult)))
return {CH_PAD: notes}
def pad_swell(bars, vel_mult=1.0):
"""Pad swell for pre-chorus — crescendo within chord."""
notes = []
total = bars * 4
chords = total // BEATS_PER_CHORD
for ci in range(chords):
ch = PROGRESSION[ci % 4]
base = ci * BEATS_PER_CHORD
for pitch in ch["chord"]:
notes.append(_mn(base, 4.0, pitch, int(45 * vel_mult)))
notes.append(_mn(base + 4, 3.5, pitch, int(70 * vel_mult)))
return {CH_PAD: notes}
# ══════════════════════════════════════════════════════════════════════════════
# PATTERN DEFINITIONS — 20 patterns
# ══════════════════════════════════════════════════════════════════════════════
# All generators return {ch_idx: [notes]}
ALL_GENERATORS = {
"dembow_kick": dembow_kick,
"perreador_kick": perreador_kick,
"sparse_kick": sparse_kick,
"snare_std": snare_standard,
"snare_intense": snare_intense,
"hh_offbeat": hihat_offbeat,
"hh_8th": hihat_8th,
"hh_16th": hihat_16th,
"clap_std": clap_standard,
"clap_soft": clap_soft,
"perc_offbeat": perc_offbeat,
"rim_build": rim_build,
"bass_full": bass_808_full,
"bass_sparse": bass_808_sparse,
"bell_chords": bell_chords,
"bell_sparse": bell_sparse,
"lead_hook": lead_hook,
"pad_sustained": pad_sustained,
"pad_swell": pad_swell,
}
PATTERNS = [
{"id": 1, "name": "Kick Dembow", "gen": "dembow_kick", "bars": 8},
{"id": 2, "name": "Kick Perreador", "gen": "perreador_kick","bars": 8},
{"id": 3, "name": "Kick Sparse", "gen": "sparse_kick", "bars": 8},
{"id": 4, "name": "Snare Standard", "gen": "snare_std", "bars": 8},
{"id": 5, "name": "Snare Intense", "gen": "snare_intense", "bars": 8},
{"id": 6, "name": "HH Offbeat", "gen": "hh_offbeat", "bars": 8},
{"id": 7, "name": "HH 8th", "gen": "hh_8th", "bars": 8},
{"id": 8, "name": "HH 16th Full", "gen": "hh_16th", "bars": 8},
{"id": 9, "name": "Clap Standard", "gen": "clap_std", "bars": 8},
{"id": 10, "name": "Perc Offbeat", "gen": "perc_offbeat", "bars": 8},
{"id": 11, "name": "Rim Build", "gen": "rim_build", "bars": 4},
{"id": 12, "name": "808 Bass Full", "gen": "bass_full", "bars": 8},
{"id": 13, "name": "808 Bass Sparse", "gen": "bass_sparse", "bars": 8},
{"id": 14, "name": "Bell Chords", "gen": "bell_chords", "bars": 8},
{"id": 15, "name": "Bell Sparse", "gen": "bell_sparse", "bars": 8},
{"id": 16, "name": "Lead Hook", "gen": "lead_hook", "bars": 8},
{"id": 17, "name": "Pad Sustained", "gen": "pad_sustained", "bars": 8},
{"id": 18, "name": "Pad Swell", "gen": "pad_swell", "bars": 8},
]
# ══════════════════════════════════════════════════════════════════════════════
# ARRANGEMENT — 48 bars, 7 sections
# 10 tracks (one per sampler channel Ch10-19)
# Track index in arrangement: 0=kick, 1=snare, 2=hh, 3=808, 4=bell,
# 5=lead, 6=pad, 7=clap, 8=perc, 9=rim
# ══════════════════════════════════════════════════════════════════════════════
ARRANGEMENT_ITEMS = [
# INTRO (0-4): ghostly, sparse
{"pattern": 3, "bar": 0, "bars": 4, "track": 0}, # sparse kick
{"pattern": 6, "bar": 0, "bars": 4, "track": 2}, # offbeat HH
{"pattern": 13, "bar": 0, "bars": 4, "track": 3}, # sparse 808
{"pattern": 15, "bar": 0, "bars": 4, "track": 4}, # sparse bell
{"pattern": 17, "bar": 0, "bars": 4, "track": 6}, # pad sustained
# VERSE 1 (4-12): warming up
{"pattern": 1, "bar": 4, "bars": 8, "track": 0}, # dembow kick
{"pattern": 4, "bar": 4, "bars": 8, "track": 1}, # snare std
{"pattern": 7, "bar": 4, "bars": 8, "track": 2}, # HH 8th
{"pattern": 12, "bar": 4, "bars": 8, "track": 3}, # 808 full
{"pattern": 15, "bar": 4, "bars": 8, "track": 4}, # sparse bell
{"pattern": 17, "bar": 4, "bars": 8, "track": 6}, # pad
# PRE-CHORUS (12-16): building tension
{"pattern": 1, "bar": 12, "bars": 4, "track": 0}, # dembow kick
{"pattern": 5, "bar": 12, "bars": 4, "track": 1}, # snare intense
{"pattern": 11, "bar": 12, "bars": 4, "track": 9}, # rim build
{"pattern": 7, "bar": 12, "bars": 4, "track": 2}, # HH 8th
{"pattern": 12, "bar": 12, "bars": 4, "track": 3}, # 808 full
{"pattern": 14, "bar": 12, "bars": 4, "track": 4}, # bell chords
{"pattern": 18, "bar": 12, "bars": 4, "track": 6}, # pad swell
# CHORUS (16-24): FULL ENERGY
{"pattern": 2, "bar": 16, "bars": 8, "track": 0}, # perreador kick!
{"pattern": 5, "bar": 16, "bars": 8, "track": 1}, # snare intense
{"pattern": 8, "bar": 16, "bars": 8, "track": 2}, # HH 16th
{"pattern": 9, "bar": 16, "bars": 8, "track": 7}, # clap
{"pattern": 10, "bar": 16, "bars": 8, "track": 8}, # perc offbeat
{"pattern": 12, "bar": 16, "bars": 8, "track": 3}, # 808 full
{"pattern": 14, "bar": 16, "bars": 8, "track": 4}, # bell chords
{"pattern": 16, "bar": 16, "bars": 8, "track": 5}, # lead hook
{"pattern": 17, "bar": 16, "bars": 8, "track": 6}, # pad
# VERSE 2 (24-32): energy maintained, no lead
{"pattern": 1, "bar": 24, "bars": 8, "track": 0}, # dembow kick
{"pattern": 4, "bar": 24, "bars": 8, "track": 1}, # snare std
{"pattern": 7, "bar": 24, "bars": 8, "track": 2}, # HH 8th
{"pattern": 9, "bar": 24, "bars": 8, "track": 7}, # clap
{"pattern": 12, "bar": 24, "bars": 8, "track": 3}, # 808 full
{"pattern": 14, "bar": 24, "bars": 8, "track": 4}, # bell chords
{"pattern": 17, "bar": 24, "bars": 8, "track": 6}, # pad
# BREAKDOWN (32-36): stripped
{"pattern": 3, "bar": 32, "bars": 4, "track": 0}, # sparse kick
{"pattern": 6, "bar": 32, "bars": 4, "track": 2}, # offbeat HH
{"pattern": 13, "bar": 32, "bars": 4, "track": 3}, # sparse 808
{"pattern": 15, "bar": 32, "bars": 4, "track": 4}, # sparse bell
{"pattern": 17, "bar": 32, "bars": 4, "track": 6}, # pad
# OUTRO (36-48): fading
{"pattern": 1, "bar": 36, "bars": 12, "track": 0}, # dembow kick
{"pattern": 4, "bar": 36, "bars": 12, "track": 1}, # snare std
{"pattern": 7, "bar": 36, "bars": 12, "track": 2}, # HH 8th
{"pattern": 17, "bar": 36, "bars": 12, "track": 6}, # pad
]
# ══════════════════════════════════════════════════════════════════════════════
# 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):
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 found")
header = bytearray(ref_bytes[22:first_pat])
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 _conv(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:
buf += encode_word_event(EventID.PatNew, pat_def["id"] - 1)
buf += encode_text_event(EventID.PatName, pat_def["name"])
notes_by_ch = ALL_GENERATORS[pat_def["gen"]](pat_def["bars"])
for ch_idx, raw_notes in notes_by_ch.items():
if not raw_notes:
continue
buf += encode_data_event(EventID.PatNotes, encode_notes_block(ch_idx, _conv(raw_notes), PPQ))
return bytes(buf)
# ══════════════════════════════════════════════════════════════════════════════
# MAIN BUILD
# ══════════════════════════════════════════════════════════════════════════════
def build_fuego():
print("=" * 60)
print("FUEGO reggaeton — REAL SAMPLES from library")
print("=" * 60)
print(f"Progression: Am -> Dm -> F -> E")
print(f"Samples from: libreria/reggaeton/")
print(f"Channels: Ch10-19 (all sampler)")
print(f"Arrangement: 48 bars, 7 sections")
print("=" * 60)
assert os.path.isfile(REF_FLP), f"MISSING: {REF_FLP}"
ref_bytes = open(REF_FLP, "rb").read()
num_channels = struct.unpack("<H", ref_bytes[10:12])[0]
print(f"\nReference: {len(ref_bytes):,} bytes, {num_channels} channels")
# 1. Load channels — ALL 10 samplers get real samples from fuego_samples/
print("\n[1/4] Loading channels with real samples...")
loader = ChannelSkeletonLoader(REF_FLP, CH11_TMPL, SAMPLES_DIR)
# All channels use samples from fuego_samples/
channel_bytes = loader.load(melodic_map=SAMPLE_ASSIGNMENT)
print(f" Channels: {len(channel_bytes):,} bytes")
for ch_idx in sorted(SAMPLE_ASSIGNMENT.keys()):
_, w = SAMPLE_ASSIGNMENT[ch_idx]
print(f" Ch{ch_idx}: {w}")
# 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
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
)
with open(FLP_OUT, "wb") as f:
f.write(flp)
duration = (48 * 4 / BPM) * 60
print(f"\n{'=' * 60}")
print(f" Output: {FLP_OUT}")
print(f" Size: {len(flp):,} bytes")
print(f" Duration: ~{duration:.0f}s (48 bars @ {BPM} BPM)")
print(f" Channels: {num_channels} (Ch0-9 plugin, Ch10-19 sampler)")
print(f" Patterns: {len(PATTERNS)}")
print(f" Sections: INTRO -> VERSE1 -> PRE-CHORUS -> CHORUS -> VERSE2 -> BREAKDOWN -> OUTRO")
print(f"{'=' * 60}")
return flp
if __name__ == "__main__":
build_fuego()