diff --git a/scripts/compose_full_track.py b/scripts/compose_full_track.py new file mode 100644 index 0000000..3d4e922 --- /dev/null +++ b/scripts/compose_full_track.py @@ -0,0 +1,239 @@ +#!/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}") \ No newline at end of file