refactor: migrate from FL Studio to REAPER with rpp library

Replace FL Studio binary .flp output with REAPER text-based .rpp output
using the rpp Python library (Perlence/rpp).

- Add core/schema.py: DAW-agnostic data types (SongDefinition, TrackDef,
  ClipDef, MidiNote, PluginDef)
- Add reaper_builder/: RPP file generation via rpp.Element + headless
  render via reaper.exe CLI
- Add composer/converters.py: bridge rhythm.py/melodic.py note dicts
  to core.schema MidiNote objects
- Rewrite scripts/compose.py: real generator pipeline with --render flag
- Delete src/flp_builder/, src/scanner/, mcp/, flstudio-mcp/, old scripts
- Add 40 passing tests (schema, builder, converters, compose, render)
This commit is contained in:
renato97
2026-05-03 09:13:35 -03:00
parent 1e2316a5a4
commit af6d61c8a1
47 changed files with 1589 additions and 4990 deletions

0
scripts/__init__.py Normal file
View File

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""Batch FLP generator — produces 50 unique reggaeton FLP+JSON pairs.
Usage:
python scripts/batch_generate.py [--count 50] [--out-dir output/batch]
Output structure:
output/batch_{timestamp}/
reggaeton_000_95bpm_Am_i-VII-VI-VII.json
reggaeton_000_95bpm_Am_i-VII-VI-VII.flp
reggaeton_001_90bpm_Dm_i-iv-VII-III.json
...
manifest.json ← list of all generated songs with metadata
"""
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1]))
from src.composer.variation import generate_batch
from src.flp_builder.builder import FLPBuilder
from src.flp_builder.schema import SongDefinition
# ---------------------------------------------------------------------------
# Filename helpers
# ---------------------------------------------------------------------------
_UNSAFE_RE = re.compile(r'[^\w\-]')
def sanitize_filename(s: str) -> str:
"""Replace unsafe filename chars with _."""
return _UNSAFE_RE.sub('_', s)
def make_filename(idx: int, song: SongDefinition) -> str:
"""Build stem like ``reggaeton_000_95bpm_Am_i_VII_VI_VII`` (no extension)."""
prog_safe = sanitize_filename(song.progression_name)
return f"reggaeton_{idx:03d}_{song.meta.bpm}bpm_{song.meta.key}_{prog_safe}"
# ---------------------------------------------------------------------------
# Manifest
# ---------------------------------------------------------------------------
def build_manifest(songs: list[SongDefinition], filenames: list[str]) -> dict:
"""Build manifest dict with per-song metadata."""
entries = []
for idx, (song, stem) in enumerate(zip(songs, filenames)):
bar_count = int(max(item.bar + item.bars for item in song.items))
entries.append({
"idx": idx,
"filename": stem,
"bpm": song.meta.bpm,
"key": song.meta.key,
"progression": song.progression_name,
"title": song.meta.title,
"bars": bar_count,
})
return {
"generated_at": datetime.now().isoformat(),
"count": len(songs),
"songs": entries,
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Batch FLP generator")
parser.add_argument("--count", type=int, default=50,
help="Number of songs to generate (default: 50)")
parser.add_argument("--out-dir", default="",
help="Output directory (default: output/batch_{timestamp})")
args = parser.parse_args()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
out_dir = Path(args.out_dir) if args.out_dir else Path("output") / f"batch_{timestamp}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"Generating {args.count} songs -> {out_dir}")
songs = generate_batch(args.count)
builder = FLPBuilder()
filenames: list[str] = []
for idx, song in enumerate(songs):
stem = make_filename(idx, song)
filenames.append(stem)
# Write JSON
json_path = out_dir / f"{stem}.json"
json_path.write_text(song.to_json(), encoding="utf-8")
# Write FLP
flp_path = out_dir / f"{stem}.flp"
flp_bytes = builder.build(song)
flp_path.write_bytes(flp_bytes)
bar_count = int(max(item.bar + item.bars for item in song.items))
print(f" [{idx+1:>3}/{args.count}] {stem}.flp {len(flp_bytes):>9,}b {bar_count}bars")
# Write manifest
manifest = build_manifest(songs, filenames)
(out_dir / "manifest.json").write_text(
json.dumps(manifest, indent=2), encoding="utf-8"
)
total_size = sum((out_dir / f"{f}.flp").stat().st_size for f in filenames)
print(f"\nDone. {args.count} FLPs in {out_dir}")
print(f" Total size: {total_size:,} bytes")
if __name__ == "__main__":
main()

View File

@@ -1,160 +0,0 @@
#!/usr/bin/env python
"""Build an FL Studio project from a composition plan JSON."""
import sys
import os
import json
import argparse
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from src.flp_builder.project import FLPProject, Note
from src.flp_builder.writer import FLPWriter
PLUGIN_NAME_MAP = {
"Serum 2": "Serum2VST3",
"Omnisphere": "Omnisphere",
"Kontakt 7": "Kontakt 7",
"Diva": "Diva",
"Electra": "Electra",
"Pigments": "Pigments",
"ravity(S)": "ravity(S)",
"FL Keys": "FL Keys",
"FPC": "FPC",
"FLEX": "FLEX",
"Sytrus": "Sytrus",
"Harmor": "Harmor",
"3x Osc": "3x Osc",
"DirectWave": "DirectWave",
"Fruity DrumSynth Live": "Fruity DrumSynth Live",
"Transistor Bass": "Transistor Bass",
"Sakura": "Sakura",
"Sawer": "Sawer",
"Toxic Biohazard": "Toxic Biohazard",
"Harmless": "Harmless",
"GMS": "GMS",
"Minisynth": "Minisynth",
"Morphine": "Morphine",
"Soundfont Player": "Soundfont Player",
}
OUTPUT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) / "output"
def resolve_plugin(preferred_list):
for name in preferred_list:
if name in PLUGIN_NAME_MAP:
internal = PLUGIN_NAME_MAP[name]
is_vst = name in [
"Serum 2", "Omnisphere", "Kontakt 7", "Diva",
"Electra", "Pigments", "ravity(S)",
]
return {
"internal_name": "Fruity Wrapper" if is_vst else internal,
"display_name": name,
"is_vst": is_vst,
}
return {
"internal_name": "MIDI Out",
"display_name": "MIDI Out",
"is_vst": False,
}
def build_project(composition: dict) -> FLPProject:
meta = composition["meta"]
tracks = composition["tracks"]
project = FLPProject(
tempo=meta["bpm"],
title=meta.get("title", f"{meta.get('genre', 'Untitled')} - {meta.get('key', 'C')}"),
genre=meta.get("genre", ""),
fl_version="24.7.1.73",
ppq=meta.get("ppq", 96),
)
channel_map = {}
for i, track in enumerate(tracks):
role = track["role"]
plugin_info = resolve_plugin(track.get("preferred_plugins", []))
ch = project.add_channel(
name=f"{role}_{plugin_info['display_name']}",
plugin_internal_name=plugin_info["internal_name"],
plugin_display_name=plugin_info["display_name"],
mixer_track=track.get("mixer_slot", i),
channel_type=2,
)
channel_map[role] = ch.index
bars = meta.get("bars", 8)
ppq = meta.get("ppq", 96)
beats_per_chord = meta.get("beats_per_chord", 4)
for section_idx, track in enumerate(tracks):
role = track["role"]
ch_idx = channel_map.get(role, 0)
raw_notes = track.get("notes", [])
if not raw_notes:
continue
pat = project.add_pattern(name=f"{role}")
for n in raw_notes:
note = Note(
position=n["position"],
length=n["length"],
key=n.get("key", 60),
velocity=n.get("velocity", 100),
pan=n.get("pan", 0),
mod_x=n.get("mod_x", 0),
mod_y=n.get("mod_y", 0),
)
pat.add_note(ch_idx, note)
return project
def main():
parser = argparse.ArgumentParser(description="Build FL Studio project from composition plan")
parser.add_argument("plan", help="Path to composition plan JSON")
parser.add_argument("--output", "-o", help="Output .flp file path", default=None)
args = parser.parse_args()
with open(args.plan, "r", encoding="utf-8") as f:
composition = json.load(f)
project = build_project(composition)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
if args.output:
output_path = args.output
else:
genre = composition["meta"].get("genre", "track")
key = composition["meta"].get("key", "C")
bpm = composition["meta"].get("bpm", 140)
output_path = str(OUTPUT_DIR / f"{genre}_{key}_{bpm}bpm.flp")
writer = FLPWriter(project)
writer.write(output_path)
result = {
"status": "ok",
"output": output_path,
"tempo": project.tempo,
"channels": len(project.channels),
"patterns": len(project.patterns),
"channel_names": [ch.name for ch in project.channels],
"pattern_names": [p.name for p in project.patterns],
"total_notes": sum(
len(notes)
for pat in project.patterns
for notes in pat.notes.values()
),
}
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()

View File

@@ -1,436 +0,0 @@
"""
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()

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env python3
"""CLI: build a single FLP from a JSON song definition.
Usage:
python scripts/build_from_json.py <song.json> [--out <output.flp>]
"""
import argparse
import sys
from pathlib import Path
# Add project root to path
sys.path.insert(0, str(Path(__file__).parents[1]))
from src.flp_builder.schema import load_song_json
from src.flp_builder.builder import FLPBuilder
def main():
parser = argparse.ArgumentParser(
description="Build FLP from JSON song definition"
)
parser.add_argument("song_json", help="Path to song .json file")
parser.add_argument(
"--out", help="Output .flp path (default: same name as JSON)"
)
args = parser.parse_args()
json_path = Path(args.song_json)
out_path = (
Path(args.out) if args.out else json_path.with_suffix(".flp")
)
song = load_song_json(json_path)
builder = FLPBuilder()
flp = builder.build(song)
out_path.write_bytes(flp)
print(f"Built {out_path} ({len(flp):,} bytes)")
if __name__ == "__main__":
main()

View File

@@ -1,610 +0,0 @@
"""
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()

View File

@@ -1,71 +1,205 @@
#!/usr/bin/env python
"""Compose and build in one step from genre knowledge base."""
import sys
import os
import json
"""Compose a REAPER .rpp project from the sample library.
Single entrypoint: loads sample index, builds a SongDefinition from the selector/composer,
and writes a .rpp file.
Usage:
python scripts/compose.py --genre reggaeton --bpm 95 --key Am
python scripts/compose.py --genre trap --bpm 140 --key Cm --output output/my_track.rpp
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
# Ensure project root on path
_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(_ROOT))
from src.composer import compose_from_genre
from scripts.build import build_project
from src.flp_builder.writer import FLPWriter
KNOWLEDGE_DIR = Path(__file__).parent.parent / "knowledge" / "genres"
OUTPUT_DIR = Path(__file__).parent.parent / "output"
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
from src.composer.rhythm import get_notes
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
from src.composer.converters import rhythm_to_midi, melodic_to_midi
from src.selector import SampleSelector
from src.reaper_builder import RPPBuilder
from src.reaper_builder.render import render_project
def main():
parser = argparse.ArgumentParser(description="Compose and build from genre")
parser.add_argument("genre", help="Genre filename (e.g. reggaeton_2009)")
parser.add_argument("--key", "-k", default=None, help="Override key (e.g. Am)")
parser.add_argument("--bpm", "-b", type=float, default=None, help="Override BPM")
parser.add_argument("--bars", type=int, default=None, help="Override bar count")
parser.add_argument("--output", "-o", default=None, help="Output .flp path")
# ---------------------------------------------------------------------------
# Track builders
# ---------------------------------------------------------------------------
def build_drum_track(
role: str,
generator_name: str,
bars: int,
) -> TrackDef:
"""Build a drum MIDI track from a rhythm generator.
Args:
role: Track name (e.g. "kick", "snare")
generator_name: Name from rhythm.GENERATORS (e.g. "kick_main_notes")
bars: Number of bars
"""
note_dict = get_notes(generator_name, bars)
midi_notes = rhythm_to_midi(note_dict)
clip = ClipDef(
position=0.0,
length=bars * 4.0,
name=f"{role.capitalize()} Pattern",
midi_notes=midi_notes,
)
return TrackDef(name=role.capitalize(), clips=[clip])
def build_melodic_track(
role: str,
generator_fn,
key: str,
bpm: float,
bars: int,
selector: SampleSelector | None = None,
) -> TrackDef:
"""Build a melodic MIDI track from a generator function.
Args:
role: Track name (e.g. "bass", "lead")
generator_fn: Callable from melodic.py (e.g. bass_tresillo)
key: Musical key (e.g. "Am")
bpm: Tempo for sample selection
bars: Number of bars
selector: Optional SampleSelector; if provided, sets audio_path on ClipDef
"""
note_list = generator_fn(key=key, bars=bars)
midi_notes = melodic_to_midi(note_list)
audio_path: str | None = None
if selector is not None:
match = selector.select_one(role=role, key=key, bpm=bpm)
if match:
audio_path = match.get("original_path", None)
clip = ClipDef(
position=0.0,
length=bars * 4.0,
name=f"{role.capitalize()} MIDI",
audio_path=audio_path,
midi_notes=midi_notes,
)
return TrackDef(name=role.capitalize(), clips=[clip])
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Compose a REAPER .rpp project from the sample library."
)
parser.add_argument(
"--genre",
default="reggaeton",
help="Genre (default: reggaeton)",
)
parser.add_argument(
"--bpm",
type=float,
default=95.0,
help="BPM (default: 95)",
)
parser.add_argument(
"--key",
default="Am",
help="Musical key (default: Am)",
)
parser.add_argument(
"--output",
default="output/track.rpp",
help="Output .rpp path (default: output/track.rpp)",
)
parser.add_argument(
"--render",
action="store_true",
help="Render the project to WAV after generating the .rpp file.",
)
parser.add_argument(
"--render-output",
default=None,
help="Output WAV path for rendering. Defaults to <output>.wav with .rpp extension replaced.",
)
args = parser.parse_args()
genre_file = KNOWLEDGE_DIR / f"{args.genre}.json"
if not genre_file.exists():
print(json.dumps({"error": f"Genre not found: {genre_file}", "available": [p.stem for p in KNOWLEDGE_DIR.glob("*.json")]}))
# Validate BPM before any writes
if args.bpm <= 0:
raise ValueError(f"bpm must be > 0, got {args.bpm}")
# Ensure output directory exists
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Load sample index (for melodic tracks that use audio samples)
index_path = _ROOT / "data" / "sample_index.json"
if not index_path.exists():
print(f"ERROR: sample index not found at {index_path}", file=sys.stderr)
sys.exit(1)
overrides = {}
if args.key:
overrides["keys"] = [args.key]
if args.bpm:
overrides["bpm"] = {"default": args.bpm}
if args.bars:
overrides["structure"] = {"sections": [{"bars": args.bars}]}
selector = SampleSelector(str(index_path))
composition = compose_from_genre(str(genre_file), overrides if overrides else None)
project = build_project(composition)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
output_path = args.output or str(
OUTPUT_DIR / f"{args.genre}_{composition['meta']['key']}_{composition['meta']['bpm']}bpm.flp"
)
writer = FLPWriter(project)
writer.write(output_path)
result = {
"status": "ok",
"output": output_path,
"genre": args.genre,
"key": composition["meta"]["key"],
"bpm": composition["meta"]["bpm"],
"chord_progression": composition["meta"]["chord_progression"],
"tracks": [
{"role": t["role"], "notes": len(t.get("notes", []))}
for t in composition["tracks"]
],
"channel_names": [ch.name for ch in project.channels],
"total_notes": sum(len(n) for t in composition["tracks"] for n in t.get("notes", [])),
# Determine bar count from genre
genre_bar_map = {
"reggaeton": 64,
"trap": 32,
"house": 64,
"drill": 32,
}
print(json.dumps(result, indent=2, ensure_ascii=False))
bar_count = genre_bar_map.get(args.genre.lower(), 48)
# Build drum tracks (no selector needed)
drum_tracks = [
build_drum_track("kick", "kick_main_notes", bar_count),
build_drum_track("snare", "snare_verse_notes", bar_count),
build_drum_track("hihat", "hihat_16th_notes", bar_count),
build_drum_track("perc", "perc_combo_notes", bar_count),
]
# Build melodic tracks (selector passed only to bass)
melodic_tracks = [
build_melodic_track("bass", bass_tresillo, args.key, args.bpm, bar_count, selector),
build_melodic_track("lead", lead_hook, args.key, args.bpm, bar_count),
build_melodic_track("chords", chords_block, args.key, args.bpm, bar_count),
build_melodic_track("pad", pad_sustain, args.key, args.bpm, bar_count),
]
# Assemble full track list
all_tracks = drum_tracks + melodic_tracks
# Build SongDefinition
meta = SongMeta(bpm=args.bpm, key=args.key, title=f"{args.genre.capitalize()} Track")
song = SongDefinition(meta=meta, tracks=all_tracks)
# Validate
errors = song.validate()
if errors:
print(f"WARNING: SongDefinition has validation errors:", file=sys.stderr)
for e in errors:
print(f" - {e}", file=sys.stderr)
# Write .rpp
builder = RPPBuilder(song)
builder.write(str(output_path))
# Render if requested
if args.render:
render_output_path = args.render_output
if render_output_path is None:
render_output_path = str(output_path).replace('.rpp', '.wav')
render_project(str(output_path), render_output_path)
print(str(output_path.resolve()))
if __name__ == "__main__":
main()
main()

View File

@@ -1,239 +0,0 @@
#!/usr/bin/env python
"""compose_full_track.py — Genera un FLP reggaeton completo de 2:30."""
from pathlib import Path
# ── resolve project root ──────────────────────────────────────────────────────
PROJECT = Path(__file__).resolve().parents[1]
import sys
sys.path.insert(0, str(PROJECT))
from src.selector import SampleSelector
from src.composer.melodic import bass_tresillo, lead_hook, pad_sustain
from src.flp_builder.schema import (
SongDefinition, SongMeta, PatternDef, ArrangementTrack,
ArrangementItemDef, MelodicTrack, MelodicNote
)
from src.flp_builder.builder import FLPBuilder
# ── timing ────────────────────────────────────────────────────────────────────
BPM = 95
KEY = "Am"
BARS_TOTAL = 60
BAR_DURATION_S = 4 / BPM * 60 # 2.526s/bar
SONG_DURATION_S = BARS_TOTAL * BAR_DURATION_S
SONG_DURATION_M = int(SONG_DURATION_S // 60), int(SONG_DURATION_S % 60)
print(f"Target: {BARS_TOTAL} bars @ {BPM} BPM = {SONG_DURATION_M[0]}:{SONG_DURATION_M[1]:02d}")
# ── sample selector ────────────────────────────────────────────────────────────
sel = SampleSelector()
def pick(role, **kwargs):
"""Select best sample, log warning if missing."""
m = sel.select_one(role=role, bpm=BPM, **kwargs)
if m:
return m.get("new_name") or m.get("original_name"), m.get("original_path") or ""
print(f" [WARN] No sample for role='{role}' {kwargs}")
return None, None
# Drum channels (10-16)
ch10_perc, path10 = pick("perc")
ch11_kick, path11 = pick("kick")
ch12_snare, path12 = pick("snare")
ch13_rim, path13 = pick("perc") # fallback to perc
ch14_perc2, path14 = pick("perc", character="aggressive")
ch15_hihat, path15 = pick("hihat")
ch16_clap, path16 = pick("snare", character="aggressive")
print("Drum samples:")
for ch, name in [(10, ch10_perc),(11, ch11_kick),(12, ch12_snare),
(13, ch13_rim),(14, ch14_perc2),(15, ch15_hihat),(16, ch16_clap)]:
print(f" ch{ch}: {name}")
# Melodic samples
ch17_bass_path = sel.select_one(role="bass", key=KEY, bpm=BPM, character="deep")["original_path"]
ch18_lead_path = sel.select_one(role="lead", key=KEY, bpm=BPM, character="bright")["original_path"]
ch19_pad_path = sel.select_one(role="pad", key=KEY, bpm=BPM, character="warm")["original_path"]
ch20_pluck_path = sel.select_one(role="pluck", key=KEY, bpm=BPM, character="warm")["original_path"]
print(f"\nMelodic samples:")
for ch, path in [(17, ch17_bass_path),(18, ch18_lead_path),(19, ch19_pad_path),(20, ch20_pluck_path)]:
print(f" ch{ch}: {Path(path).name}")
# ── helpers ───────────────────────────────────────────────────────────────────
def section_notes(generator_fn, key, bars, start_bar, **kwargs):
"""Generate notes from a melodic generator, offset to start_bar."""
raw = generator_fn(key, bars=bars, **kwargs)
return [
MelodicNote(
pos=n["pos"] + start_bar * 4.0,
len=n["len"],
key=n["key"],
vel=n["vel"]
)
for n in raw
]
def place_items(pattern_id, start_bar, total_bars, track_idx, pat_bars=4):
"""Generate ArrangementItemDefs to fill total_bars with pat_bars chunks."""
items = []
for b in range(0, total_bars, pat_bars):
items.append(ArrangementItemDef(
pattern=pattern_id,
bar=start_bar + b,
bars=pat_bars,
track=track_idx,
))
return items
# ── melodic notes (only in sections where they play) ─────────────────────────
print("\nGenerating melodic notes...")
# Bass: verse1, chorus1, verse2, chorus2 (not intro/outro)
bass_notes = (
section_notes(bass_tresillo, KEY, 12, 8, octave=3) +
section_notes(bass_tresillo, KEY, 12, 20, octave=3) +
section_notes(bass_tresillo, KEY, 12, 32, octave=3) +
section_notes(bass_tresillo, KEY, 12, 44, octave=3)
)
print(f" Bass: {len(bass_notes)} notes")
# Lead: chorus1 + chorus2 only
lead_notes = (
section_notes(lead_hook, KEY, 12, 20, octave=5) +
section_notes(lead_hook, KEY, 12, 44, octave=5)
)
print(f" Lead: {len(lead_notes)} notes")
# Pad: verse1, chorus1, verse2, chorus2
pad_notes = (
section_notes(pad_sustain, KEY, 12, 8, octave=4) +
section_notes(pad_sustain, KEY, 12, 20, octave=4) +
section_notes(pad_sustain, KEY, 12, 32, octave=4) +
section_notes(pad_sustain, KEY, 12, 44, octave=4)
)
print(f" Pad: {len(pad_notes)} notes")
# Pluck: verse1 + verse2 (harmonic fill)
pluck_notes = (
section_notes(lead_hook, KEY, 12, 8, octave=5, density=0.4) +
section_notes(lead_hook, KEY, 12, 32, octave=5, density=0.4)
)
print(f" Pluck: {len(pluck_notes)} notes")
# ── SongDefinition ─────────────────────────────────────────────────────────────
meta = SongMeta(bpm=BPM, key=KEY, title=f"Reggaeton Full {SONG_DURATION_M[0]}:{SONG_DURATION_M[1]:02d}")
patterns = [
PatternDef(id=1, name="kick_sparse", instrument="kick", channel=11, bars=4, generator="kick_sparse_notes", velocity_mult=0.7),
PatternDef(id=2, name="kick_main", instrument="kick", channel=11, bars=4, generator="kick_main_notes"),
PatternDef(id=3, name="snare_main", instrument="snare", channel=12, bars=4, generator="snare_verse_notes"),
PatternDef(id=4, name="hihat_main", instrument="hihat", channel=15, bars=4, generator="hihat_16th_notes"),
PatternDef(id=5, name="clap_main", instrument="clap", channel=16, bars=4, generator="clap_24_notes"),
PatternDef(id=6, name="perc_main", instrument="perc", channel=10, bars=4, generator="perc_combo_notes"),
PatternDef(id=7, name="perc2_main", instrument="perc", channel=14, bars=4, generator="perc_combo_notes"),
PatternDef(id=8, name="hihat_intro", instrument="hihat", channel=15, bars=4, generator="hihat_8th_notes", velocity_mult=0.8),
]
tracks = [
ArrangementTrack(index=1, name="Kick"),
ArrangementTrack(index=2, name="Snare"),
ArrangementTrack(index=3, name="HiHat"),
ArrangementTrack(index=4, name="Clap"),
ArrangementTrack(index=5, name="Perc"),
ArrangementTrack(index=6, name="Perc2"),
]
# ── arrangement items ──────────────────────────────────────────────────────────
items: list[ArrangementItemDef] = []
# INTRO (0-7): kick sparse + hihat 8th
items += place_items(1, 0, 8, 1) # kick sparse
items += place_items(8, 0, 8, 3) # hihat intro (8th notes)
# VERSE 1 (8-19): kick, snare, hihat, perc (no clap)
items += place_items(2, 8, 12, 1) # kick main
items += place_items(3, 8, 12, 2) # snare
items += place_items(4, 8, 12, 3) # hihat
items += place_items(6, 8, 12, 5) # perc1
items += place_items(7, 8, 12, 6) # perc2
# CHORUS 1 (20-31): all drums
items += place_items(2, 20, 12, 1) # kick
items += place_items(3, 20, 12, 2) # snare
items += place_items(4, 20, 12, 3) # hihat
items += place_items(5, 20, 12, 4) # clap
items += place_items(6, 20, 12, 5) # perc1
items += place_items(7, 20, 12, 6) # perc2
# VERSE 2 (32-43)
items += place_items(2, 32, 12, 1) # kick
items += place_items(3, 32, 12, 2) # snare
items += place_items(4, 32, 12, 3) # hihat
items += place_items(6, 32, 12, 5) # perc1
items += place_items(7, 32, 12, 6) # perc2
# CHORUS 2 (44-55)
items += place_items(2, 44, 12, 1) # kick
items += place_items(3, 44, 12, 2) # snare
items += place_items(4, 44, 12, 3) # hihat
items += place_items(5, 44, 12, 4) # clap
items += place_items(6, 44, 12, 5) # perc1
items += place_items(7, 44, 12, 6) # perc2
# OUTRO (56-59): kick sparse + hihat
items += place_items(1, 56, 4, 1) # kick sparse
items += place_items(8, 56, 4, 3) # hihat intro (8th notes)
print(f"\nArrangement: {len(items)} items placed")
# ── melodic tracks ─────────────────────────────────────────────────────────────
drum_pattern_count = len(patterns) # 8
melodic_tracks = [
MelodicTrack(role="bass", sample_path=ch17_bass_path, notes=bass_notes, channel_index=17, volume=0.85, pan=0.0),
MelodicTrack(role="lead", sample_path=ch18_lead_path, notes=lead_notes, channel_index=18, volume=0.75, pan=0.15),
MelodicTrack(role="pad", sample_path=ch19_pad_path, notes=pad_notes, channel_index=19, volume=0.55, pan=0.0),
MelodicTrack(role="pluck", sample_path=ch20_pluck_path, notes=pluck_notes, channel_index=20, volume=0.65, pan=-0.1),
]
# samples dict for skeleton loader
samples = {
"channel10": ch10_perc,
"channel11": ch11_kick,
"channel12": ch12_snare,
"channel13": ch13_rim,
"channel14": ch14_perc2,
"channel15": ch15_hihat,
"channel16": ch16_clap,
}
song = SongDefinition(
meta=meta,
samples=samples,
patterns=patterns,
tracks=tracks,
items=items,
melodic_tracks=melodic_tracks,
)
# ── build ─────────────────────────────────────────────────────────────────────
print("\nValidating and building...")
errors = song.validate()
if errors:
print("VALIDATION ERRORS:")
for e in errors:
print(f" - {e}")
sys.exit(1)
builder = FLPBuilder()
flp_bytes = builder.build(song)
out_path = PROJECT / "output" / "reggaeton_full.flp"
out_path.parent.mkdir(exist_ok=True)
out_path.write_bytes(flp_bytes)
size_kb = len(flp_bytes) / 1024
print(f"\nOK {out_path}")
print(f" {size_kb:.1f} KB -- {BARS_TOTAL} bars @ {BPM} BPM = {SONG_DURATION_M[0]}:{SONG_DURATION_M[1]:02d}")

View File

@@ -1,251 +0,0 @@
#!/usr/bin/env python
"""compose_track.py — CLI para generar un .flp reggaeton completo desde cero."""
import argparse
import sys
from pathlib import Path
# Agregar project root al path
sys.path.insert(0, str(Path(__file__).parents[1]))
from src.selector import SampleSelector
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
from src.composer.rhythm import get_notes
from src.flp_builder.schema import (
SongDefinition, SongMeta, PatternDef, ArrangementTrack,
ArrangementItemDef, MelodicTrack, MelodicNote
)
from src.flp_builder.builder import FLPBuilder
# ---------------------------------------------------------------------------
# Drum track configuration
# ---------------------------------------------------------------------------
DRUM_SAMPLE_KEYS = ["channel10", "channel11", "channel12", "channel13",
"channel14", "channel15", "channel16"]
DRUM_ROLES = ["perc", "kick", "snare", "rim",
"perc", "hihat", "clap"]
DRUM_CHANNELS = [10, 11, 12, 13,
14, 15, 16 ]
# Pattern definitions for drum tracks (1 pattern per relevant drum instrument)
DRUM_PATTERNS = [
# id, name, instrument, channel, generator
(1, "Kick Main", "kick", 11, "kick_main_notes"),
(2, "Snare Basic", "snare", 12, "snare_verse_notes"),
(3, "Hihat Straight","hihat", 15, "hihat_16th_notes"),
(4, "Clap On2and4", "clap", 16, "clap_24_notes"),
(5, "Perc Sparse", "perc", 10, "perc_combo_notes"),
]
# Melodic track configuration: (role, channel_index, volume, pan, generator_fn)
MELODIC_CONFIG = [
("bass", 17, 0.85, 0.0, bass_tresillo),
("lead", 18, 0.75, 0.1, lead_hook),
("pad", 19, 0.60, 0.0, pad_sustain),
("pluck", 20, 0.70, -0.15, lead_hook), # lead_hook with octave=5
]
# ---------------------------------------------------------------------------
# Sampler template — extract once from reference FLP
# ---------------------------------------------------------------------------
def _ensure_sampler_template() -> Path:
"""Extract sampler channel template from reference FLP if not cached."""
project = Path(__file__).parents[1]
template_path = project / "output" / "flstudio_sampler_template.bin"
if template_path.exists():
return template_path
ref_flp = project / "my space ryt" / "my space ryt.flp"
ch11_path = project / "output" / "ch11_kick_template.bin"
# Try ch11_kick_template.bin first (legacy name)
if ch11_path.exists():
return ch11_path
# Extract channel 11 from reference FLP
from src.flp_builder.skeleton import ChannelSkeletonLoader
loader = ChannelSkeletonLoader(str(ref_flp), str(ch11_path), str(project / "output" / "samples"))
segments = loader._extract_channels_raw()
if 11 in segments:
template_path.parent.mkdir(parents=True, exist_ok=True)
template_path.write_bytes(segments[11])
print(f" [OK] Sampler template extracted -> {template_path}")
return template_path
raise FileNotFoundError(
"No sampler template found. "
"Please ensure output/ch11_kick_template.bin exists, "
"or the reference FLP contains channel 11."
)
def _build_sample_path(sample: dict) -> str:
"""Build absolute path to a sample file.
The sample dict has ``original_path`` pointing to the source file.
We map it to the analyzed library path:
librerias/reggaeton/.../role/name.wav →
librerias/analyzed_samples/{role}/{new_name}
"""
role = sample.get("role", "")
new_name = sample.get("new_name", "")
project = Path(__file__).parents[1]
analyzed = project / "librerias" / "analyzed_samples" / role / new_name
if analyzed.exists():
return str(analyzed)
# Fallback: try original_path if it still exists
orig = sample.get("original_path", "")
if orig and Path(orig).exists():
return orig
# Last resort: return analyzed path even if missing (let FLPBuilder handle it)
return str(analyzed)
def main():
parser = argparse.ArgumentParser(
description="Genera un archivo .flp reggaeton completo con drums, bass, lead y pads."
)
parser.add_argument("--key", default="Am", help="Tonalidad (e.g. Am, Dm, Gm)")
parser.add_argument("--bpm", type=float, default=95, help="Tempo BPM")
parser.add_argument("--bars", type=int, default=8, help="Duración en bars")
parser.add_argument("--output", default="output/composed.flp", help="Ruta del .flp de salida")
parser.add_argument("--title", default="Reggaeton Track", help="Título del song")
args = parser.parse_args()
# ---------------------------------------------------------------------------
# 1. Sample selection
# ---------------------------------------------------------------------------
sel = SampleSelector()
samples: dict[str, str] = {}
for key_name, role, ch in zip(DRUM_SAMPLE_KEYS, DRUM_ROLES, DRUM_CHANNELS):
match = sel.select_one(role=role, bpm=args.bpm)
if match:
samples[key_name] = match["new_name"]
else:
samples[key_name] = f"{role}.wav"
# ---------------------------------------------------------------------------
# 2. Drum patterns
# ---------------------------------------------------------------------------
patterns: list[PatternDef] = []
for pid, name, instrument, channel, generator in DRUM_PATTERNS:
patterns.append(PatternDef(
id=pid,
name=name,
instrument=instrument,
channel=channel,
bars=args.bars,
generator=generator,
velocity_mult=1.0,
density=1.0,
))
# ---------------------------------------------------------------------------
# 3. Melodic tracks with sample selection
# ---------------------------------------------------------------------------
melodic_tracks: list[MelodicTrack] = []
for role, ch_idx, vol, pan, generator_fn in MELODIC_CONFIG:
match = sel.select_one(role=role, key=args.key, bpm=args.bpm)
if match is None:
print(f" [WARN] No sample found for role '{role}', skipping.")
continue
# Build notes using the generator
if role == "pluck":
raw_notes = generator_fn(args.key, bars=args.bars, octave=5)
else:
raw_notes = generator_fn(args.key, bars=args.bars)
notes = [
MelodicNote(pos=n["pos"], len=n["len"], key=n["key"], vel=n["vel"])
for n in raw_notes
]
sample_path = _build_sample_path(match)
melodic_tracks.append(MelodicTrack(
role=role,
sample_path=sample_path,
notes=notes,
channel_index=ch_idx,
volume=vol,
pan=pan,
))
# ---------------------------------------------------------------------------
# 4. Arrangement tracks and items
# ---------------------------------------------------------------------------
# Tracks: 1 drum track + 1 per melodic track
tracks: list[ArrangementTrack] = [
ArrangementTrack(index=1, name="Drums"),
]
for i, mt in enumerate(melodic_tracks):
tracks.append(ArrangementTrack(index=2 + i, name=mt.role.capitalize()))
# Items: each drum pattern placed at bar 0
items: list[ArrangementItemDef] = []
for p in patterns:
items.append(ArrangementItemDef(
pattern=p.id,
bar=0.0,
bars=float(args.bars),
track=1, # all drum patterns on the Drums track
muted=False,
))
# Melodic items are added by FLPBuilder._build_arrangement (auto-added at bar 0)
# ---------------------------------------------------------------------------
# Build and save
# ---------------------------------------------------------------------------
meta = SongMeta(
bpm=args.bpm,
key=args.key,
title=args.title,
ppq=96,
time_sig_num=4,
time_sig_den=4,
)
song = SongDefinition(
meta=meta,
samples=samples,
patterns=patterns,
tracks=tracks,
items=items,
melodic_tracks=melodic_tracks,
)
errors = song.validate()
if errors:
print("Validation errors:")
for e in errors:
print(f" - {e}")
sys.exit(1)
# Ensure sampler template exists before building
template_path = _ensure_sampler_template()
project = Path(__file__).parents[1]
builder = FLPBuilder(
ref_flp=str(project / "my space ryt" / "my space ryt.flp"),
ch11_template=str(template_path),
samples_dir=str(project / "librerias" / "analyzed_samples"),
)
flp_bytes = builder.build(song)
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(flp_bytes)
print(f"[OK] FLP generado: {out_path} ({len(flp_bytes):,} bytes)")
print(f" Key: {args.key} | BPM: {args.bpm} | Bars: {args.bars}")
print(f" Patterns: {len(patterns)} drum + {len(melodic_tracks)} melodic")
print(f" Tracks: {len(tracks)}")
if __name__ == "__main__":
main()

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env python
"""Inventory scanner - outputs JSON of all available resources."""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from src.scanner import full_inventory
import json
def main():
inv = full_inventory()
plugins = inv["plugins"]
summary = {
"generators": plugins["generator_names"],
"effects": plugins["effect_names"],
"total_generators": len(plugins["generators"]),
"total_effects": len(plugins["effects"]),
"sample_categories": {
k: len(v) for k, v in inv["samples"]["categories"].items()
},
"total_samples": inv["samples"]["total_files"],
"packs": [
{
"name": p["name"],
"audio": len(p["contents"].get("audio", [])),
"midi": len(p["contents"].get("midi", [])),
}
for p in inv["packs"]["packs"]
],
"vector_store": {
"total": inv["vector_store"]["total"],
"types": inv["vector_store"]["types"],
},
"organized_samples": {},
}
for cat, files in inv["samples"]["categories"].items():
summary["organized_samples"][cat] = [f["name"] for f in files[:20]]
print(json.dumps(summary, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()