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

@@ -199,7 +199,162 @@ Kick-avoidance: skip notes within ±0.125 beats of a kick hit
---
## Generation + Validation Pipeline
### CLI: generate-song
A thin CLI wrapper (`scripts/generate.py`) delegates to `compose.main()` and optionally validates output.
**Flags:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--bpm` | float | 95 | Tempo |
| `--key` | str | Am | Musical key |
| `--output` | str | output/song.rpp | Output path |
| `--seed` | int | 42 | Random seed (reproducibility) |
| `--validate` | flag | False | Run `validate_rpp_output()` after generation |
**BPM validation:** Raises `ValueError` if `bpm <= 0`.
**Data flow:**
```
generate.py main()
├── argparse (--bpm, --key, --output, --seed, --validate)
├── import compose.main
│ └── compose.main(args) → writes output.rpp
└── if --validate:
└── validate_rpp_output(output_path) → list[str]
├── count <TRACK> blocks
├── regex <SOURCE WAVE> for audio paths → verify exist
├── regex NOTE for MIDI clip notes
├── compute arrangement end beat
├── regex AUXRECV for send routing
└── count VST/FXCHAIN for plugin chains
```
### Validator: rpp_validator.py
`src/validator/rpp_validator.py` exports `validate_rpp_output(rpp_path: str) -> list[str]`.
Returns `[]` for valid `.rpp`, or a list of error strings for any violation.
**6 structural checks:**
1. **Track count** — must be exactly 9 (`<TRACK` blocks)
2. **Audio clip paths** — all `<SOURCE WAVE "...">` paths must exist on disk
3. **MIDI note presence** — MIDI items must contain at least one `NOTE` event
4. **Arrangement duration** — last item end must be ≥ 52 bars × 4 beats at given BPM
5. **Send routing** — each non-return track must have `AUXRECV` to a return track
6. **Plugin chains** — each non-return track must have `<FXCHAIN>` with `VST` entry
### Perc Loop Fallback
`build_perc_track()` in `compose.py` silently skips missing `91bpm bellako percloop.wav` files. No changes needed — this matches the spec requirement.
---
## ReaScript Hybrid Pipeline (Phase 1 + Phase 2)
### Overview
Two-phase architecture: Phase 1 generates `.rpp` offline; Phase 2 runs inside REAPER via ReaScript for FX verification, track calibration, rendering, and loudness measurement.
```
Phase 1 (offline, Python) Phase 2 (inside REAPER, ReaScript)
───────────────────────── ─────────────────────────────────
RPPBuilder.build() Main_openProject(rpp_path)
│ │
▼ ▼
output/song.rpp TrackFX_GetCount + TrackFX_GetFXName
→ fx_errors (missing plugins)
│ │
│ SetMediaTrackInfo_Value(VOLUME/PAN)
│ CreateTrackSend for each send
│ │
│ ▼
│ Main_RenderFile → output/song.wav
│ │
│ ▼
│ CalcMediaSrcLoudness
│ → integrated_lufs, short_term_lufs
│ │
│ ▼
│ write result.json
run_in_reaper.py
→ generate phase2.py
→ write command.json ──────────────────────────────►
→ poll result.json ◄──────────────────────────────
→ print LUFS, write fx_errors.json
```
### Bridge: JSON File Protocol
Communication via `fl_control_command.json` / `fl_control_result.json` in REAPER ResourcePath:
- No network dependency
- REAPER owns timing — avoids race conditions
- Human-readable JSON for debugging
### Command JSON 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}]
}
]
}
```
### Result JSON 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
}
```
### Key Files
| File | Role |
|------|------|
| `src/reaper_scripting/__init__.py` | `ReaScriptGenerator` — generates self-contained Python ReaScript |
| `src/reaper_scripting/commands.py` | `ReaScriptCommand`, `ReaScriptResult` dataclasses + protocol |
| `scripts/run_in_reaper.py` | CLI: generate script → write command → poll result → output LUFS |
### ReaScript API Subset (Stable)
`Main_openProject`, `TrackFX_GetCount`, `TrackFX_GetFXName`, `TrackFX_AddByName`, `SetMediaTrackInfo_Value`, `CreateTrackSend`, `Main_RenderFile`, `CalcMediaSrcLoudness`, `GetFunctionMetadata`, and others listed in the spec.
### Architecture Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| JSON file protocol over python-reapy | `fl_control_command.json` / `fl_control_result.json` | No network dependency; REAPER owns timing |
| Self-contained ReaScript (no `import json`) | Hand-rolled JSON parser via string splitting | Maximum REAPER version compatibility |
| Separate `commands.py` for protocol | Protocol isolated from generator | Stable, testable in isolation |
| `track_calibration` JSON array | Stateless interface for volume/pan/sends | Retry-friendly; replay on REAPER crash |
---
## Open Questions
- [ ] `skeleton.py` `EMPTY_SAMPLER_CHANNELS` includes `{17,18,19}` — need to confirm that adding melodic channels beyond 19 doesn't require reference FLP changes or if we expand the sampler clone range.
- [ ] `sample_index.json` entries use `original_path` (absolute Windows paths). CLI on other machines will break. Decision needed: embed relative paths or make selector rebase paths against a configurable `library_root`.
- [ ] 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?