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