# Reaper Scripting Module > Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md) ## Purpose Generates self-contained Python ReaScript files that REAPER executes for post-processing: plugin loading, FX parameter configuration, mix calibration, verification, and rendering. Communicates via JSON files on disk. **Location**: `src/reaper_scripting/` ## Public API ### ReaScriptGenerator (`__init__.py`) ```python class ReaScriptGenerator: def generate(self, path: Path, command: ReaScriptCommand) -> None ``` - **`generate()`**: Writes a self-contained Python ReaScript to `path`. The script opens the `.rpp` file, executes the action pipeline, and writes results to `fl_control_result.json`. - **Internal**: `_build_script(command)` assembles the full script source, conditionally including only the action functions needed by the command. ### ReaScriptCommand (`commands.py`) ```python @dataclass class ReaScriptCommand: version: int = 1 action: str | list[str] = "calibrate" rpp_path: str = "" render_path: str = "" timeout: int = 120 track_calibration: list[dict] = field(default_factory=list) plugins_to_add: list[dict] = field(default_factory=list) ``` ### ReaScriptResult (`commands.py`) ```python @dataclass class ReaScriptResult: version: int = 1 status: str = "ok" message: str = "" lufs: float | None = None integrated_lufs: float | None = None short_term_lufs: float | None = None fx_errors: list[dict] = field(default_factory=list) tracks_verified: int = 0 added_plugins: list[dict] = field(default_factory=list) ``` ### ProtocolVersionError (`commands.py`) ```python class ProtocolVersionError(Exception): """Raised when command/result version doesn't match expected.""" ``` ### Serialization Functions (`commands.py`) ```python def write_command(path: Path, cmd: ReaScriptCommand) -> None def read_result(path: Path, expected_version: int = 1) -> ReaScriptResult ``` - **`write_command()`**: Serializes `ReaScriptCommand` to JSON file. Omits `plugins_to_add` key if empty. - **`read_result()`**: Deserializes `ReaScriptResult` from JSON file. Raises `ProtocolVersionError` if version != expected_version. ## Data Flow ``` ReaScriptCommand │ ▼ ReaScriptGenerator.generate(script_path, command) │ _build_script(command) │ ├── check_api() — verifies REAPER API function availability │ ├── parse_json() / write_json() — hand-rolled JSON (no stdlib json) │ ├── find_track() — lookup by name via RPR_GetSetMediaTrackInfo_String │ ├── add_plugins() — RPR_TrackFX_AddByName for built-in plugins │ ├── configure_fx_params() — RPR_TrackFX_SetParam for param config │ ├── calibrate() — RPR_SetMediaTrackInfo_Value (volume/pan) + sends │ ├── verify_fx() — enumerates all tracks/FX, reports missing │ ├── render() — RPR_Main_RenderFile for WAV export │ ├── measure_loudness() — RPR_CalcMediaSrcLoudness for LUFS │ └── main() — dispatch loop: read command → open project → actions → write result ▼ fl_control_phase2.py (self-contained ReaScript) │ (REAPER executes this) ▼ fl_control_result.json │ ▼ read_result() → ReaScriptResult ``` ## Command/Result Contract ### Command JSON (`fl_control_command.json`) Written by `write_command()`: ```json { "version": 1, "action": ["add_plugins", "calibrate", "render"], "rpp_path": "C:/path/to/song.rpp", "render_path": "C:/path/to/song_rendered.wav", "timeout": 120, "track_calibration": [ {"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []} ], "plugins_to_add": [ {"track_name": "808 Bass", "fx_name": "ReaEQ", "params": {"0": "1", "1": "0", "2": "300.0"}} ] } ``` ### Result JSON (`fl_control_result.json`) Written by the ReaScript's `write_json()`: ```json { "version": 1, "status": "ok", "message": "", "lufs": -14.2, "integrated_lufs": -14.2, "short_term_lufs": -13.8, "fx_errors": [], "tracks_verified": 10, "added_plugins": [ {"fx_name": "ReaEQ", "instance_id": 0, "track_name": "808 Bass", "status": "ok"} ] } ``` ## Known Actions | Action | Function | Description | |--------|----------|-------------| | `add_plugins` | `add_plugins(cmd)` | Loads plugins via `TrackFX_AddByName`. Returns per-plugin status. | | `configure_fx_params` | `configure_fx_params(cmd)` | Sets FX parameters via `TrackFX_SetParam`. | | `verify_fx` | `verify_fx()` | Enumerates all tracks/FX, reports empty/missing FX names. | | `calibrate` | `calibrate(track_cal)` | Sets track volume, pan, and sends from calibration data. | | `render` | `do_render(render_path)` + `measure_loudness()` | Renders WAV and measures LUFS via `RPR_CalcMediaSrcLoudness`. | Actions execute in the order specified by the `action` list. The script's `main()` opens the project once, then runs the action dispatch loop, then writes the result JSON. ## ReaScript Architecture The generated script is a single self-contained `.py` file with: 1. **Zero external imports**: No `json`, no `os`, no third-party libraries. Only REAPER's built-in `RPR_*` functions. 2. **Hand-rolled JSON parser**: ~100 lines of string-splitting logic (`parse_json()`). Handles strings, integers, floats, booleans, nulls, nested objects, arrays. 3. **API availability check**: `check_api()` verifies all required REAPER API functions exist before any work begins. Missing APIs → immediate error result. 4. **Conditional function inclusion**: Only the action functions needed by the specific command are emitted in the script. The generator checks `action` list and includes only relevant source blocks. 5. **Adaptive API check**: `check_api()` includes `TrackFX_AddByName` and `TrackFX_SetParam` only when `add_plugins` or `configure_fx_params` actions are present. ### REAPER API Functions Used | API Function | Purpose | |-------------|---------| | `RPR_CountTracks` | Enumerate tracks | | `RPR_GetTrack` | Get track by index | | `RPR_GetSetMediaTrackInfo_String` | Get/set track name | | `RPR_SetMediaTrackInfo_Value` | Set track volume (D_VOL), pan (D_PAN) | | `RPR_TrackFX_GetCount` | Count FX on a track | | `RPR_TrackFX_GetFXName` | Get FX name by index | | `RPR_TrackFX_AddByName` | Add FX by display name | | `RPR_TrackFX_SetParam` | Set FX parameter by index | | `RPR_CreateTrackSend` | Create send between tracks | | `RPR_Main_openProject` | Open .rpp project | | `RPR_Main_RenderFile` | Render to file | | `RPR_CalcMediaSrcLoudness` | Measure LUFS | | `RPR_ResourcePath` | Get REAPER resource directory path | | `RPR_GetFunctionMetadata` | Check API function availability | | `RPR_GetLastError` | Get last error | | `RPR_GetProjectName` | Get project name | ## Dependencies - `src/reaper_scripting/commands.py` — `ReaScriptCommand`, `ReaScriptResult`, `write_command()`, `read_result()` - `src/core/schema.py` — `SongDefinition`, `PluginDef` (indirect, via `get_builtin_plugins()`) - `pathlib` (stdlib) — file path handling ## Known Gotchas 1. **REAPER Python has no `json` module**: The generated ReaScript cannot `import json`. All serialization uses the hand-rolled parser. This is a fundamental REAPER limitation — its embedded Python is a stripped-down distribution. 2. **Command JSON MUST be at the right path**: The ReaScript reads `fl_control_command.json` from `RPR_ResourcePath() + "scripts/"`. If the file isn't there when the script runs, it fails immediately. 3. **`write_command()` omits empty `plugins_to_add`**: If `plugins_to_add` is empty, the key is not written. The ReaScript's `cmd.get("plugins_to_add", [])` handles this with a default. 4. **Track name matching is case-insensitive**: `find_track()` normalizes both the search name and the REAPER track name to lowercase for matching. Track names that differ only by case will collide. 5. **`configure_fx_params` uses string keys for param indices**: The `params` dict in `plugins_to_add` uses string keys (`"0"`, `"1"`) because JSON keys are always strings. The ReaScript converts to `int` before calling `TrackFX_SetParam`. 6. **No error recovery per action**: If `add_plugins` fails for one track, it continues with the next. But if any action crashes (unhandled exception), the script terminates without writing the result JSON — leading to polling timeout in `run_in_reaper.py`. 7. **Calibrate sends use `CreateTrackSend`**: Every call to `calibrate()` with sends creates a new send connection. If calibrate is called multiple times, duplicate sends accumulate. 8. **LUFS is optional**: If `RPR_CalcMediaSrcLoudness` returns an unexpected format or throws, `measure_loudness()` returns `None` values silently.