feat: reggaeton production system with intelligent sample selection and FLP generation
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user