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