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.
This commit is contained in:
114
.sdd/changes/archive/reascript-first/design.md
Normal file
114
.sdd/changes/archive/reascript-first/design.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user