# 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).
