Files
renato97 b08dcccca2 feat: reascript-first — built-in REAPER plugin insertion via ReaScript API
Hybrid pipeline: RPPBuilder writes VST3/MIDI/audio skeleton, ReaScript
handles built-in plugins (ReaEQ, ReaComp) via TrackFX_AddByName +
TrackFX_SetParam with multi-action dispatch, adaptive API check, and
builtin plugin auto-detection from PLUGIN_REGISTRY.

326 tests (298 existing + 28 new), 12/12 spec scenarios compliant.
2026-05-04 09:38:58 -03:00

6.7 KiB

Design: ReaScript-First Built-in Plugin Configuration

Technical Approach

Hybrid pipeline: RPPBuilder writes skeleton .rpp (VST3 + MIDI + audio only). Built-in plugins (ReaEQ, ReaComp, etc.) are deferred to ReaScript which inserts via TrackFX_AddByName and configures via TrackFX_SetParam. ReaScriptGenerator._build_script() produces dispatch-based main() that iterates cmd["action"] (now str | list[str]) and calls per-action functions conditionally. Backward compat: action="calibrate" still works.

Architecture Decisions

Multi-Action Dispatch Model

Option Tradeoff Decision
action: str | list[str] Normalization in Python; trivial backward compat Chosen
Pipeline object with stages Type-safe but adds dataclass overhead; overkill for 5 actions Rejected
Separate command per action Multiple script invocations (5x slower); breaks single-script model Rejected

Rationale: _build_script() normalizes str → [str]. Generated main() loops actions and calls per-action functions (add_plugins(), configure_fx_params(), verify_fx(), calibrate(), render()). Each function is emitted only if its action appears in the list.

Built-in FX Parameter Mapping

Option Tradeoff Decision
Index-based (dict[int, float]) Stable across REAPER versions; matches existing PluginDef.params Chosen
Name-based (dict[str, float]) User-friendly but fragile (band labels change); requires lookup table Rejected
Hybrid index + name aliases Too complex for v1; doubles code Rejected

Rationale: Codebase already uses PluginDef.params: dict[int, float] (schema.py:169) and RPPBuilder populates param slots by index (lines 1828-1831). JSON uses {"2": 200.0} for transport (string keys). No mapping layer needed.

Command JSON Protocol Evolution

Option Tradeoff Decision
Extend existing dataclasses with optional fields, keep v=1 Zero breaking changes; all new fields optional Chosen
New schema v2 Clear boundary but breaks 298 existing tests Rejected
Separate plugin command file Coordination complexity between two JSON files Rejected

Rationale: plugins_to_add and added_plugins are additive optional fields. write_command / read_result serialize them only when present. Old scripts without plugins_to_add produce identical JSON.

Data Flow

compose.py ──→ SongDefinition ──→ RPPBuilder.write()
                 │  PluginDef.builtin=True        │
                 │  (index-based params)          ▼
                 │                    skeleton .rpp (VST3 only)
                 │                         │
                 │                         ▼
                 │                  run_in_reaper.py
                 │                  │ builds ReaScriptCommand
                 │                  │ ▸ action=["add_plugins","configure_fx_params",
                 │                  │           "verify_fx","calibrate","render"]
                 │                  │ ▸ plugins_to_add=[{track_name, fx_name, params}]
                 │                  ▼
                 │                  ReaScriptGenerator.generate()
                 │                  │ adaptive check_api(actions)
                 │                  │ dispatch loop: add_plugins→configure→verify→calibrate→render
                 │                  ▼
                 │                  REAPER executes → rendered .wav
                 │                                         │
                 ▼                                         ▼
           final .rpp                            ReaScriptResult JSON
                                               ▸ added_plugins, lufs

File Changes

File Action Description
src/reaper_scripting/__init__.py Modify Add _add_plugins_src(), _configure_fx_params_src(), _action_dispatch_src(). Refactor _build_script() to per-action functions + dispatch loop. Adaptive check_api(actions).
src/reaper_scripting/commands.py Modify ReaScriptCommand: action: str | list[str], plugins_to_add: list[dict]. ReaScriptResult: added_plugins: list[dict]. Update write_command/read_result.
scripts/run_in_reaper.py Modify Accept --plugins-config flag; derive plugins_to_add from SongDefinition + RPPBuilder built-in set. Normalize action to list.
src/reaper_builder/__init__.py Modify Add REAPER_BUILTINS: frozenset[str]. Expose get_builtin_plugins(song) → list[dict]. Skip VST element generation for built-in plugins in _build_fx_chain().
src/core/schema.py Modify Add builtin: bool = False to PluginDef.
tests/test_reaper_scripting.py Modify Tests: multi-action dispatch output, plugins_to_add block, adaptive check_api, backward compat string action.

Interfaces / Contracts

PluginDef (new field):

builtin: bool = False  # True → deferred to ReaScript, not written to .rpp

ReaScriptCommand (modified):

action: str | list[str] = "calibrate"  # string normalized to list in _build_script
plugins_to_add: list[dict] = field(default_factory=list)
# Each dict: {"track_name": str, "fx_name": str, "params": {"0": float, ...}}

ReaScriptResult (modified):

added_plugins: list[dict] = field(default_factory=list)
# Each dict: {"fx_name": str, "instance_id": int, "track_name": str, "status": str}

RPPBuilder (new export):

REAPER_BUILTINS: frozenset[str]  # {"ReaEQ", "ReaComp", "ReaDelay", ...}
def get_builtin_plugins(song: SongDefinition) -> list[dict]: ...

Testing Strategy

Layer What Approach
Unit Multi-action dispatch generates correct code blocks Parametrize action list variants; assert source contains expected per-action functions
Unit Adaptive check_api requires FX APIs only when relevant Test with/without add_plugins; grep generated source for required API names
Unit plugins_to_add serialization round-trip JSON write → read → assert optional fields survive
Integration Built-in plugin detection in RPPBuilder Add builtin=True to PluginDef; assert skipped in .rpp output, listed in command
Regression All 298 existing tests pass unchanged Run pytest before merge; string action backward compat verified

Migration / Rollout

No data migration required. All new fields are optional with defaults. action="calibrate" produces identical script output. Rollback: revert commands.py and __init__.py changes; existing tests remain green.