feat: reggaeton production system with intelligent sample selection and FLP generation
This commit is contained in:
122
scripts/batch_generate.py
Normal file
122
scripts/batch_generate.py
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch FLP generator — produces 50 unique reggaeton FLP+JSON pairs.
|
||||
|
||||
Usage:
|
||||
python scripts/batch_generate.py [--count 50] [--out-dir output/batch]
|
||||
|
||||
Output structure:
|
||||
output/batch_{timestamp}/
|
||||
reggaeton_000_95bpm_Am_i-VII-VI-VII.json
|
||||
reggaeton_000_95bpm_Am_i-VII-VI-VII.flp
|
||||
reggaeton_001_90bpm_Dm_i-iv-VII-III.json
|
||||
...
|
||||
manifest.json ← list of all generated songs with metadata
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
from src.composer.variation import generate_batch
|
||||
from src.flp_builder.builder import FLPBuilder
|
||||
from src.flp_builder.schema import SongDefinition
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filename helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_UNSAFE_RE = re.compile(r'[^\w\-]')
|
||||
|
||||
|
||||
def sanitize_filename(s: str) -> str:
|
||||
"""Replace unsafe filename chars with _."""
|
||||
return _UNSAFE_RE.sub('_', s)
|
||||
|
||||
|
||||
def make_filename(idx: int, song: SongDefinition) -> str:
|
||||
"""Build stem like ``reggaeton_000_95bpm_Am_i_VII_VI_VII`` (no extension)."""
|
||||
prog_safe = sanitize_filename(song.progression_name)
|
||||
return f"reggaeton_{idx:03d}_{song.meta.bpm}bpm_{song.meta.key}_{prog_safe}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifest
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_manifest(songs: list[SongDefinition], filenames: list[str]) -> dict:
|
||||
"""Build manifest dict with per-song metadata."""
|
||||
entries = []
|
||||
for idx, (song, stem) in enumerate(zip(songs, filenames)):
|
||||
bar_count = int(max(item.bar + item.bars for item in song.items))
|
||||
entries.append({
|
||||
"idx": idx,
|
||||
"filename": stem,
|
||||
"bpm": song.meta.bpm,
|
||||
"key": song.meta.key,
|
||||
"progression": song.progression_name,
|
||||
"title": song.meta.title,
|
||||
"bars": bar_count,
|
||||
})
|
||||
return {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"count": len(songs),
|
||||
"songs": entries,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Batch FLP generator")
|
||||
parser.add_argument("--count", type=int, default=50,
|
||||
help="Number of songs to generate (default: 50)")
|
||||
parser.add_argument("--out-dir", default="",
|
||||
help="Output directory (default: output/batch_{timestamp})")
|
||||
args = parser.parse_args()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_dir = Path(args.out_dir) if args.out_dir else Path("output") / f"batch_{timestamp}"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"Generating {args.count} songs -> {out_dir}")
|
||||
|
||||
songs = generate_batch(args.count)
|
||||
builder = FLPBuilder()
|
||||
filenames: list[str] = []
|
||||
|
||||
for idx, song in enumerate(songs):
|
||||
stem = make_filename(idx, song)
|
||||
filenames.append(stem)
|
||||
|
||||
# Write JSON
|
||||
json_path = out_dir / f"{stem}.json"
|
||||
json_path.write_text(song.to_json(), encoding="utf-8")
|
||||
|
||||
# Write FLP
|
||||
flp_path = out_dir / f"{stem}.flp"
|
||||
flp_bytes = builder.build(song)
|
||||
flp_path.write_bytes(flp_bytes)
|
||||
|
||||
bar_count = int(max(item.bar + item.bars for item in song.items))
|
||||
print(f" [{idx+1:>3}/{args.count}] {stem}.flp {len(flp_bytes):>9,}b {bar_count}bars")
|
||||
|
||||
# Write manifest
|
||||
manifest = build_manifest(songs, filenames)
|
||||
(out_dir / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2), encoding="utf-8"
|
||||
)
|
||||
|
||||
total_size = sum((out_dir / f"{f}.flp").stat().st_size for f in filenames)
|
||||
print(f"\nDone. {args.count} FLPs in {out_dir}")
|
||||
print(f" Total size: {total_size:,} bytes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
160
scripts/build.py
Normal file
160
scripts/build.py
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python
|
||||
"""Build an FL Studio project from a composition plan JSON."""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from src.flp_builder.project import FLPProject, Note
|
||||
from src.flp_builder.writer import FLPWriter
|
||||
|
||||
PLUGIN_NAME_MAP = {
|
||||
"Serum 2": "Serum2VST3",
|
||||
"Omnisphere": "Omnisphere",
|
||||
"Kontakt 7": "Kontakt 7",
|
||||
"Diva": "Diva",
|
||||
"Electra": "Electra",
|
||||
"Pigments": "Pigments",
|
||||
"ravity(S)": "ravity(S)",
|
||||
"FL Keys": "FL Keys",
|
||||
"FPC": "FPC",
|
||||
"FLEX": "FLEX",
|
||||
"Sytrus": "Sytrus",
|
||||
"Harmor": "Harmor",
|
||||
"3x Osc": "3x Osc",
|
||||
"DirectWave": "DirectWave",
|
||||
"Fruity DrumSynth Live": "Fruity DrumSynth Live",
|
||||
"Transistor Bass": "Transistor Bass",
|
||||
"Sakura": "Sakura",
|
||||
"Sawer": "Sawer",
|
||||
"Toxic Biohazard": "Toxic Biohazard",
|
||||
"Harmless": "Harmless",
|
||||
"GMS": "GMS",
|
||||
"Minisynth": "Minisynth",
|
||||
"Morphine": "Morphine",
|
||||
"Soundfont Player": "Soundfont Player",
|
||||
}
|
||||
|
||||
OUTPUT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) / "output"
|
||||
|
||||
|
||||
def resolve_plugin(preferred_list):
|
||||
for name in preferred_list:
|
||||
if name in PLUGIN_NAME_MAP:
|
||||
internal = PLUGIN_NAME_MAP[name]
|
||||
is_vst = name in [
|
||||
"Serum 2", "Omnisphere", "Kontakt 7", "Diva",
|
||||
"Electra", "Pigments", "ravity(S)",
|
||||
]
|
||||
return {
|
||||
"internal_name": "Fruity Wrapper" if is_vst else internal,
|
||||
"display_name": name,
|
||||
"is_vst": is_vst,
|
||||
}
|
||||
return {
|
||||
"internal_name": "MIDI Out",
|
||||
"display_name": "MIDI Out",
|
||||
"is_vst": False,
|
||||
}
|
||||
|
||||
|
||||
def build_project(composition: dict) -> FLPProject:
|
||||
meta = composition["meta"]
|
||||
tracks = composition["tracks"]
|
||||
|
||||
project = FLPProject(
|
||||
tempo=meta["bpm"],
|
||||
title=meta.get("title", f"{meta.get('genre', 'Untitled')} - {meta.get('key', 'C')}"),
|
||||
genre=meta.get("genre", ""),
|
||||
fl_version="24.7.1.73",
|
||||
ppq=meta.get("ppq", 96),
|
||||
)
|
||||
|
||||
channel_map = {}
|
||||
for i, track in enumerate(tracks):
|
||||
role = track["role"]
|
||||
plugin_info = resolve_plugin(track.get("preferred_plugins", []))
|
||||
ch = project.add_channel(
|
||||
name=f"{role}_{plugin_info['display_name']}",
|
||||
plugin_internal_name=plugin_info["internal_name"],
|
||||
plugin_display_name=plugin_info["display_name"],
|
||||
mixer_track=track.get("mixer_slot", i),
|
||||
channel_type=2,
|
||||
)
|
||||
channel_map[role] = ch.index
|
||||
|
||||
bars = meta.get("bars", 8)
|
||||
ppq = meta.get("ppq", 96)
|
||||
beats_per_chord = meta.get("beats_per_chord", 4)
|
||||
|
||||
for section_idx, track in enumerate(tracks):
|
||||
role = track["role"]
|
||||
ch_idx = channel_map.get(role, 0)
|
||||
raw_notes = track.get("notes", [])
|
||||
|
||||
if not raw_notes:
|
||||
continue
|
||||
|
||||
pat = project.add_pattern(name=f"{role}")
|
||||
for n in raw_notes:
|
||||
note = Note(
|
||||
position=n["position"],
|
||||
length=n["length"],
|
||||
key=n.get("key", 60),
|
||||
velocity=n.get("velocity", 100),
|
||||
pan=n.get("pan", 0),
|
||||
mod_x=n.get("mod_x", 0),
|
||||
mod_y=n.get("mod_y", 0),
|
||||
)
|
||||
pat.add_note(ch_idx, note)
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Build FL Studio project from composition plan")
|
||||
parser.add_argument("plan", help="Path to composition plan JSON")
|
||||
parser.add_argument("--output", "-o", help="Output .flp file path", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.plan, "r", encoding="utf-8") as f:
|
||||
composition = json.load(f)
|
||||
|
||||
project = build_project(composition)
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.output:
|
||||
output_path = args.output
|
||||
else:
|
||||
genre = composition["meta"].get("genre", "track")
|
||||
key = composition["meta"].get("key", "C")
|
||||
bpm = composition["meta"].get("bpm", 140)
|
||||
output_path = str(OUTPUT_DIR / f"{genre}_{key}_{bpm}bpm.flp")
|
||||
|
||||
writer = FLPWriter(project)
|
||||
writer.write(output_path)
|
||||
|
||||
result = {
|
||||
"status": "ok",
|
||||
"output": output_path,
|
||||
"tempo": project.tempo,
|
||||
"channels": len(project.channels),
|
||||
"patterns": len(project.patterns),
|
||||
"channel_names": [ch.name for ch in project.channels],
|
||||
"pattern_names": [p.name for p in project.patterns],
|
||||
"total_notes": sum(
|
||||
len(notes)
|
||||
for pat in project.patterns
|
||||
for notes in pat.notes.values()
|
||||
),
|
||||
}
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
436
scripts/build_complete_reggaeton.py
Normal file
436
scripts/build_complete_reggaeton.py
Normal file
@@ -0,0 +1,436 @@
|
||||
"""
|
||||
Build a COMPLETE reggaeton FLP with drums + melodic MIDI patterns.
|
||||
|
||||
Strategy:
|
||||
1. Load 20 sampler channels from reference FLP (ChannelSkeletonLoader)
|
||||
2. Melodic MIDI notes go on existing EMPTY channels (3, 4, 8, 17)
|
||||
which are empty samplers — user assigns VST plugins in FL Studio.
|
||||
3. Build 14 patterns with drum generators + inline melodic generators
|
||||
4. Build 36-bar arrangement (~1:31 at 95 BPM)
|
||||
5. Assemble identically to proven v15 builder — 20 channels, no VST hacks.
|
||||
|
||||
Output: output/reggaeton_completo.flp
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────────────
|
||||
BASE = r"C:\Users\Administrator\Documents\fl_control"
|
||||
SAMPLES_DIR = os.path.join(BASE, "output", "samples")
|
||||
CH11_TMPL = os.path.join(BASE, "output", "ch11_kick_template.bin")
|
||||
REF_FLP = os.path.join(BASE, r"my space ryt\my space ryt.flp")
|
||||
FLP_OUT = os.path.join(BASE, "output", "reggaeton_completo.flp")
|
||||
|
||||
sys.path.insert(0, BASE)
|
||||
|
||||
from src.flp_builder.events import (
|
||||
EventID,
|
||||
encode_text_event,
|
||||
encode_word_event,
|
||||
encode_data_event,
|
||||
encode_byte_event,
|
||||
encode_notes_block,
|
||||
)
|
||||
from src.flp_builder.skeleton import ChannelSkeletonLoader
|
||||
from src.flp_builder.arrangement import (
|
||||
ArrangementItem,
|
||||
build_arrangement_section,
|
||||
build_track_data_template,
|
||||
)
|
||||
from src.composer.rhythm import get_notes
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────────
|
||||
BPM = 95
|
||||
PPQ = 96
|
||||
|
||||
# Channel indices — drums (from rhythm.py)
|
||||
CH_P1 = 10; CH_K = 11; CH_S = 12; CH_R = 13
|
||||
CH_P2 = 14; CH_H = 15; CH_CL = 16
|
||||
|
||||
# Channel indices — melodic (reuse empty sampler channels from reference)
|
||||
# Ch 3, 4, 8, 17 are empty samplers (no sample loaded, cloned from ch11 tmpl)
|
||||
# MIDI notes go here — user assigns VSTs manually in FL Studio
|
||||
CH_808 = 3
|
||||
CH_PIANO = 4
|
||||
CH_LEAD = 8
|
||||
CH_PAD = 17
|
||||
|
||||
|
||||
# ── Chord Progression: Am → G → F → G (each chord = 2 bars = 8 beats) ──────
|
||||
PROGRESSION = [
|
||||
{
|
||||
"name": "Am",
|
||||
"bass_root": 45, # A2
|
||||
"chord": [57, 60, 64], # A3, C4, E4
|
||||
"pad": [45, 48, 52], # A2, C3, E3
|
||||
"lead_root": 69, # A4
|
||||
},
|
||||
{
|
||||
"name": "G",
|
||||
"bass_root": 43, # G2
|
||||
"chord": [55, 59, 62], # G3, B3, D4
|
||||
"pad": [43, 47, 50], # G2, B2, D3
|
||||
"lead_root": 67, # G4
|
||||
},
|
||||
{
|
||||
"name": "F",
|
||||
"bass_root": 41, # F2
|
||||
"chord": [53, 57, 60], # F3, A3, C4
|
||||
"pad": [41, 45, 48], # F2, A2, C3
|
||||
"lead_root": 65, # F4
|
||||
},
|
||||
{
|
||||
"name": "G",
|
||||
"bass_root": 43,
|
||||
"chord": [55, 59, 62],
|
||||
"pad": [43, 47, 50],
|
||||
"lead_root": 67,
|
||||
},
|
||||
]
|
||||
BEATS_PER_CHORD = 8 # 2 bars per chord
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MELODIC GENERATORS (inline)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _note(pos, length, key, vel):
|
||||
return {"pos": pos, "len": length, "key": key, "vel": vel}
|
||||
|
||||
|
||||
def bass_808_notes(bars):
|
||||
"""808 bass following root notes. Pattern per chord (2 bars):
|
||||
Beat 0: root vel110 dur3 | Beat 3.5: root vel90 dur1.5
|
||||
Beat 5: root vel100 dur2 | Beat 7.5: root vel85 dur0.5
|
||||
"""
|
||||
notes = []
|
||||
total_beats = bars * 4
|
||||
chords_needed = total_beats // BEATS_PER_CHORD
|
||||
for ci in range(chords_needed):
|
||||
ch = PROGRESSION[ci % len(PROGRESSION)]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
root = ch["bass_root"]
|
||||
notes.append(_note(base + 0.0, 3.0, root, 110))
|
||||
notes.append(_note(base + 3.5, 1.5, root, 90))
|
||||
notes.append(_note(base + 5.0, 2.0, root, 100))
|
||||
notes.append(_note(base + 7.5, 0.5, root, 85))
|
||||
return {CH_808: notes}
|
||||
|
||||
|
||||
def piano_stabs_notes(bars):
|
||||
"""Offbeat piano stabs: beats 1.5, 2.5, 3.5, 5.5, 6.5, 7.5 per chord.
|
||||
3-note triads, vel 80-90."""
|
||||
notes = []
|
||||
total_beats = bars * 4
|
||||
chords_needed = total_beats // BEATS_PER_CHORD
|
||||
stab_positions = [1.5, 2.5, 3.5, 5.5, 6.5, 7.5]
|
||||
for ci in range(chords_needed):
|
||||
ch = PROGRESSION[ci % len(PROGRESSION)]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for sp in stab_positions:
|
||||
vel = 80 + (hash((ci, sp)) % 11)
|
||||
for pitch in ch["chord"]:
|
||||
notes.append(_note(base + sp, 0.15, pitch, vel))
|
||||
return {CH_PIANO: notes}
|
||||
|
||||
|
||||
def piano_sparse_notes(bars):
|
||||
"""Sparse piano for intro/breakdown: beats 2.5 and 6.5 only, vel 65-70."""
|
||||
notes = []
|
||||
total_beats = bars * 4
|
||||
chords_needed = total_beats // BEATS_PER_CHORD
|
||||
for ci in range(chords_needed):
|
||||
ch = PROGRESSION[ci % len(PROGRESSION)]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for sp in [2.5, 6.5]:
|
||||
vel = 65 + (hash((ci, sp)) % 6)
|
||||
for pitch in ch["chord"]:
|
||||
notes.append(_note(base + sp, 0.15, pitch, vel))
|
||||
return {CH_PIANO: notes}
|
||||
|
||||
|
||||
def lead_hook_notes(bars):
|
||||
"""Melodic hook emphasizing chord tones per 2-bar cycle."""
|
||||
notes = []
|
||||
total_beats = bars * 4
|
||||
chords_needed = total_beats // BEATS_PER_CHORD
|
||||
for ci in range(chords_needed):
|
||||
ch = PROGRESSION[ci % len(PROGRESSION)]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
lr = ch["lead_root"]
|
||||
notes.append(_note(base + 0.0, 1.0, ch["chord"][0], 100))
|
||||
notes.append(_note(base + 1.0, 0.5, ch["chord"][2], 95))
|
||||
notes.append(_note(base + 2.0, 0.75, ch["chord"][1], 90))
|
||||
notes.append(_note(base + 3.5, 0.25, lr, 85))
|
||||
notes.append(_note(base + 5.0, 0.5, ch["chord"][2], 95))
|
||||
notes.append(_note(base + 5.5, 1.0, ch["chord"][0], 100))
|
||||
notes.append(_note(base + 6.5, 0.5, lr + 2, 80))
|
||||
return {CH_LEAD: notes}
|
||||
|
||||
|
||||
def pad_sustained_notes(bars):
|
||||
"""Long sustained pad chords. 3 notes per chord, vel 65, dur 7.5 beats."""
|
||||
notes = []
|
||||
total_beats = bars * 4
|
||||
chords_needed = total_beats // BEATS_PER_CHORD
|
||||
for ci in range(chords_needed):
|
||||
ch = PROGRESSION[ci % len(PROGRESSION)]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for pitch in ch["pad"]:
|
||||
notes.append(_note(base + 0.0, 7.5, pitch, 65))
|
||||
return {CH_PAD: notes}
|
||||
|
||||
|
||||
MELODIC_GENERATORS = {
|
||||
"bass_808_notes": bass_808_notes,
|
||||
"piano_stabs_notes": piano_stabs_notes,
|
||||
"piano_sparse_notes": piano_sparse_notes,
|
||||
"lead_hook_notes": lead_hook_notes,
|
||||
"pad_sustained_notes": pad_sustained_notes,
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# PATTERN DEFINITIONS
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PATTERNS = [
|
||||
{"id": 1, "name": "Kick Main", "generator": "kick_main_notes", "bars": 8},
|
||||
{"id": 2, "name": "Kick Sparse", "generator": "kick_sparse_notes", "bars": 8},
|
||||
{"id": 3, "name": "Snare Verse", "generator": "snare_verse_notes", "bars": 8},
|
||||
{"id": 4, "name": "Hihat 16th", "generator": "hihat_16th_notes", "bars": 8},
|
||||
{"id": 5, "name": "Hihat 8th", "generator": "hihat_8th_notes", "bars": 8},
|
||||
{"id": 6, "name": "Clap 24", "generator": "clap_24_notes", "bars": 8},
|
||||
{"id": 7, "name": "Rim Build", "generator": "rim_build_notes", "bars": 4},
|
||||
{"id": 8, "name": "Perc Combo", "generator": "perc_combo_notes", "bars": 8},
|
||||
{"id": 9, "name": "Kick Outro", "generator": "kick_outro_notes", "bars": 8},
|
||||
{"id": 10, "name": "808 Bass", "generator": "bass_808_notes", "bars": 8, "melodic": True},
|
||||
{"id": 11, "name": "Piano Stabs", "generator": "piano_stabs_notes", "bars": 8, "melodic": True},
|
||||
{"id": 12, "name": "Piano Sparse", "generator": "piano_sparse_notes","bars": 8, "melodic": True},
|
||||
{"id": 13, "name": "Lead Hook", "generator": "lead_hook_notes", "bars": 8, "melodic": True},
|
||||
{"id": 14, "name": "Pad Sustained","generator": "pad_sustained_notes","bars": 8, "melodic": True},
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# ARRANGEMENT (36 bars = ~1:31 at 95 BPM)
|
||||
# 9 arrangement tracks
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ARRANGEMENT_ITEMS = [
|
||||
# INTRO (0-4)
|
||||
{"pattern": 2, "bar": 0, "bars": 4, "track": 0},
|
||||
{"pattern": 5, "bar": 0, "bars": 4, "track": 2},
|
||||
{"pattern": 14, "bar": 0, "bars": 4, "track": 8},
|
||||
{"pattern": 12, "bar": 0, "bars": 4, "track": 6},
|
||||
# VERSE (4-12)
|
||||
{"pattern": 1, "bar": 4, "bars": 8, "track": 0},
|
||||
{"pattern": 3, "bar": 4, "bars": 8, "track": 1},
|
||||
{"pattern": 4, "bar": 4, "bars": 8, "track": 2},
|
||||
{"pattern": 8, "bar": 4, "bars": 8, "track": 4},
|
||||
{"pattern": 10, "bar": 4, "bars": 8, "track": 5},
|
||||
{"pattern": 11, "bar": 4, "bars": 8, "track": 6},
|
||||
{"pattern": 14, "bar": 4, "bars": 8, "track": 8},
|
||||
# PRE-CHORUS (12-16)
|
||||
{"pattern": 1, "bar": 12, "bars": 4, "track": 0},
|
||||
{"pattern": 3, "bar": 12, "bars": 4, "track": 1},
|
||||
{"pattern": 4, "bar": 12, "bars": 4, "track": 2},
|
||||
{"pattern": 7, "bar": 12, "bars": 4, "track": 3},
|
||||
{"pattern": 8, "bar": 12, "bars": 4, "track": 4},
|
||||
{"pattern": 10, "bar": 12, "bars": 4, "track": 5},
|
||||
{"pattern": 11, "bar": 12, "bars": 4, "track": 6},
|
||||
{"pattern": 14, "bar": 12, "bars": 4, "track": 8},
|
||||
# CHORUS (16-24)
|
||||
{"pattern": 1, "bar": 16, "bars": 8, "track": 0},
|
||||
{"pattern": 3, "bar": 16, "bars": 8, "track": 1},
|
||||
{"pattern": 4, "bar": 16, "bars": 8, "track": 2},
|
||||
{"pattern": 6, "bar": 16, "bars": 8, "track": 3},
|
||||
{"pattern": 8, "bar": 16, "bars": 8, "track": 4},
|
||||
{"pattern": 10, "bar": 16, "bars": 8, "track": 5},
|
||||
{"pattern": 11, "bar": 16, "bars": 8, "track": 6},
|
||||
{"pattern": 13, "bar": 16, "bars": 8, "track": 7},
|
||||
{"pattern": 14, "bar": 16, "bars": 8, "track": 8},
|
||||
# BREAKDOWN (24-28)
|
||||
{"pattern": 5, "bar": 24, "bars": 4, "track": 2},
|
||||
{"pattern": 14, "bar": 24, "bars": 4, "track": 8},
|
||||
{"pattern": 12, "bar": 24, "bars": 4, "track": 6},
|
||||
# OUTRO (28-36)
|
||||
{"pattern": 9, "bar": 28, "bars": 8, "track": 0},
|
||||
{"pattern": 3, "bar": 28, "bars": 8, "track": 1},
|
||||
{"pattern": 4, "bar": 28, "bars": 8, "track": 2},
|
||||
{"pattern": 14, "bar": 28, "bars": 8, "track": 8},
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# HEADER BUILDER
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _read_ev(data, pos):
|
||||
s = pos
|
||||
ib = data[pos]
|
||||
pos += 1
|
||||
if ib < 64:
|
||||
return pos + 1, s, ib, data[s + 1], "byte"
|
||||
elif ib < 128:
|
||||
return pos + 2, s, ib, struct.unpack("<H", data[pos:pos + 2])[0], "word"
|
||||
elif ib < 192:
|
||||
return pos + 4, s, ib, struct.unpack("<I", data[pos:pos + 4])[0], "dword"
|
||||
else:
|
||||
sz = 0; sh = 0
|
||||
while True:
|
||||
b = data[pos]; pos += 1
|
||||
sz |= (b & 0x7F) << sh; sh += 7
|
||||
if not (b & 0x80): break
|
||||
return pos + sz, s, ib, data[pos:pos + sz], "data"
|
||||
|
||||
|
||||
def build_header(ref_bytes):
|
||||
"""Extract header events from reference FLP, patch BPM to 95."""
|
||||
pos = 22
|
||||
first_pat = None
|
||||
while pos < len(ref_bytes):
|
||||
np, st, ib, val, vt = _read_ev(ref_bytes, pos)
|
||||
if ib == EventID.PatNew:
|
||||
first_pat = st
|
||||
break
|
||||
pos = np
|
||||
|
||||
if first_pat is None:
|
||||
raise ValueError("No PatNew event found in reference FLP")
|
||||
|
||||
header = bytearray(ref_bytes[22:first_pat])
|
||||
|
||||
# Patch BPM
|
||||
p = 0
|
||||
while p < len(header):
|
||||
np, _, ib, val, vt = _read_ev(bytes(header), p)
|
||||
if ib == EventID.Tempo:
|
||||
struct.pack_into("<I", header, p + 1, BPM * 1000)
|
||||
break
|
||||
p = np
|
||||
|
||||
return bytes(header)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# PATTERN BUILDER
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _convert_rhythm_notes(notes):
|
||||
return [
|
||||
{"position": n["pos"], "length": n["len"], "key": n["key"], "velocity": n["vel"]}
|
||||
for n in notes
|
||||
]
|
||||
|
||||
|
||||
def build_all_patterns():
|
||||
buf = bytearray()
|
||||
for pat_def in PATTERNS:
|
||||
pat_id = pat_def["id"]
|
||||
gen_name = pat_def["generator"]
|
||||
bars = pat_def["bars"]
|
||||
is_melodic = pat_def.get("melodic", False)
|
||||
|
||||
buf += encode_word_event(EventID.PatNew, pat_id - 1)
|
||||
buf += encode_text_event(EventID.PatName, pat_def["name"])
|
||||
|
||||
if is_melodic:
|
||||
notes_by_channel = MELODIC_GENERATORS[gen_name](bars)
|
||||
else:
|
||||
notes_by_channel = get_notes(gen_name, bars)
|
||||
|
||||
for ch_idx, raw_notes in notes_by_channel.items():
|
||||
if not raw_notes:
|
||||
continue
|
||||
converted = _convert_rhythm_notes(raw_notes)
|
||||
buf += encode_data_event(
|
||||
EventID.PatNotes,
|
||||
encode_notes_block(ch_idx, converted, PPQ),
|
||||
)
|
||||
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN BUILD — identical assembly to proven v15 builder
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_complete_reggaeton():
|
||||
print("=" * 60)
|
||||
print("Building COMPLETE reggaeton FLP (drums + melodic MIDI)")
|
||||
print("=" * 60)
|
||||
|
||||
for p in [REF_FLP, CH11_TMPL]:
|
||||
assert os.path.isfile(p), f"MISSING: {p}"
|
||||
|
||||
ref_bytes = open(REF_FLP, "rb").read()
|
||||
num_channels = struct.unpack("<H", ref_bytes[10:12])[0]
|
||||
print(f"Reference FLP: {len(ref_bytes):,} bytes, {num_channels} channels")
|
||||
|
||||
# 1. Load sampler channels (identical to v15)
|
||||
print("\n[1/4] Loading sampler channels...")
|
||||
loader = ChannelSkeletonLoader(REF_FLP, CH11_TMPL, SAMPLES_DIR)
|
||||
sample_map = {
|
||||
"perc1": "perc1.wav", "kick": "kick.wav", "snare": "snare.wav",
|
||||
"rim": "rim.wav", "perc2": "perc2.wav", "hihat": "hihat.wav",
|
||||
"clap": "clap.wav",
|
||||
}
|
||||
channel_bytes = loader.load(sample_map)
|
||||
print(f" Channels: {len(channel_bytes):,} bytes ({num_channels} channels)")
|
||||
|
||||
# 2. Build header + patterns
|
||||
print("\n[2/4] Building header + patterns...")
|
||||
header_bytes = build_header(ref_bytes)
|
||||
pattern_bytes = build_all_patterns()
|
||||
print(f" Header: {len(header_bytes):,} bytes")
|
||||
print(f" Patterns: {len(pattern_bytes):,} bytes ({len(PATTERNS)} patterns)")
|
||||
|
||||
# 3. Build arrangement
|
||||
print("\n[3/4] Building arrangement...")
|
||||
track_data_template = build_track_data_template(ref_bytes)
|
||||
items = [
|
||||
ArrangementItem(
|
||||
pattern_id=it["pattern"], bar=it["bar"],
|
||||
num_bars=it["bars"], track_index=it["track"],
|
||||
)
|
||||
for it in ARRANGEMENT_ITEMS
|
||||
]
|
||||
arrangement_bytes = build_arrangement_section(items, track_data_template, ppq=PPQ)
|
||||
print(f" Arrangement: {len(arrangement_bytes):,} bytes ({len(items)} items)")
|
||||
|
||||
# 4. Assemble — identical to v15 builder
|
||||
print("\n[4/4] Assembling FLP...")
|
||||
body = header_bytes + pattern_bytes + channel_bytes + arrangement_bytes
|
||||
|
||||
flp = (
|
||||
struct.pack("<4sIhHH", b"FLhd", 6, 0, num_channels, PPQ)
|
||||
+ b"FLdt" + struct.pack("<I", len(body))
|
||||
+ body
|
||||
)
|
||||
|
||||
os.makedirs(os.path.dirname(FLP_OUT), exist_ok=True)
|
||||
with open(FLP_OUT, "wb") as f:
|
||||
f.write(flp)
|
||||
|
||||
duration = (36 * 4 / BPM) * 60
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Output: {FLP_OUT}")
|
||||
print(f"Size: {len(flp):,} bytes")
|
||||
print(f"Duration: ~{duration:.0f}s (36 bars at {BPM} BPM)")
|
||||
print(f"Channels: {num_channels} (unchanged from reference)")
|
||||
print(f"Patterns: {len(PATTERNS)} (9 drums + 5 melodic)")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
print("MELODIC CHANNELS (assign VSTs manually in FL Studio):")
|
||||
print(f" Ch {CH_808}: 808 Bass -> Serum2")
|
||||
print(f" Ch {CH_PIANO}: Piano -> Pigments")
|
||||
print(f" Ch {CH_LEAD}: Lead -> Serum2")
|
||||
print(f" Ch {CH_PAD}: Pad -> Omnisphere")
|
||||
|
||||
return flp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_complete_reggaeton()
|
||||
42
scripts/build_from_json.py
Normal file
42
scripts/build_from_json.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI: build a single FLP from a JSON song definition.
|
||||
|
||||
Usage:
|
||||
python scripts/build_from_json.py <song.json> [--out <output.flp>]
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
from src.flp_builder.schema import load_song_json
|
||||
from src.flp_builder.builder import FLPBuilder
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build FLP from JSON song definition"
|
||||
)
|
||||
parser.add_argument("song_json", help="Path to song .json file")
|
||||
parser.add_argument(
|
||||
"--out", help="Output .flp path (default: same name as JSON)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
json_path = Path(args.song_json)
|
||||
out_path = (
|
||||
Path(args.out) if args.out else json_path.with_suffix(".flp")
|
||||
)
|
||||
|
||||
song = load_song_json(json_path)
|
||||
builder = FLPBuilder()
|
||||
flp = builder.build(song)
|
||||
|
||||
out_path.write_bytes(flp)
|
||||
print(f"Built {out_path} ({len(flp):,} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
610
scripts/build_reggaeton_fuego.py
Normal file
610
scripts/build_reggaeton_fuego.py
Normal file
@@ -0,0 +1,610 @@
|
||||
"""
|
||||
Build a PROFESSIONAL reggaeton FLP with REAL SAMPLES from the user's library.
|
||||
|
||||
Key facts:
|
||||
- Only Ch10-19 are sampler channels in the reference FLP (Ch0-9 are VST/plugin)
|
||||
- Each sampler channel loads a real WAV from libreria/reggaeton/
|
||||
- MIDI notes trigger those real samples
|
||||
- 10 channels = kick, snare, hihat, 808, bell, lead, pad, clap, perc, rim
|
||||
|
||||
Sample selection (professional reggaeton):
|
||||
Ch10: kick nes 1 — classic reggaeton kick
|
||||
Ch11: snare nes 1 — clean reggaeton snare
|
||||
Ch12: hi-hat 1 — tight hihat
|
||||
Ch13: Bass Reventado — deep 808 bass (dastin.prod)
|
||||
Ch14: bell 4 — bell tone for chords
|
||||
Ch15: lead 3 — melodic lead
|
||||
Ch16: pad 1 — sustained pad
|
||||
Ch17: clap — reggaeton clap (using snap from perc loop)
|
||||
Ch18: perc 1 — perc one shot
|
||||
Ch19: rim — rim/rimshot
|
||||
|
||||
Output: output/reggaeton_fuego.flp
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────────────
|
||||
BASE = r"C:\Users\Administrator\Documents\fl_control"
|
||||
CH11_TMPL = os.path.join(BASE, "output", "ch11_kick_template.bin")
|
||||
REF_FLP = os.path.join(BASE, r"my space ryt\my space ryt.flp")
|
||||
FLP_OUT = os.path.join(BASE, "output", "reggaeton_fuego.flp")
|
||||
|
||||
# All samples copied here — clean names, no special chars
|
||||
SAMPLES_DIR = os.path.join(BASE, "output", "fuego_samples")
|
||||
|
||||
sys.path.insert(0, BASE)
|
||||
|
||||
from src.flp_builder.events import (
|
||||
EventID,
|
||||
encode_text_event,
|
||||
encode_word_event,
|
||||
encode_data_event,
|
||||
encode_notes_block,
|
||||
)
|
||||
from src.flp_builder.skeleton import ChannelSkeletonLoader
|
||||
from src.flp_builder.arrangement import (
|
||||
ArrangementItem,
|
||||
build_arrangement_section,
|
||||
build_track_data_template,
|
||||
)
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────────
|
||||
BPM = 95
|
||||
PPQ = 96
|
||||
|
||||
# Channel indices — ALL sampler channels (10-19)
|
||||
CH_KICK = 10
|
||||
CH_SNARE = 11
|
||||
CH_HH = 12
|
||||
CH_808 = 13
|
||||
CH_BELL = 14
|
||||
CH_LEAD = 15
|
||||
CH_PAD = 16
|
||||
CH_CLAP = 17
|
||||
CH_PERC = 18
|
||||
CH_RIM = 19
|
||||
|
||||
|
||||
# Sample assignment: ch_idx → (samples_dir, wav_filename)
|
||||
# All samples in fuego_samples/ with clean names
|
||||
SAMPLE_ASSIGNMENT = {
|
||||
CH_KICK: (SAMPLES_DIR, "kick.wav"),
|
||||
CH_SNARE: (SAMPLES_DIR, "snare.wav"),
|
||||
CH_HH: (SAMPLES_DIR, "hihat.wav"),
|
||||
CH_808: (SAMPLES_DIR, "bass_808.wav"),
|
||||
CH_BELL: (SAMPLES_DIR, "bell.wav"),
|
||||
CH_LEAD: (SAMPLES_DIR, "lead.wav"),
|
||||
CH_PAD: (SAMPLES_DIR, "pad.wav"),
|
||||
CH_CLAP: (SAMPLES_DIR, "clap.wav"),
|
||||
CH_PERC: (SAMPLES_DIR, "perc.wav"),
|
||||
CH_RIM: (SAMPLES_DIR, "rim.wav"),
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# FUEGO CHORD PROGRESSION: Am → Dm → F → E
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
PROGRESSION = [
|
||||
{"name": "Am", "bass": 33, "chord": [45,48,52,57], "triad": [57,60,64], "root": 69},
|
||||
{"name": "Dm", "bass": 38, "chord": [50,53,57,62], "triad": [62,65,69], "root": 74},
|
||||
{"name": "F", "bass": 41, "chord": [53,57,60,65], "triad": [65,69,72], "root": 77},
|
||||
{"name": "E", "bass": 40, "chord": [52,56,59,64], "triad": [64,68,71], "root": 76},
|
||||
]
|
||||
BEATS_PER_CHORD = 8
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# DRUM GENERATORS — using correct channel indices
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _n(pos, length, ch, vel):
|
||||
return {"pos": pos, "len": length, "key": 60, "vel": max(1, min(127, vel))}
|
||||
|
||||
|
||||
def dembow_kick(bars, vel_mult=1.0):
|
||||
"""REAL dembow: 0.0, 2.0, 3.25"""
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o, 0.25, CH_KICK, int(120 * vel_mult)))
|
||||
notes.append(_n(o + 2.0, 0.25, CH_KICK, int(110 * vel_mult)))
|
||||
notes.append(_n(o + 3.25, 0.15, CH_KICK, int(90 * vel_mult)))
|
||||
return {CH_KICK: notes}
|
||||
|
||||
|
||||
def perreador_kick(bars, vel_mult=1.0):
|
||||
"""Perreador: every beat + offbeat ghosts."""
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for beat in range(4):
|
||||
notes.append(_n(o + beat, 0.25, CH_KICK, int(115 * vel_mult)))
|
||||
notes.append(_n(o + beat + 0.5, 0.15, CH_KICK, int(80 * vel_mult)))
|
||||
return {CH_KICK: notes}
|
||||
|
||||
|
||||
def sparse_kick(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
notes.append(_n(b * 4.0, 0.25, CH_KICK, int(100 * vel_mult)))
|
||||
return {CH_KICK: notes}
|
||||
|
||||
|
||||
def snare_standard(bars, vel_mult=1.0):
|
||||
"""Snare: beats 2, 3-and (positions 1.25, 3.0)."""
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o + 1.25, 0.15, CH_SNARE, int(105 * vel_mult)))
|
||||
notes.append(_n(o + 3.0, 0.15, CH_SNARE, int(100 * vel_mult)))
|
||||
return {CH_SNARE: notes}
|
||||
|
||||
|
||||
def snare_intense(bars, vel_mult=1.0):
|
||||
"""Intense snare with ghost hits."""
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o + 1.25, 0.15, CH_SNARE, int(110 * vel_mult)))
|
||||
notes.append(_n(o + 1.75, 0.10, CH_SNARE, int(70 * vel_mult)))
|
||||
notes.append(_n(o + 3.0, 0.15, CH_SNARE, int(105 * vel_mult)))
|
||||
notes.append(_n(o + 3.5, 0.10, CH_SNARE, int(65 * vel_mult)))
|
||||
return {CH_SNARE: notes}
|
||||
|
||||
|
||||
def hihat_offbeat(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for i in range(4):
|
||||
notes.append(_n(o + i + 0.5, 0.1, CH_HH, int(55 * vel_mult)))
|
||||
return {CH_HH: notes}
|
||||
|
||||
|
||||
def hihat_8th(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for i in range(8):
|
||||
v = 70 if i % 2 == 0 else 50
|
||||
notes.append(_n(o + i * 0.5, 0.1, CH_HH, int(v * vel_mult)))
|
||||
return {CH_HH: notes}
|
||||
|
||||
|
||||
def hihat_16th(bars, vel_mult=1.0):
|
||||
"""Full 16ths with accents and open hats."""
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for i in range(16):
|
||||
p = i * 0.25
|
||||
if p % 1.0 == 0.0:
|
||||
v, l = 90, 0.1
|
||||
elif p % 0.5 == 0.0:
|
||||
v, l = 65, 0.1
|
||||
else:
|
||||
v, l = 40, 0.08
|
||||
if i in [5, 10]:
|
||||
l = 0.2; v = int(v * 1.2)
|
||||
notes.append(_n(o + p, l, CH_HH, int(v * vel_mult)))
|
||||
return {CH_HH: notes}
|
||||
|
||||
|
||||
def clap_standard(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o + 1.0, 0.15, CH_CLAP, int(120 * vel_mult)))
|
||||
notes.append(_n(o + 3.0, 0.15, CH_CLAP, int(115 * vel_mult)))
|
||||
return {CH_CLAP: notes}
|
||||
|
||||
|
||||
def clap_soft(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o + 1.0, 0.15, CH_CLAP, int(80 * vel_mult)))
|
||||
notes.append(_n(o + 3.0, 0.15, CH_CLAP, int(75 * vel_mult)))
|
||||
return {CH_CLAP: notes}
|
||||
|
||||
|
||||
def perc_offbeat(bars, vel_mult=1.0):
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_n(o + 0.75, 0.1, CH_PERC, int(85 * vel_mult)))
|
||||
notes.append(_n(o + 2.75, 0.1, CH_PERC, int(80 * vel_mult)))
|
||||
return {CH_PERC: notes}
|
||||
|
||||
|
||||
def rim_build(bars, vel_mult=1.0):
|
||||
"""Rim roll building intensity."""
|
||||
PATTERNS = [[0,2,8,14], [0,2,4,8,10,14], [0,2,4,6,8,10,12,14], list(range(16))]
|
||||
VELS = [50, 65, 80, 100]
|
||||
notes = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
v = int(VELS[b % 4] * vel_mult)
|
||||
for idx in PATTERNS[b % 4]:
|
||||
notes.append(_n(o + idx * 0.25, 0.1, CH_RIM, v))
|
||||
return {CH_RIM: notes}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MELODIC GENERATORS
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _mn(pos, length, key, vel):
|
||||
"""Melodic note — pitch matters."""
|
||||
return {"pos": pos, "len": length, "key": key, "vel": max(1, min(127, vel))}
|
||||
|
||||
|
||||
def bass_808_full(bars, vel_mult=1.0):
|
||||
"""808 bass with chord-root movement + fifth variation."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
r = ch["bass"]
|
||||
f = r + 7
|
||||
v = vel_mult
|
||||
notes.append(_mn(base + 0.0, 2.5, r, int(110*v)))
|
||||
notes.append(_mn(base + 2.5, 0.5, f, int(80*v)))
|
||||
notes.append(_mn(base + 3.0, 2.0, r, int(105*v)))
|
||||
notes.append(_mn(base + 5.0, 1.0, r, int(90*v)))
|
||||
notes.append(_mn(base + 6.0, 0.5, f, int(75*v)))
|
||||
notes.append(_mn(base + 6.5, 1.5, r, int(100*v)))
|
||||
return {CH_808: notes}
|
||||
|
||||
|
||||
def bass_808_sparse(bars, vel_mult=1.0):
|
||||
"""Sparse 808 for intro — just root, long sustain."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
notes.append(_mn(ci * BEATS_PER_CHORD, 7.5, ch["bass"], int(60 * vel_mult)))
|
||||
return {CH_808: notes}
|
||||
|
||||
|
||||
def bell_chords(bars, vel_mult=1.0):
|
||||
"""Bell playing offbeat chord stabs — 4-note voicings."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
stabs = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for sp in stabs:
|
||||
v = int((85 + (hash((ci, sp)) % 10)) * vel_mult)
|
||||
for pitch in ch["triad"]:
|
||||
notes.append(_mn(base + sp, 0.12, pitch, v))
|
||||
return {CH_BELL: notes}
|
||||
|
||||
|
||||
def bell_sparse(bars, vel_mult=1.0):
|
||||
"""Sparse bell for intro — 4-note voicings, beats 2.5 and 6.5."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for sp in [2.5, 6.5]:
|
||||
v = int(60 * vel_mult)
|
||||
for pitch in ch["chord"]:
|
||||
notes.append(_mn(base + sp, 0.15, pitch, v))
|
||||
return {CH_BELL: notes}
|
||||
|
||||
|
||||
def lead_hook(bars, vel_mult=1.0):
|
||||
"""Lead melody — arch contour, chord tones on strong beats."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
lr = ch["root"]
|
||||
c = ch["triad"]
|
||||
v = vel_mult
|
||||
notes.append(_mn(base + 0.0, 1.0, c[0], int(95*v)))
|
||||
notes.append(_mn(base + 1.0, 0.5, c[1], int(85*v)))
|
||||
notes.append(_mn(base + 1.5, 0.5, c[2], int(100*v)))
|
||||
notes.append(_mn(base + 2.0, 1.5, lr, int(105*v)))
|
||||
notes.append(_mn(base + 3.5, 0.5, c[2], int(90*v)))
|
||||
notes.append(_mn(base + 4.0, 0.5, c[1], int(80*v)))
|
||||
notes.append(_mn(base + 4.5, 1.5, c[0], int(95*v)))
|
||||
notes.append(_mn(base + 6.0, 0.5, lr-2, int(75*v)))
|
||||
notes.append(_mn(base + 6.5, 1.5, c[0], int(90*v)))
|
||||
return {CH_LEAD: notes}
|
||||
|
||||
|
||||
def pad_sustained(bars, vel_mult=1.0):
|
||||
"""Sustained pad — 4-note voicings."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for pitch in ch["chord"]:
|
||||
notes.append(_mn(base, 7.5, pitch, int(60 * vel_mult)))
|
||||
return {CH_PAD: notes}
|
||||
|
||||
|
||||
def pad_swell(bars, vel_mult=1.0):
|
||||
"""Pad swell for pre-chorus — crescendo within chord."""
|
||||
notes = []
|
||||
total = bars * 4
|
||||
chords = total // BEATS_PER_CHORD
|
||||
for ci in range(chords):
|
||||
ch = PROGRESSION[ci % 4]
|
||||
base = ci * BEATS_PER_CHORD
|
||||
for pitch in ch["chord"]:
|
||||
notes.append(_mn(base, 4.0, pitch, int(45 * vel_mult)))
|
||||
notes.append(_mn(base + 4, 3.5, pitch, int(70 * vel_mult)))
|
||||
return {CH_PAD: notes}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# PATTERN DEFINITIONS — 20 patterns
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# All generators return {ch_idx: [notes]}
|
||||
ALL_GENERATORS = {
|
||||
"dembow_kick": dembow_kick,
|
||||
"perreador_kick": perreador_kick,
|
||||
"sparse_kick": sparse_kick,
|
||||
"snare_std": snare_standard,
|
||||
"snare_intense": snare_intense,
|
||||
"hh_offbeat": hihat_offbeat,
|
||||
"hh_8th": hihat_8th,
|
||||
"hh_16th": hihat_16th,
|
||||
"clap_std": clap_standard,
|
||||
"clap_soft": clap_soft,
|
||||
"perc_offbeat": perc_offbeat,
|
||||
"rim_build": rim_build,
|
||||
"bass_full": bass_808_full,
|
||||
"bass_sparse": bass_808_sparse,
|
||||
"bell_chords": bell_chords,
|
||||
"bell_sparse": bell_sparse,
|
||||
"lead_hook": lead_hook,
|
||||
"pad_sustained": pad_sustained,
|
||||
"pad_swell": pad_swell,
|
||||
}
|
||||
|
||||
PATTERNS = [
|
||||
{"id": 1, "name": "Kick Dembow", "gen": "dembow_kick", "bars": 8},
|
||||
{"id": 2, "name": "Kick Perreador", "gen": "perreador_kick","bars": 8},
|
||||
{"id": 3, "name": "Kick Sparse", "gen": "sparse_kick", "bars": 8},
|
||||
{"id": 4, "name": "Snare Standard", "gen": "snare_std", "bars": 8},
|
||||
{"id": 5, "name": "Snare Intense", "gen": "snare_intense", "bars": 8},
|
||||
{"id": 6, "name": "HH Offbeat", "gen": "hh_offbeat", "bars": 8},
|
||||
{"id": 7, "name": "HH 8th", "gen": "hh_8th", "bars": 8},
|
||||
{"id": 8, "name": "HH 16th Full", "gen": "hh_16th", "bars": 8},
|
||||
{"id": 9, "name": "Clap Standard", "gen": "clap_std", "bars": 8},
|
||||
{"id": 10, "name": "Perc Offbeat", "gen": "perc_offbeat", "bars": 8},
|
||||
{"id": 11, "name": "Rim Build", "gen": "rim_build", "bars": 4},
|
||||
{"id": 12, "name": "808 Bass Full", "gen": "bass_full", "bars": 8},
|
||||
{"id": 13, "name": "808 Bass Sparse", "gen": "bass_sparse", "bars": 8},
|
||||
{"id": 14, "name": "Bell Chords", "gen": "bell_chords", "bars": 8},
|
||||
{"id": 15, "name": "Bell Sparse", "gen": "bell_sparse", "bars": 8},
|
||||
{"id": 16, "name": "Lead Hook", "gen": "lead_hook", "bars": 8},
|
||||
{"id": 17, "name": "Pad Sustained", "gen": "pad_sustained", "bars": 8},
|
||||
{"id": 18, "name": "Pad Swell", "gen": "pad_swell", "bars": 8},
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# ARRANGEMENT — 48 bars, 7 sections
|
||||
# 10 tracks (one per sampler channel Ch10-19)
|
||||
# Track index in arrangement: 0=kick, 1=snare, 2=hh, 3=808, 4=bell,
|
||||
# 5=lead, 6=pad, 7=clap, 8=perc, 9=rim
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ARRANGEMENT_ITEMS = [
|
||||
# INTRO (0-4): ghostly, sparse
|
||||
{"pattern": 3, "bar": 0, "bars": 4, "track": 0}, # sparse kick
|
||||
{"pattern": 6, "bar": 0, "bars": 4, "track": 2}, # offbeat HH
|
||||
{"pattern": 13, "bar": 0, "bars": 4, "track": 3}, # sparse 808
|
||||
{"pattern": 15, "bar": 0, "bars": 4, "track": 4}, # sparse bell
|
||||
{"pattern": 17, "bar": 0, "bars": 4, "track": 6}, # pad sustained
|
||||
|
||||
# VERSE 1 (4-12): warming up
|
||||
{"pattern": 1, "bar": 4, "bars": 8, "track": 0}, # dembow kick
|
||||
{"pattern": 4, "bar": 4, "bars": 8, "track": 1}, # snare std
|
||||
{"pattern": 7, "bar": 4, "bars": 8, "track": 2}, # HH 8th
|
||||
{"pattern": 12, "bar": 4, "bars": 8, "track": 3}, # 808 full
|
||||
{"pattern": 15, "bar": 4, "bars": 8, "track": 4}, # sparse bell
|
||||
{"pattern": 17, "bar": 4, "bars": 8, "track": 6}, # pad
|
||||
|
||||
# PRE-CHORUS (12-16): building tension
|
||||
{"pattern": 1, "bar": 12, "bars": 4, "track": 0}, # dembow kick
|
||||
{"pattern": 5, "bar": 12, "bars": 4, "track": 1}, # snare intense
|
||||
{"pattern": 11, "bar": 12, "bars": 4, "track": 9}, # rim build
|
||||
{"pattern": 7, "bar": 12, "bars": 4, "track": 2}, # HH 8th
|
||||
{"pattern": 12, "bar": 12, "bars": 4, "track": 3}, # 808 full
|
||||
{"pattern": 14, "bar": 12, "bars": 4, "track": 4}, # bell chords
|
||||
{"pattern": 18, "bar": 12, "bars": 4, "track": 6}, # pad swell
|
||||
|
||||
# CHORUS (16-24): FULL ENERGY
|
||||
{"pattern": 2, "bar": 16, "bars": 8, "track": 0}, # perreador kick!
|
||||
{"pattern": 5, "bar": 16, "bars": 8, "track": 1}, # snare intense
|
||||
{"pattern": 8, "bar": 16, "bars": 8, "track": 2}, # HH 16th
|
||||
{"pattern": 9, "bar": 16, "bars": 8, "track": 7}, # clap
|
||||
{"pattern": 10, "bar": 16, "bars": 8, "track": 8}, # perc offbeat
|
||||
{"pattern": 12, "bar": 16, "bars": 8, "track": 3}, # 808 full
|
||||
{"pattern": 14, "bar": 16, "bars": 8, "track": 4}, # bell chords
|
||||
{"pattern": 16, "bar": 16, "bars": 8, "track": 5}, # lead hook
|
||||
{"pattern": 17, "bar": 16, "bars": 8, "track": 6}, # pad
|
||||
|
||||
# VERSE 2 (24-32): energy maintained, no lead
|
||||
{"pattern": 1, "bar": 24, "bars": 8, "track": 0}, # dembow kick
|
||||
{"pattern": 4, "bar": 24, "bars": 8, "track": 1}, # snare std
|
||||
{"pattern": 7, "bar": 24, "bars": 8, "track": 2}, # HH 8th
|
||||
{"pattern": 9, "bar": 24, "bars": 8, "track": 7}, # clap
|
||||
{"pattern": 12, "bar": 24, "bars": 8, "track": 3}, # 808 full
|
||||
{"pattern": 14, "bar": 24, "bars": 8, "track": 4}, # bell chords
|
||||
{"pattern": 17, "bar": 24, "bars": 8, "track": 6}, # pad
|
||||
|
||||
# BREAKDOWN (32-36): stripped
|
||||
{"pattern": 3, "bar": 32, "bars": 4, "track": 0}, # sparse kick
|
||||
{"pattern": 6, "bar": 32, "bars": 4, "track": 2}, # offbeat HH
|
||||
{"pattern": 13, "bar": 32, "bars": 4, "track": 3}, # sparse 808
|
||||
{"pattern": 15, "bar": 32, "bars": 4, "track": 4}, # sparse bell
|
||||
{"pattern": 17, "bar": 32, "bars": 4, "track": 6}, # pad
|
||||
|
||||
# OUTRO (36-48): fading
|
||||
{"pattern": 1, "bar": 36, "bars": 12, "track": 0}, # dembow kick
|
||||
{"pattern": 4, "bar": 36, "bars": 12, "track": 1}, # snare std
|
||||
{"pattern": 7, "bar": 36, "bars": 12, "track": 2}, # HH 8th
|
||||
{"pattern": 17, "bar": 36, "bars": 12, "track": 6}, # pad
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# HEADER BUILDER
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _read_ev(data, pos):
|
||||
s = pos
|
||||
ib = data[pos]; pos += 1
|
||||
if ib < 64: return pos + 1, s, ib, data[s + 1], "byte"
|
||||
elif ib < 128: return pos + 2, s, ib, struct.unpack("<H", data[pos:pos + 2])[0], "word"
|
||||
elif ib < 192: return pos + 4, s, ib, struct.unpack("<I", data[pos:pos + 4])[0], "dword"
|
||||
else:
|
||||
sz = 0; sh = 0
|
||||
while True:
|
||||
b = data[pos]; pos += 1
|
||||
sz |= (b & 0x7F) << sh; sh += 7
|
||||
if not (b & 0x80): break
|
||||
return pos + sz, s, ib, data[pos:pos + sz], "data"
|
||||
|
||||
|
||||
def build_header(ref_bytes):
|
||||
pos = 22
|
||||
first_pat = None
|
||||
while pos < len(ref_bytes):
|
||||
np, st, ib, val, vt = _read_ev(ref_bytes, pos)
|
||||
if ib == EventID.PatNew:
|
||||
first_pat = st
|
||||
break
|
||||
pos = np
|
||||
if first_pat is None:
|
||||
raise ValueError("No PatNew found")
|
||||
header = bytearray(ref_bytes[22:first_pat])
|
||||
p = 0
|
||||
while p < len(header):
|
||||
np, _, ib, val, vt = _read_ev(bytes(header), p)
|
||||
if ib == EventID.Tempo:
|
||||
struct.pack_into("<I", header, p + 1, BPM * 1000)
|
||||
break
|
||||
p = np
|
||||
return bytes(header)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# PATTERN BUILDER
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _conv(notes):
|
||||
return [{"position": n["pos"], "length": n["len"], "key": n["key"], "velocity": n["vel"]} for n in notes]
|
||||
|
||||
|
||||
def build_all_patterns():
|
||||
buf = bytearray()
|
||||
for pat_def in PATTERNS:
|
||||
buf += encode_word_event(EventID.PatNew, pat_def["id"] - 1)
|
||||
buf += encode_text_event(EventID.PatName, pat_def["name"])
|
||||
notes_by_ch = ALL_GENERATORS[pat_def["gen"]](pat_def["bars"])
|
||||
for ch_idx, raw_notes in notes_by_ch.items():
|
||||
if not raw_notes:
|
||||
continue
|
||||
buf += encode_data_event(EventID.PatNotes, encode_notes_block(ch_idx, _conv(raw_notes), PPQ))
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN BUILD
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_fuego():
|
||||
print("=" * 60)
|
||||
print("FUEGO reggaeton — REAL SAMPLES from library")
|
||||
print("=" * 60)
|
||||
print(f"Progression: Am -> Dm -> F -> E")
|
||||
print(f"Samples from: libreria/reggaeton/")
|
||||
print(f"Channels: Ch10-19 (all sampler)")
|
||||
print(f"Arrangement: 48 bars, 7 sections")
|
||||
print("=" * 60)
|
||||
|
||||
assert os.path.isfile(REF_FLP), f"MISSING: {REF_FLP}"
|
||||
ref_bytes = open(REF_FLP, "rb").read()
|
||||
num_channels = struct.unpack("<H", ref_bytes[10:12])[0]
|
||||
print(f"\nReference: {len(ref_bytes):,} bytes, {num_channels} channels")
|
||||
|
||||
# 1. Load channels — ALL 10 samplers get real samples from fuego_samples/
|
||||
print("\n[1/4] Loading channels with real samples...")
|
||||
loader = ChannelSkeletonLoader(REF_FLP, CH11_TMPL, SAMPLES_DIR)
|
||||
|
||||
# All channels use samples from fuego_samples/
|
||||
channel_bytes = loader.load(melodic_map=SAMPLE_ASSIGNMENT)
|
||||
print(f" Channels: {len(channel_bytes):,} bytes")
|
||||
for ch_idx in sorted(SAMPLE_ASSIGNMENT.keys()):
|
||||
_, w = SAMPLE_ASSIGNMENT[ch_idx]
|
||||
print(f" Ch{ch_idx}: {w}")
|
||||
|
||||
# 2. Build header + patterns
|
||||
print("\n[2/4] Building header + patterns...")
|
||||
header_bytes = build_header(ref_bytes)
|
||||
pattern_bytes = build_all_patterns()
|
||||
print(f" Header: {len(header_bytes):,} bytes")
|
||||
print(f" Patterns: {len(pattern_bytes):,} bytes ({len(PATTERNS)} patterns)")
|
||||
|
||||
# 3. Build arrangement
|
||||
print("\n[3/4] Building arrangement...")
|
||||
track_data_template = build_track_data_template(ref_bytes)
|
||||
items = [
|
||||
ArrangementItem(
|
||||
pattern_id=it["pattern"], bar=it["bar"],
|
||||
num_bars=it["bars"], track_index=it["track"],
|
||||
)
|
||||
for it in ARRANGEMENT_ITEMS
|
||||
]
|
||||
arrangement_bytes = build_arrangement_section(items, track_data_template, ppq=PPQ)
|
||||
print(f" Arrangement: {len(arrangement_bytes):,} bytes ({len(items)} items)")
|
||||
|
||||
# 4. Assemble
|
||||
print("\n[4/4] Assembling FLP...")
|
||||
body = header_bytes + pattern_bytes + channel_bytes + arrangement_bytes
|
||||
flp = (
|
||||
struct.pack("<4sIhHH", b"FLhd", 6, 0, num_channels, PPQ)
|
||||
+ b"FLdt" + struct.pack("<I", len(body))
|
||||
+ body
|
||||
)
|
||||
|
||||
with open(FLP_OUT, "wb") as f:
|
||||
f.write(flp)
|
||||
|
||||
duration = (48 * 4 / BPM) * 60
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Output: {FLP_OUT}")
|
||||
print(f" Size: {len(flp):,} bytes")
|
||||
print(f" Duration: ~{duration:.0f}s (48 bars @ {BPM} BPM)")
|
||||
print(f" Channels: {num_channels} (Ch0-9 plugin, Ch10-19 sampler)")
|
||||
print(f" Patterns: {len(PATTERNS)}")
|
||||
print(f" Sections: INTRO -> VERSE1 -> PRE-CHORUS -> CHORUS -> VERSE2 -> BREAKDOWN -> OUTRO")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
return flp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_fuego()
|
||||
71
scripts/compose.py
Normal file
71
scripts/compose.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
"""Compose and build in one step from genre knowledge base."""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from src.composer import compose_from_genre
|
||||
from scripts.build import build_project
|
||||
from src.flp_builder.writer import FLPWriter
|
||||
|
||||
KNOWLEDGE_DIR = Path(__file__).parent.parent / "knowledge" / "genres"
|
||||
OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compose and build from genre")
|
||||
parser.add_argument("genre", help="Genre filename (e.g. reggaeton_2009)")
|
||||
parser.add_argument("--key", "-k", default=None, help="Override key (e.g. Am)")
|
||||
parser.add_argument("--bpm", "-b", type=float, default=None, help="Override BPM")
|
||||
parser.add_argument("--bars", type=int, default=None, help="Override bar count")
|
||||
parser.add_argument("--output", "-o", default=None, help="Output .flp path")
|
||||
args = parser.parse_args()
|
||||
|
||||
genre_file = KNOWLEDGE_DIR / f"{args.genre}.json"
|
||||
if not genre_file.exists():
|
||||
print(json.dumps({"error": f"Genre not found: {genre_file}", "available": [p.stem for p in KNOWLEDGE_DIR.glob("*.json")]}))
|
||||
sys.exit(1)
|
||||
|
||||
overrides = {}
|
||||
if args.key:
|
||||
overrides["keys"] = [args.key]
|
||||
if args.bpm:
|
||||
overrides["bpm"] = {"default": args.bpm}
|
||||
if args.bars:
|
||||
overrides["structure"] = {"sections": [{"bars": args.bars}]}
|
||||
|
||||
composition = compose_from_genre(str(genre_file), overrides if overrides else None)
|
||||
project = build_project(composition)
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
output_path = args.output or str(
|
||||
OUTPUT_DIR / f"{args.genre}_{composition['meta']['key']}_{composition['meta']['bpm']}bpm.flp"
|
||||
)
|
||||
|
||||
writer = FLPWriter(project)
|
||||
writer.write(output_path)
|
||||
|
||||
result = {
|
||||
"status": "ok",
|
||||
"output": output_path,
|
||||
"genre": args.genre,
|
||||
"key": composition["meta"]["key"],
|
||||
"bpm": composition["meta"]["bpm"],
|
||||
"chord_progression": composition["meta"]["chord_progression"],
|
||||
"tracks": [
|
||||
{"role": t["role"], "notes": len(t.get("notes", []))}
|
||||
for t in composition["tracks"]
|
||||
],
|
||||
"channel_names": [ch.name for ch in project.channels],
|
||||
"total_notes": sum(len(n) for t in composition["tracks"] for n in t.get("notes", [])),
|
||||
}
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
251
scripts/compose_track.py
Normal file
251
scripts/compose_track.py
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/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()
|
||||
47
scripts/inventory.py
Normal file
47
scripts/inventory.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
"""Inventory scanner - outputs JSON of all available resources."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from src.scanner import full_inventory
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
inv = full_inventory()
|
||||
|
||||
plugins = inv["plugins"]
|
||||
summary = {
|
||||
"generators": plugins["generator_names"],
|
||||
"effects": plugins["effect_names"],
|
||||
"total_generators": len(plugins["generators"]),
|
||||
"total_effects": len(plugins["effects"]),
|
||||
"sample_categories": {
|
||||
k: len(v) for k, v in inv["samples"]["categories"].items()
|
||||
},
|
||||
"total_samples": inv["samples"]["total_files"],
|
||||
"packs": [
|
||||
{
|
||||
"name": p["name"],
|
||||
"audio": len(p["contents"].get("audio", [])),
|
||||
"midi": len(p["contents"].get("midi", [])),
|
||||
}
|
||||
for p in inv["packs"]["packs"]
|
||||
],
|
||||
"vector_store": {
|
||||
"total": inv["vector_store"]["total"],
|
||||
"types": inv["vector_store"]["types"],
|
||||
},
|
||||
"organized_samples": {},
|
||||
}
|
||||
|
||||
for cat, files in inv["samples"]["categories"].items():
|
||||
summary["organized_samples"][cat] = [f["name"] for f in files[:20]]
|
||||
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user