"""CLI to run ReaScript refinement on a .rpp file. Usage: python scripts/run_in_reaper.py [--output ] [--timeout ] [--plugins-config ] [--action ] """ from __future__ import annotations import argparse import json import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).parents[1])) from src.reaper_scripting import ReaScriptGenerator from src.reaper_scripting.commands import ( ReaScriptCommand, ReaScriptResult, read_result, write_command, ) from src.reaper_builder import get_builtin_plugins from src.core.schema import ( SongDefinition, SongMeta, TrackDef, PluginDef, ) def _load_song_config(path: Path) -> SongDefinition: """Reconstruct a SongDefinition from a JSON file (produced by SongDefinition.to_json).""" data: dict = json.loads(path.read_text(encoding="utf-8")) meta_data: dict = data["meta"] meta = SongMeta( bpm=float(meta_data["bpm"]), key=str(meta_data["key"]), title=str(meta_data.get("title", "")), ppq=int(meta_data.get("ppq", 960)), time_sig_num=int(meta_data.get("time_sig_num", 4)), time_sig_den=int(meta_data.get("time_sig_den", 4)), calibrate=bool(meta_data.get("calibrate", True)), ) tracks: list[TrackDef] = [] for t in data.get("tracks", []): plugins: list[PluginDef] = [] for p in t.get("plugins", []): params: dict[int, float] = {} for k, v in p.get("params", {}).items(): params[int(k)] = float(v) plugins.append(PluginDef( name=str(p["name"]), path=str(p.get("path", "")), index=int(p.get("index", 0)), params=params, preset_data=p.get("preset_data"), role=str(p.get("role", "")), builtin=bool(p.get("builtin", False)), )) tracks.append(TrackDef( name=str(t["name"]), volume=float(t.get("volume", 0.85)), pan=float(t.get("pan", 0.0)), color=int(t.get("color", 0)), plugins=plugins, send_reverb=float(t.get("send_reverb", 0.0)), send_delay=float(t.get("send_delay", 0.0)), send_level={int(k): float(v) for k, v in t.get("send_level", {}).items()}, )) return SongDefinition(meta=meta, tracks=tracks) def _normalize_action(action_arg: str | None) -> list[str]: """Parse space-separated action names into a list. Defaults to ['calibrate'].""" if not action_arg: return ["calibrate"] return [a.strip() for a in action_arg.split() if a.strip()] def main() -> None: parser = argparse.ArgumentParser( description="Run ReaScript refinement on a .rpp file" ) parser.add_argument("rpp_path", help="Path to .rpp file") parser.add_argument( "--output", "-o", help="Path for rendered WAV output", default=None ) parser.add_argument( "--timeout", type=int, default=120, help="Timeout in seconds for polling result (default: 120)", ) parser.add_argument( "--plugins-config", help="Path to JSON serialized SongDefinition for deriving plugins_to_add", default=None, ) parser.add_argument( "--action", help="Space-separated action pipeline (e.g. 'add_plugins calibrate render'). Default: calibrate", default=None, ) args = parser.parse_args() rpp_path = Path(args.rpp_path) if not rpp_path.exists(): print(f"ERROR: .rpp file not found: {rpp_path}", file=sys.stderr) sys.exit(1) # Resolve render_path: default to .rpp folder with _rendered.wav if args.output: render_path = Path(args.output) else: render_path = rpp_path.parent / f"{rpp_path.stem}_rendered.wav" # Normalize action to list actions = _normalize_action(args.action) # Derive plugins_to_add from song if --plugins-config is provided plugins_to_add: list[dict] = [] if args.plugins_config: config_path = Path(args.plugins_config) if not config_path.exists(): print(f"ERROR: plugins config file not found: {config_path}", file=sys.stderr) sys.exit(1) song = _load_song_config(config_path) plugins_to_add = get_builtin_plugins(song) print(f"Derived {len(plugins_to_add)} builtin plugins from song config") # 1. Build command command = ReaScriptCommand( version=1, action=actions, rpp_path=str(rpp_path.resolve()), render_path=str(render_path.resolve()), timeout=args.timeout, track_calibration=[], plugins_to_add=plugins_to_add, ) # 2. Generate ReaScript generator = ReaScriptGenerator() # Write to a fixed path - in real usage this would be REAPER ResourcePath()/scripts/ reaper_scripts_dir = Path(sys.path[1]) / "scripts" if len(sys.path) > 1 else Path("scripts") script_path = reaper_scripts_dir / "fl_control_phase2.py" generator.generate(script_path, command) print(f"Generated ReaScript: {script_path}") # 3. Write command JSON (same directory as script) cmd_json_path = reaper_scripts_dir / "fl_control_command.json" write_command(cmd_json_path, command) print(f"Wrote command JSON: {cmd_json_path}") # 4. Poll for result JSON res_json_path = reaper_scripts_dir / "fl_control_result.json" timeout = args.timeout poll_interval = 2.0 elapsed = 0.0 print(f"Polling for result at {res_json_path} (timeout={timeout}s)...") while elapsed < timeout: if res_json_path.exists(): break time.sleep(poll_interval) elapsed += poll_interval if not res_json_path.exists(): print("TIMEOUT: result JSON not found within timeout", file=sys.stderr) sys.exit(2) # 5. Read and print results try: result = read_result(res_json_path) except Exception as e: print(f"ERROR: failed to read result: {e}", file=sys.stderr) sys.exit(1) if result.status == "error": print(f"REAPER error: {result.message}", file=sys.stderr) sys.exit(1) # Print LUFS metrics print("\n=== Phase 2 Result ===") print(f"Status: {result.status}") if result.lufs is not None: print(f"LUFS: {result.lufs}") if result.integrated_lufs is not None: print(f"Integrated LUFS: {result.integrated_lufs}") if result.short_term_lufs is not None: print(f"Short-term LUFS: {result.short_term_lufs}") print(f"Tracks verified: {result.tracks_verified}") if result.fx_errors: print(f"FX errors: {json.dumps(result.fx_errors, indent=2)}") if result.added_plugins: print(f"Added plugins: {json.dumps(result.added_plugins, indent=2)}") print(f"Message: {result.message}") # 6. Write output files output_dir = rpp_path.parent / "output" output_dir.mkdir(exist_ok=True) # LUFS metrics lufs_data = { "lufs": result.lufs, "integrated_lufs": result.integrated_lufs, "short_term_lufs": result.short_term_lufs, "render_path": str(render_path.resolve()), } lufs_path = output_dir / f"{rpp_path.stem}_lufs.json" with open(lufs_path, "w", encoding="utf-8") as f: json.dump(lufs_data, f, indent=2) print(f"Wrote LUFS data: {lufs_path}") # FX errors if result.fx_errors: fx_path = output_dir / f"{rpp_path.stem}_fx_errors.json" with open(fx_path, "w", encoding="utf-8") as f: json.dump(result.fx_errors, f, indent=2) print(f"Wrote FX errors: {fx_path}") print("\nPhase 2 complete.") sys.exit(0) if __name__ == "__main__": main()