# 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" `