251 lines
9.2 KiB
Python
251 lines
9.2 KiB
Python
#!/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() |