Files
reaper-control/scripts/compose_full_track.py

239 lines
10 KiB
Python

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