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

251
scripts/compose_track.py Normal file
View File

@@ -0,0 +1,251 @@
#!/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()