Files
reaper-control/.sdd/changes/transitions-fx/design.md
renato97 014e636889 feat: professional reggaeton production engine — 7 SDD changes, 302 tests
- section-energy: track activity matrix + volume/velocity multipliers per section
- smart-chords: ChordEngine with voice leading, inversions, 4 emotion modes
- hook-melody: melody engine with hook/stabs/smooth styles, call-and-response
- mix-calibration: Calibrator module (LUFS volumes, HPF/LPF, stereo, sends, master)
- transitions-fx: FX track with risers/impacts/sweeps at section boundaries
- sidechain: MIDI CC11 bass ducking on kick hits via DrumLoopAnalyzer
- presets-pack: role-aware plugin presets (Serum/Decapitator/Omnisphere per role)

Full SDD pipeline (propose→spec→design→tasks→apply→verify) for all 7 changes.
302/302 tests passing.
2026-05-03 23:54:29 -03:00

89 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: Transitions FX
## Technical Approach
Add `build_fx_track()` to `scripts/compose.py` that places audio FX clips from the sample library at 7 section boundaries. Uses `SampleSelector.select_one(role="fx")` with per-type character hints. Reuses `ClipDef.fade_in/out`. New track inserted after Clap, before Pad — after main tracks, before sends are wired.
## Architecture Decisions
| Decision | Choice | Rejected | Rationale |
|----------|--------|----------|-----------|
| One FX track vs per-section | Single dedicated track | Per-section tracks | Simpler; one import per sample in REAPER; manageable clip count (79) |
| Sample selection | Weighted random top-5 | Pinned specific files | Variety across runs; selector scoring already works |
| Boundary timing | Hardcoded beat-offset map | Audio analysis | Section structure is deterministic; bar counts are fixed |
| Riser+impact at chorus | Two clips, same boundary | Single combined clip | Requires different timing; riser before boundary, impact on it |
## Data Flow
```
SECTIONS → offsets (bar → beat)
FX_TRANSITIONS map: {boundary_idx: (type, start_offset, length, fade_in, fade_out)}
build_fx_track(sections, offsets, selector, seed)
├── for each entry in FX_TRANSITIONS:
│ ├── boundary_beat = offsets[boundary_idx] * 4
│ ├── position = boundary_beat + start_offset
│ ├── sample = selector.select_one(role="fx", seed=seed+idx)
│ └── ClipDef(position, length, audio_path, fade_in, fade_out)
TrackDef("Transition FX", volume=0.72, clips=[...], send_level={...})
```
## Boundary → FX Map
| # | Boundary | Beat | FX Type | Position | Length | Fade In | Fade Out |
|---|----------|------|---------|----------|--------|---------|----------|
| 2 | verse→build | 48 | sweep | 46 | 2 | 0.3 | 0.0 |
| 3 | build→chorus | 64 | **riser** | 60 | 4 | 1.5 | 0.0 |
| 3 | build→chorus | 64 | **impact** | 64 | 2 | 0.0 | 0.3 |
| 4 | chorus→verse2 | 96 | transition | 94 | 2 | 0.2 | 0.2 |
| 5 | verse2→chorus2 | 128 | riser | 124 | 4 | 1.0 | 0.0 |
| 6 | chorus2→bridge | 160 | sweep | 158 | 2 | 0.2 | 0.2 |
| 7 | bridge→final | 176 | riser | 172 | 4 | 1.0 | 0.0 |
| 8 | final→outro | 208 | sweep | 206 | 2 | 0.3 | 0.5 |
## File Changes
| File | Action | Description |
|------|--------|-------------|
| `scripts/compose.py` | Modify | Add `FX_TRANSITIONS` dict + `build_fx_track()` (~50 lines); call in `main()` after clap track, before return tracks |
## Key Implementation Detail
`SampleSelector.select_one()` has a `seed` kwarg — new in the selector API. If not yet supported, use `select(role="fx", limit=5)` with manual `random.choice()`. Since FX is in `ATONAL_ROLES`, key compatibility scoring is skipped (neutral 0.5).
## Track Ordering
```
tracks = [
build_drumloop_track(...), # 0
build_perc_track(...), # 1
build_bass_track(...), # 2
build_chords_track(...), # 3
build_lead_track(...), # 4
build_clap_track(...), # 5
build_fx_track(...), # 6 ← NEW
build_pad_track(...), # 7
]
return_tracks = create_return_tracks() # 8 (Reverb), 9 (Delay)
```
Send wiring applies to all non-return tracks automatically via existing loop. FX track sends: Reverb=0.08, Delay=0.05.
## Testing Strategy
| Layer | What | How |
|-------|------|-----|
| Unit | `build_fx_track` returns TrackDef with 8 clips | Mock selector via `SampleSelector.__init__` patching |
| Unit | Clip positions match boundary map | Assert `clip.position` values equal expected beats |
| Integration | End-to-end .rpp output | `compose.py --bpm 99 --key Am --output test.rpp`; grep for "Transition FX" `<TRACK` block |
| Existing | 110 tests pass | `pytest` before/after regression |
## Open Questions
None — all dependencies exist today (`SampleSelector`, `ClipDef.fade_in/out`, `SECTIONS` structure).