# Design: reascript-hybrid

## Technical Approach

Phase 2 runs inside REAPER via a self-contained Python ReaScript. Our Python generates the ReaScript file and drives it via a JSON file protocol — no network, no distant API. REAPER controls timing; we just poll for the result.

## Architecture Decisions

### Decision: JSON file protocol over python-reapy

**Choice**: JSON files via `fl_control_command.json` / `fl_control_result.json` in REAPER's ResourcePath
**Alternatives considered**: python-reapy (network/WebSocket, REAPER distant API)
**Rationale**: No network dependency. REAPER owns the timing — avoids race conditions when REAPER is busy. Simpler debugging: JSON is readable in any editor.

### Decision: Self-contained ReaScript with no external imports

**Choice**: Generated ReaScript uses only the built-in ReaScript API (no `import json` — use `os` and string manipulation)
**Alternatives considered**: Importing Python's `json` module via Python 3.x ReaScript support
**Rationale**: Maximum compatibility across REAPER versions. JSON parsing via hand-rolled parser is ~20 lines of string splitting. Avoids any import-time failures.

### Decision: Separate commands.py for protocol testability

**Choice**: `commands.py` exposes `read_command`, `write_result`, `ReaScriptCommand`, `ReaScriptResult`
**Alternatives considered**: Protocol classes in `__init__.py`
**Rationale**: Unit test the protocol without instantiating ReaScriptGenerator or touching REAPER. The protocol is stable and worth isolating.

### Decision: Track calibration via JSON array, not direct API calls from Python

**Choice**: `track_calibration` list in command JSON describes volume/pan/sends per track
**Alternatives considered**: Python calls REAPER API directly for each calibration step
**Rationale**: Keeps the interface stateless and retry-friendly. If REAPER crashes mid-calibration, the command JSON is still valid for replay.

## Data Flow

```
scripts/run_in_reaper.py        src/reaper_scripting/          REAPER
         │                              │                          │
         │ generate(cmd)              │                          │
         │──────────────────────────> ReaScriptGenerator         │
         │                              │ generates .py           │
         │ write_command(cmd.json)     │                          │
         │────────────────────────────>│                          │
         │                              │ write to ResourcePath()  │
         │                              │────────────────────────>│
         │                              │                          │ Action triggered
         │                              │                          │ reads command.json
         │                              │                          │ executes pipeline
         │                              │                          │ writes result.json
         │                              │<─────────────────────────│
         │ read_result()                │                          │
         │<─────────────────────────────│                          │
```

## File Changes

| File | Action | Description |
|------|--------|-------------|
| `src/reaper_scripting/__init__.py` | Create | `ReaScriptGenerator.generate(path, cmd)` — writes self-contained ReaScript |
| `src/reaper_scripting/commands.py` | Create | `ReaScriptCommand`, `ReaScriptResult` dataclasses + `write_command()`, `read_result()` |
| `scripts/run_in_reaper.py` | Create | CLI: generate script → write command JSON → poll result → print LUFS |

## Interface Contracts

### ReaScriptGenerator

```python
class ReaScriptGenerator:
    def generate(self, path: Path, command: ReaScriptCommand) -> None:
        """Write a self-contained ReaScript .py to path."""
```

The generated script reads `fl_control_command.json`, runs the pipeline, writes `fl_control_result.json`.

### Command JSON schema (`fl_control_command.json`)

```json
{
  "version": 1,
  "action": "calibrate" | "verify_fx" | "render",
  "rpp_path": "absolute path",
  "render_path": "absolute path for WAV output",
  "timeout": 120,
  "track_calibration": [
    {
      "track_index": 0,
      "volume": 0.85,
      "pan": 0.0,
      "sends": [{"dest_track_index": 5, "level": 0.05}]
    }
  ]
}
```

### Result JSON schema (`fl_control_result.json`)

```json
{
  "version": 1,
  "status": "ok" | "error" | "timeout",
  "message": "",
  "lufs": -14.2,
  "integrated_lufs": -14.2,
  "short_term_lufs": -12.1,
  "fx_errors": [{"track_index": 2, "fx_index": 1, "name": "", "expected": "Serum_2"}],
  "tracks_verified": 8
}
```

## Phase 2 Pipeline (ReaScript)

1. `GetFunctionMetadata` — verify API availability
2. `Main_openProject(rpp_path)` — load .rpp
3. Iterate tracks: `TrackFX_GetCount` + `TrackFX_GetFXName` per slot → collect `fx_errors`
4. For each `track_calibration` entry: `SetMediaTrackInfo_Value(VOLUME/PAN)` + `CreateTrackSend`
5. `Main_RenderFile` → render to `render_path`
6. `CalcMediaSrcLoudness(render_path)` → extract `integrated_lufs`, `short_term_lufs`
7. Write result JSON

## Testing Strategy

| Layer | What | How |
|-------|------|-----|
| Unit | `ReaScriptCommand`/`ReaScriptResult` JSON round-trip | `pytest tests/test_commands.py` — serialize/deserialize, version mismatch raises `ProtocolVersionError` |
| Unit | ReaScriptGenerator output is valid Python | `pytest tests/test_reagenerator.py` — parse generated script with `ast.parse`, check it contains required API calls |
| Integration | Full pipeline with REAPER | `pytest tests/test_phase2.py -k integration` — skipped in CI, runs against live REAPER |

## Open Questions

- [ ] Should `render_path` default to the .rpp's folder with `_rendered.wav` suffix?
- [ ] Do we need to handle REAPER's `__startup__.py` registration automatically, or is manual Action registration acceptable for Phase 1?