Files
reaper-control/.sdd/changes/archive/2026-05-03-reascript-hybrid/spec.md
renato97 48bc271afc 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
2026-05-03 22:00:26 -03:00

9.3 KiB

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:

{
  "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:

{
  "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)