feat: add compose_full_track script for 2:30 arrangement generation
This commit is contained in:
239
scripts/compose_full_track.py
Normal file
239
scripts/compose_full_track.py
Normal file
@@ -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}")
|
||||
Reference in New Issue
Block a user