- compose-test-sync: fix 3 failing tests (NOTE_TO_MIDI, DrumLoopAnalyzer mock, section name) - generate-song: CLI wrapper + RPP validator (6 structural checks) + 4 e2e tests - reascript-hybrid: ReaScriptGenerator + command protocol + CLI + 16 unit tests - 110/110 tests passing - Full SDD cycle (propose→spec→design→tasks→apply→verify) for all 3 changes
186 lines
9.3 KiB
Markdown
186 lines
9.3 KiB
Markdown
# Delta for reascript-generator
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: ReaScriptGenerator — File Output
|
|
|
|
The system SHALL generate a self-contained Python ReaScript file that REAPER can execute via a custom Action.
|
|
|
|
The `ReaScriptGenerator` class MUST expose a `generate(path: Path, command: ReaScriptCommand) -> None` method that writes a valid Python 3.x ReaScript to `path`.
|
|
|
|
The generated script MUST include:
|
|
- A `MAIN` block that reads a JSON command file, executes the action, and writes a JSON result file
|
|
- ReaScript API calls for all required operations (open project, verify FX, calibrate, render, measure loudness)
|
|
- Proper error handling that writes error status to the result JSON
|
|
|
|
#### Scenario: Generate ReaScript for open-verify-render-loudness pipeline
|
|
|
|
- GIVEN a `ReaScriptCommand` with `action="calibrate"`, `rpp_path="output/song.rpp"`, `render_path="output/song.wav"`, and `timeout=120`
|
|
- WHEN `generator.generate(path, command)` is called
|
|
- THEN a Python file is written to `path` that calls `Main_openProject(rpp_path)`, iterates tracks calling `TrackFX_GetCount` and `TrackFX_GetFXName`, calls `SetMediaTrackInfo_Value` for volume/pan, calls `CreateTrackSend` for sends, calls `Main_RenderFile`, and calls `CalcMediaSrcLoudness` on the rendered file
|
|
- AND the script writes a result JSON with fields: `status` ("ok" | "error"), `lufs` (float or null), `fx_errors` (list), `track_count` (int)
|
|
|
|
#### Scenario: Generate ReaScript for FX verification only
|
|
|
|
- GIVEN a `ReaScriptCommand` with `action="verify_fx"` and `rpp_path="output/song.rpp"`
|
|
- WHEN `generator.generate(path, command)` is called
|
|
- THEN the generated script opens the project, iterates all tracks, calls `TrackFX_GetCount` and `TrackFX_GetFXName` for each FX slot, and writes a result JSON listing loaded FX names and any slots where `TrackFX_GetFXName` returned an empty string (missing plugin)
|
|
|
|
### Requirement: Command Protocol — JSON File Contract
|
|
|
|
The system SHALL use a versioned JSON command file for two-way communication with the ReaScript.
|
|
|
|
The command file (written by our Python, read by ReaScript) SHALL have schema:
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"action": "calibrate" | "verify_fx" | "render",
|
|
"rpp_path": "absolute path to .rpp",
|
|
"render_path": "absolute path for rendered output (wav)",
|
|
"timeout": 120,
|
|
"track_calibration": [
|
|
{
|
|
"track_index": 0,
|
|
"volume": 0.85,
|
|
"pan": 0.0,
|
|
"sends": [{"dest_track_index": 5, "level": 0.05}]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
The result file (written by ReaScript, read by our Python) SHALL have schema:
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"status": "ok" | "error" | "timeout",
|
|
"message": "optional error 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
|
|
}
|
|
```
|
|
|
|
#### Scenario: Command file round-trip
|
|
|
|
- GIVEN a valid `ReaScriptCommand` dataclass
|
|
- WHEN `write_command(path, cmd)` is called
|
|
- THEN a JSON file is written to `path` with the exact schema above
|
|
- AND `read_result(path)` returns a `ReaScriptResult` with parsed fields
|
|
|
|
#### Scenario: Version mismatch handling
|
|
|
|
- GIVEN `read_result(path)` is called and the result JSON has `version` ≠ 1
|
|
- THEN a `ProtocolVersionError` SHALL be raised with a message indicating the mismatch
|
|
|
|
### Requirement: run_in_reaper.py — CLI Entry Point
|
|
|
|
The system SHALL provide a CLI script at `scripts/run_in_reaper.py` that accepts a `.rpp` path and runs Phase 2 (calibrate + render + measure loudness).
|
|
|
|
Usage: `python scripts/run_in_reaper.py <rpp_path> [--output <wav_path>] [--timeout <seconds>]`
|
|
|
|
The CLI SHALL:
|
|
1. Generate a ReaScript file to a watched folder (e.g. `REAPER ResourcePath()/scripts/fl_control_phase2.py`)
|
|
2. Write the command JSON to `REAPER ResourcePath()/scripts/fl_control_command.json`
|
|
3. Poll `REAPER ResourcePath()/scripts/fl_control_result.json` until it exists or timeout is reached
|
|
4. Parse the result and print loudness metrics
|
|
5. Exit with code 0 on success, non-zero on failure
|
|
|
|
#### Scenario: Successful Phase 2 run
|
|
|
|
- GIVEN REAPER is running with the custom Action registered
|
|
- AND the .rpp file at `output/song.rpp` exists with valid tracks
|
|
- WHEN `python scripts/run_in_reaper.py output/song.rpp --output output/song.wav` is executed
|
|
- THEN the script generates the ReaScript, writes command JSON, waits for result JSON, and prints the LUFS measurement
|
|
- AND files `output/song_lufs.json` and `output/song_fx_errors.json` are written with results
|
|
|
|
#### Scenario: Missing FX detection
|
|
|
|
- GIVEN a track in the .rpp references a plugin not installed in REAPER
|
|
- WHEN Phase 2 runs and the ReaScript calls `TrackFX_GetFXName` on that slot
|
|
- THEN `fx_errors` in the result JSON SHALL contain an entry with the track index, FX index, and empty name
|
|
- AND the CLI writes `output/song_fx_errors.json` with the error details
|
|
|
|
#### Scenario: Timeout on REAPER non-response
|
|
|
|
- GIVEN REAPER is not running or the Action is not triggered
|
|
- WHEN the CLI polls for `fl_control_result.json` beyond the timeout
|
|
- THEN the CLI SHALL exit with code 2 and print a timeout message
|
|
|
|
### Requirement: ReaScript API Subset
|
|
|
|
The system SHALL only use a stable, documented subset of the ReaScript API to minimize version compatibility issues.
|
|
|
|
Required API functions:
|
|
- `Main_openProject(path)` — open .rpp project file
|
|
- `TrackFX_GetCount(track)` — get number of FX on track
|
|
- `TrackFX_GetFXName(track, fx, buf, bufsize)` — get FX name by index
|
|
- `TrackFX_AddByName(track, fxname, recFX, instantiate)` — add FX by name (for remediation)
|
|
- `SetMediaTrackInfo_Value(track, paramname, value)` — set VOLUME, PAN, etc.
|
|
- `CreateTrackSend(tr, desttr)` — create send to destination track
|
|
- `Main_RenderFile(proj, filename)` — render project to file (or use project render settings)
|
|
- `GetProjectName(proj, buf, bufsize)` — get current project name
|
|
- `GetTrackNumMediaItems(track)` — count items on track
|
|
- `GetMediaItem(track, idx)` — get media item by index
|
|
- `GetMediaItemTake(mediaitem, idx)` — get take from item
|
|
- `GetMediaItemTake_Source(take)` — get source from take
|
|
- `GetMediaSourceType(src, buf)` — check if source is audio/video
|
|
- `PCM_Source_GetSectionInfo(src)` — get source length
|
|
- `GetMediaSourceFileName(src, buf, bufsize)` — get source file path
|
|
- `SetMediaItemInfo_Value(item, param, value)` — set item properties
|
|
- `SetMediaItemLength(item, length, update)` — set item length
|
|
- `AddMediaItemToTrack(track)` — add new media item
|
|
- `CreateNewMediaItem(track)` — create new item
|
|
- `GetTrack(guid, flag)` — get track by GUID (flag=0)
|
|
- `GetTrackGUID(track, buf)` — get track GUID
|
|
- `GetItemOwnership(mediaitem)` — item ownership check
|
|
- `DeleteTrack(track)` — delete track
|
|
- `DeleteMediaItem(mediaitem)` — delete media item
|
|
- `Undo_OnStateChange(s)` — mark undo point
|
|
- `PreventUIRefresh(PreventCount)` — prevent UI refresh during batch ops
|
|
- `OnPauseButton()` — pause playback
|
|
- `OnPlayButton()` — start playback
|
|
- `GetPlayState()` — get play state (0=stopped, 1=playing, 2=paused)
|
|
- `GetCursorPosition()` — get playhead position in seconds
|
|
- `GetProjectLengthSeconds(proj)` — get project length
|
|
- `GetUserInputs(title, captions, retvals)` — optional: prompt user for input
|
|
- `ShowConsoleMsg(msg)` — debug output to REAPER console
|
|
- `TimeMap_cur_qn_to_beats(qn)` — convert quarter notes to beats
|
|
- `TimeMap_qn_to_time(qn)` — convert quarter notes to time in seconds
|
|
- `GetFunctionMetadata(functionName)` — detect if function exists
|
|
|
|
#### Scenario: API availability check on startup
|
|
|
|
- GIVEN the ReaScript is executed inside REAPER
|
|
- WHEN the script starts
|
|
- THEN it SHALL call `GetFunctionMetadata("Main_openProject")` to verify the required API functions are available
|
|
- AND if any required function is missing, write `{"status": "error", "message": "API not available"}` to result JSON and exit
|
|
|
|
### Requirement: Phase 2 Pipeline Steps
|
|
|
|
The ReaScript SHALL execute these steps in order when `action="calibrate"`:
|
|
|
|
1. **Open Project**: Call `Main_openProject(rpp_path)` to load the .rpp
|
|
2. **Verify FX**: Iterate all tracks, for each call `TrackFX_GetCount(track)`, then for each FX slot call `TrackFX_GetFXName` — log empty/missing plugins
|
|
3. **Calibrate Tracks**: For each entry in `track_calibration`, call `SetMediaTrackInfo_Value(track, "VOLUME", volume)` and `SetMediaTrackInfo_Value(track, "PAN", pan)`, then for each send call `CreateTrackSend`
|
|
4. **Render**: Call `Main_RenderFile` with the project and `render_path` (or use project render settings if `render_path` is not provided)
|
|
5. **Measure Loudness**: Call `CalcMediaSrcLoudness` on the rendered WAV file to obtain integrated LUFS, short-term LUFS
|
|
6. **Write Result**: Write the result JSON with status, lufs metrics, and fx_errors
|
|
|
|
#### Scenario: Full pipeline execution
|
|
|
|
- GIVEN a valid .rpp with 8 tracks, each with 1-3 FX plugins, and valid render settings
|
|
- WHEN the ReaScript executes with `action="calibrate"`
|
|
- THEN all 6 steps execute in order
|
|
- AND the result JSON contains `status: "ok"`, `lufs` (integrated LUFS), `fx_errors: []`, and `tracks_verified: 8`
|
|
|
|
#### Scenario: FX verification catches missing plugin
|
|
|
|
- GIVEN track 3 has 2 FX slots but slot 1 contains a missing plugin (empty string from `TrackFX_GetFXName`)
|
|
- WHEN the ReaScript runs FX verification
|
|
- THEN `fx_errors` SHALL contain `{"track_index": 3, "fx_index": 1, "name": "", "expected": ""}`
|
|
- AND rendering SHALL still proceed (verification does not halt the pipeline) |