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).
This commit is contained in:
230
docs/modules/reaper-builder.md
Normal file
230
docs/modules/reaper-builder.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user