Complete documentation system for LLM consumption: primary LLM_CONTEXT.md (27KB system overview), 4 module deep-dives (composer, reaper-builder, reaper-scripting, calibrator), 2 JSON Schema draft-07 contracts, CLI reference, and README correction (FL Studio -> REAPER identity).
8.6 KiB
Reaper Scripting Module
Parent doc: 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)
class ReaScriptGenerator:
def generate(self, path: Path, command: ReaScriptCommand) -> None
generate(): Writes a self-contained Python ReaScript topath. The script opens the.rppfile, executes the action pipeline, and writes results tofl_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)
@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)
@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)
class ProtocolVersionError(Exception):
"""Raised when command/result version doesn't match expected."""
Serialization Functions (commands.py)
def write_command(path: Path, cmd: ReaScriptCommand) -> None
def read_result(path: Path, expected_version: int = 1) -> ReaScriptResult
write_command(): SerializesReaScriptCommandto JSON file. Omitsplugins_to_addkey if empty.read_result(): DeserializesReaScriptResultfrom JSON file. RaisesProtocolVersionErrorif 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():
{
"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():
{
"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:
- Zero external imports: No
json, noos, no third-party libraries. Only REAPER's built-inRPR_*functions. - Hand-rolled JSON parser: ~100 lines of string-splitting logic (
parse_json()). Handles strings, integers, floats, booleans, nulls, nested objects, arrays. - API availability check:
check_api()verifies all required REAPER API functions exist before any work begins. Missing APIs → immediate error result. - Conditional function inclusion: Only the action functions needed by the specific command are emitted in the script. The generator checks
actionlist and includes only relevant source blocks. - Adaptive API check:
check_api()includesTrackFX_AddByNameandTrackFX_SetParamonly whenadd_pluginsorconfigure_fx_paramsactions 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, viaget_builtin_plugins())pathlib(stdlib) — file path handling
Known Gotchas
-
REAPER Python has no
jsonmodule: The generated ReaScript cannotimport json. All serialization uses the hand-rolled parser. This is a fundamental REAPER limitation — its embedded Python is a stripped-down distribution. -
Command JSON MUST be at the right path: The ReaScript reads
fl_control_command.jsonfromRPR_ResourcePath() + "scripts/". If the file isn't there when the script runs, it fails immediately. -
write_command()omits emptyplugins_to_add: Ifplugins_to_addis empty, the key is not written. The ReaScript'scmd.get("plugins_to_add", [])handles this with a default. -
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. -
configure_fx_paramsuses string keys for param indices: Theparamsdict inplugins_to_adduses string keys ("0","1") because JSON keys are always strings. The ReaScript converts tointbefore callingTrackFX_SetParam. -
No error recovery per action: If
add_pluginsfails 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 inrun_in_reaper.py. -
Calibrate sends use
CreateTrackSend: Every call tocalibrate()with sends creates a new send connection. If calibrate is called multiple times, duplicate sends accumulate. -
LUFS is optional: If
RPR_CalcMediaSrcLoudnessreturns an unexpected format or throws,measure_loudness()returnsNonevalues silently.