#!/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()