Files
reaper-control/docs/modules/reaper-scripting.md
renato97 7bcd8052a9 docs: LLM-ready documentation suite — LLM_CONTEXT.md, module deep-dives, JSON schemas, CLI reference
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).
2026-05-04 10:30:24 -03:00

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 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)

@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(): 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():

{
  "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:

  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.pyReaScriptCommand, ReaScriptResult, write_command(), read_result()
  • src/core/schema.pySongDefinition, 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.