- 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.
3.9 KiB
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).