Files
reaper-control/docs/modules/reaper-builder.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

231 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Reaper Builder Module
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
## Purpose
Generates valid REAPER `.rpp` project files from a `SongDefinition`. Maintains the plugin registry (~97 VST2/VST3 plugins with GUIDs), builds the `.rpp` element tree, and supports headless rendering via REAPER CLI.
**Location**: `src/reaper_builder/`
## Public API
### RPPBuilder (`__init__.py`)
```python
class RPPBuilder:
def __init__(self, song: SongDefinition, seed: int | None = None) -> None
def write(self, path: str | Path) -> None
```
- **`__init__()`**: Receives a `SongDefinition` and optional seed. Seeds `random` for deterministic GUID generation when seed is not None.
- **`write()`**: Serializes the project to a `.rpp` text file. Raises `OSError` if file cannot be written. Uses the `rpp` library (`Element`, `dumps`) for XML-like structure.
- **Internal**: `_build_element()``_build_track(track)``_build_plugin(plugin)``_build_clip(clip)``_build_midi_source(clip)`.
### Plugin Registry (`__init__.py`)
```python
PLUGIN_REGISTRY: dict[str, tuple[str, str, str]]
```
Format: `"key": ("display_name", "filename", "uid_guid")`
~97 entries covering:
- **FabFilter**: Pro-Q 3, Pro-C 2, Pro-R 2, Pro-L 2, Saturn 2, Timeless 3, Pro-DS, Pro-G, Pro-MB, Micro, One, Simplon, Twin 3, Volcano 3 (both VST2 and VST3 versions)
- **SoundToys**: Decapitator, EchoBoy, Crystallizer, Devil-Loc, EffectRack, FilterFreak, MicroShift, PanMan, PhaseMistress, PrimalTap, Radiator, Tremolator, Little AlterBoy, Little MicroShift, Little PrimalTap, Little Radiator
- **iZotope Ozone 12**: Equalizer, Dynamics, Maximizer, plus 17 additional Ozone modules
- **Spectrasonics**: Omnisphere, FX-Omnisphere
- **Xfer**: Serum 2, Serum 2 FX
- **u-he**: Diva
- **Arturia**: Pigments
- **Cableguys**: ShaperBox 3
- **Native Instruments**: Kontakt 7, VC 160, VC 2A, VC 76
- **Cockos (REAPER native)**: ReaEQ, ReaComp, ReaDelay, ReaVerb, ReaVerbate, ReaFIR, ReaGate, ReaLimit, ReaPitch, ReaXcomp, ReaTune, ReaSynth, ReaSamplOmatic5000, and more (20+ plugins)
- **Other**: ValhallaDelay, Gullfoss (Standard/Master/Live), The Glue, Trackspacer 2.5, ravity, Tone2 Electra
```python
ALIAS_MAP: dict[str, str]
```
Maps alternate names to registry keys:
- `"Serum2"``"Serum_2"`
- `"FabFilter Pro-Q 3"``"Pro-Q_3"` (space-separated → underscore VST3 key)
- `"Pro-Q 3"``"Pro-Q_3"` (short name → VST3 key)
- `"Valhalla Delay"``"ValhallaDelay"`
- VST2 SoundToys old names → new underscore names
```python
REAPER_BUILTINS: frozenset[str]
```
Set of Cockos plugin keys. Plugins in this set are deferred to ReaScript insertion.
```python
def get_builtin_plugins(song: SongDefinition) -> list[dict[str, object]]
```
Extracts plugins where `PluginDef.builtin == True` or name is in `REAPER_BUILTINS`. Returns list of `{"track_name", "fx_name", "params"}` dicts for `ReaScriptCommand.plugins_to_add`.
### Preset Transformer (`preset_transformer.py`)
```python
# Transforms flat _PRESETS_FLAT dict → role-aware PLUGIN_PRESETS
# Format: {(plugin_key, role): [chunk1, chunk2, ...]}
PLUGIN_PRESETS: dict[tuple[str, str], list[str]]
```
Lookup chain: `(key, role)``(key, "")` (default) → `fallback``None`.
Plugins with preset data: Arcade, Omnisphere, Pro-Q_3, Pro-C_2, Pro-R_2, Pro-L_2, Saturn 2, Timeless 3, The Glue, ValhallaDelay, Serum 2, Diva, Kontakt 7, VC 160, VC 76, Gullfoss, Gullfoss Master, Decapitator, EchoBoy, EffectRack, FilterFreak1, MicroShift, Little AlterBoy, PhaseMistress, Tremolator.
Plugins with empty presets (no saved state): Most Ozone 12 modules, Pigments, ShaperBox 3, Gullfoss Live, Trackspacer 2.5, Crystallizer, FilterFreak2, Little MicroShift, Little PrimalTap, Little Radiator, PanMan, VC 2A, Elektra.
### Headless Render (`render.py`)
```python
def render_project(
rpp_path: str | Path,
output_wav: str | Path,
reaper_exe: str | Path | None = None,
timeout_seconds: int = 120,
) -> None
```
Renders a `.rpp` to WAV via `reaper.exe -nosplash -render`. Default REAPER path: `C:\Program Files\REAPER (x64)\reaper.exe`. Raises `FileNotFoundError` if REAPER not found, `RuntimeError` on render failure.
## Data Flow
```
SongDefinition
RPPBuilder.__init__(song, seed)
RPPBuilder.write(path)
│ _build_element()
│ ├── Project header (_PROJECT_HEADER static lines)
│ ├── TEMPO line (dynamic from song.meta)
│ ├── Master TRACK with FXCHAIN
│ │ ├── master_plugins resolved via ALIAS_MAP → PLUGIN_REGISTRY
│ │ └── VST elements with preset data
│ ├── Per-track TRACK elements
│ │ ├── NAME, VOLPAN (from TrackDef.volume/.pan), TRACKID
│ │ ├── AUXRECV (from send_level dict)
│ │ ├── FXCHAIN with VST elements (non-builtin plugins)
│ │ └── ITEM elements (clips)
│ │ ├── SOURCE WAVE (audio clips: FILE path)
│ │ └── SOURCE MIDI (MIDI clips: E-lines at 960 PPQ)
│ └── TEXT output → .rpp file
output/song.rpp
```
## `.rpp` File Structure
The generated `.rpp` follows REAPER's ground-truth format extracted from `output/test_vst3.rpp`:
```rpp
<REAPER_PROJECT 0.1 "7.65/win64" ...>
<NOTES ...>
...static project metadata...
TEMPO 95 4 4 0
<TRACK {master-guid}>
NAME master
...
<FXCHAIN>
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {GUID} ...>
<VST "VST3: Pro-C 2 (FabFilter)" FabFilter {GUID} ...>
<VST "VST3: Pro-L 2 (FabFilter)" FabFilter {GUID} ...>
>
>
<TRACK {track-guid}>
NAME "808 Bass"
VOLPAN 0.820000 0.000000 -1 -1 1
<FXCHAIN>
<VST "VST3i: Serum 2 (Xfer Records)" Serum2.vst3 {GUID} ...>
>
AUXRECV 8 0.050000 -1 -1 0
AUXRECV 9 0.000000 -1 -1 0
<ITEM>
POSITION 0.0
LENGTH 32.0
NAME "Verse 808"
<SOURCE MIDI>
HASDATA 1 960 QN
E 0 90 21 50
E 960 80 21 00
...
>
>
>
<TRACK {track-guid}>
NAME "Reverb"
...
>
<TRACK {track-guid}>
NAME "Delay"
...
>
>
```
### VST Element Format
**VST3** (`.vst3` files with `{GUID}`):
```rpp
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {72C4DB71...} 0 "" 1158812272...>
base64chunk1
base64chunk2
...
>
```
**VST2** (`.dll` files with `<GUID>`):
```rpp
<VST "VST: Decapitator (SoundToys)" Decapitator.dll 0 "" <56535453744463...> 0 "" ...>
base64chunk1
base64chunk2
...
>
```
### MIDI Source Format
MIDI data is written as E-lines at 960 PPQ with 16th-note quantization (120-tick grid):
```rpp
<SOURCE MIDI>
HASDATA 1 960 QN
E 0 90 21 50 ; delta=0, note-on, pitch=0x21 (33=A1), velocity=0x50 (80)
E 1440 80 21 00 ; delta=1440, note-off, pitch=0x21
E 120 B0 0B 32 ; delta=120, CC 11 (Expression), value=0x32 (50)
...
>
```
## Dependencies
- `src/core/schema.py``SongDefinition`, `TrackDef`, `ClipDef`, `PluginDef`
- `rpp` library — `Element`, `dumps` for `.rpp` XML-like serialization
- `uuid`, `random` (stdlib) — GUID generation
- `subprocess` (for `render.py`) — REAPER CLI execution
## Known Gotchas
1. **VST3 display names MUST match exactly**: The display_name in the registry must match what `TrackFX_AddByName()` expects in REAPER. A typo in the display_name field causes silent plugin load failure.
2. **Plugin builtin flag skips .rpp writing**: When `PluginDef.builtin == True`, the plugin is NOT written to the `.rpp` file. It's deferred to ReaScript. If the ReaScript doesn't run, the plugin won't be loaded.
3. **GUID format matters**: VST2 uses `<GUID>` with angle brackets in the `.rpp`. VST3 uses `{GUID}` with curly braces. Using the wrong delimiter format causes REAPER to reject the plugin entry.
4. **REAPER version string is hardcoded**: `_build_element()` hardcodes `"7.65/win64"` as the REAPER version. Different REAPER versions may behave differently.
5. **VOLPAN format**: REAPER expects volume as a linear value 0.01.0, not dB. Pan is -1.0 to 1.0. Values outside these ranges produce unexpected behavior.
6. **AUXRECV indices**: Send indices reference return track positions. Reverb is at index `len(content_tracks)`, Delay at `len(content_tracks) + 1`. Adding content tracks before wiring sends shifts these indices.
7. **Preset data is raw base64**: Plugin preset data is stored as base64-encoded binary chunks extracted from ground-truth `.rpp` files. They are NOT parameter lists — they're full plugin state dumps. Modifying presets requires re-extracting from REAPER.
8. **Headless render requires REAPER installed**: `render_project()` calls `reaper.exe` as a subprocess. REAPER must be installed at the expected path for rendering to work.