# 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): ```python builtin: bool = False # True → deferred to ReaScript, not written to .rpp ``` **ReaScriptCommand** (modified): ```python 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): ```python added_plugins: list[dict] = field(default_factory=list) # Each dict: {"fx_name": str, "instance_id": int, "track_name": str, "status": str} ``` **RPPBuilder** (new export): ```python 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.