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).
231 lines
8.6 KiB
Markdown
231 lines
8.6 KiB
Markdown
# 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.0–1.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.
|