- 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.
89 lines
3.9 KiB
Markdown
89 lines
3.9 KiB
Markdown
# 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 (7–9) |
|
||
| 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).
|