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:
155
.sdd/design.md
155
.sdd/design.md
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user