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

206 lines
8.6 KiB
Markdown

# 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.