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).
206 lines
8.6 KiB
Markdown
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.
|