161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
#!/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()
|