feat: SDD workflow — test sync, song generation + validation, ReaScript hybrid pipeline

- 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
This commit is contained in:
renato97
2026-05-03 22:00:26 -03:00
parent 7729d5f12f
commit 48bc271afc
25 changed files with 2842 additions and 343 deletions

View File

@@ -0,0 +1,186 @@
# 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)