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

8.6 KiB
Raw Permalink Blame History

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 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)

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) → fallbackNone.

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.pySongDefinition, 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.