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).
8.6 KiB
Reaper Builder Module
Parent doc: 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)
class RPPBuilder:
def __init__(self, song: SongDefinition, seed: int | None = None) -> None
def write(self, path: str | Path) -> None
__init__(): Receives aSongDefinitionand optional seed. Seedsrandomfor deterministic GUID generation when seed is not None.write(): Serializes the project to a.rpptext file. RaisesOSErrorif file cannot be written. Uses therpplibrary (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)
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
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
REAPER_BUILTINS: frozenset[str]
Set of Cockos plugin keys. Plugins in this set are deferred to ReaScript insertion.
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)
# 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)
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:
<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}):
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {72C4DB71...} 0 "" 1158812272...>
base64chunk1
base64chunk2
...
>
VST2 (.dll files with <GUID>):
<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):
<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,PluginDefrpplibrary —Element,dumpsfor.rppXML-like serializationuuid,random(stdlib) — GUID generationsubprocess(forrender.py) — REAPER CLI execution
Known Gotchas
-
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. -
Plugin builtin flag skips .rpp writing: When
PluginDef.builtin == True, the plugin is NOT written to the.rppfile. It's deferred to ReaScript. If the ReaScript doesn't run, the plugin won't be loaded. -
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. -
REAPER version string is hardcoded:
_build_element()hardcodes"7.65/win64"as the REAPER version. Different REAPER versions may behave differently. -
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.
-
AUXRECV indices: Send indices reference return track positions. Reverb is at index
len(content_tracks), Delay atlen(content_tracks) + 1. Adding content tracks before wiring sends shifts these indices. -
Preset data is raw base64: Plugin preset data is stored as base64-encoded binary chunks extracted from ground-truth
.rppfiles. They are NOT parameter lists — they're full plugin state dumps. Modifying presets requires re-extracting from REAPER. -
Headless render requires REAPER installed:
render_project()callsreaper.exeas a subprocess. REAPER must be installed at the expected path for rendering to work.