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.
This commit is contained in:
88
.sdd/changes/transitions-fx/design.md
Normal file
88
.sdd/changes/transitions-fx/design.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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).
|
||||
77
.sdd/changes/transitions-fx/proposal.md
Normal file
77
.sdd/changes/transitions-fx/proposal.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Proposal: transitions-fx
|
||||
|
||||
## Intent
|
||||
|
||||
9 sections play back-to-back with zero transition — the song feels like disjointed loops. Add transition FX (risers, impacts, sweeps) at section boundaries to glue sections into a coherent arrangement.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- Place transition FX clips (audio samples) on a dedicated "Transition FX" track at section boundaries
|
||||
- Riser/wash FX: 2–4 beats before section changes (e.g., build → chorus drop)
|
||||
- Impact/hit FX: on the downbeat of CHORUS, FINAL, VERSE2 entries
|
||||
- Filter sweep simulation via fade-in/fade-out on adjacent clips
|
||||
- Transition plan: which boundary gets which FX type + duration
|
||||
- Reuse existing FX-role samples from library (impacts, risers, transition FX, wash)
|
||||
|
||||
### Out of Scope
|
||||
- Synthesized FX generation (numpy waveform synthesis) — deferred to future
|
||||
- MIDI CC filter automation in RPP (no CC support in builder today)
|
||||
- Per-track volume automation curves
|
||||
- Reverse cymbal (no suitable samples in library)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `transition-fx`: Placement of audio FX clips at section boundaries for arrangement glue
|
||||
|
||||
### Modified Capabilities
|
||||
None — existing section/track structure unchanged.
|
||||
|
||||
## Approach
|
||||
|
||||
**Audio samples from library** — the library has 57 FX-role samples including:
|
||||
- Impacts: `fx_C2_126_boomy` (2.5s, from `impact.wav`)
|
||||
- Risers: `fx_C#5_123_aggressive` (30s), `fx_G3_143_boomy` (6.6s, "RISER 3")
|
||||
- Transition FX: 4 "transicion fx" variants (1.0–1.7s)
|
||||
- Wash/noise: `fx_G#6_136_aggressive` (3.3s)
|
||||
- Short shots/gates: "CAMTAZO 1–2" (1.5–2.0s), "PUERTA" (0.2s)
|
||||
|
||||
Place audio clips on a new "FX" track at section boundaries:
|
||||
- **Riser/wash**: starts 2–4 beats BEFORE boundary, ends on boundary downbeat
|
||||
- **Impact**: starts on boundary downbeat, short duration (1–2 beats)
|
||||
- Use existing `fade_in`/`fade_out` on ClipDef for filter-like sweeps
|
||||
- Use SampleSelector with `role="fx"` to pick compatible samples
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `scripts/compose.py` | Modified | Add `build_fx_track()` — places FX clips between sections |
|
||||
| `src/core/schema.py` | Unchanged | `ClipDef` already has `position`, `length`, `audio_path`, `fade_in`, `fade_out` |
|
||||
| `src/reaper_builder/__init__.py` | Unchanged | Audio clip building already works |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| FX samples may not match project key | Low | FX role is ATONAL_ROLES — key scoring skipped by selector |
|
||||
| Long riser samples exceed needed duration | Low | Clip length sets playback window; sample trimmed automatically |
|
||||
| No suitable riser for specific boundary | Med | Fall back to fade_in on next section clip |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Remove `build_fx_track()` call from `main()`. Existing tracks untouched.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `data/sample_index.json` (already exists)
|
||||
- `SampleSelector` with `role="fx"` (already works)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `python scripts/compose.py --bpm 99 --key Am` produces .rpp with FX clips at section boundaries
|
||||
- [ ] At least one FX clip between: build→chorus, chorus→verse2, bridge→final, outro end
|
||||
- [ ] FX clips have appropriate fade_in/fade_out curves
|
||||
- [ ] 110 existing tests continue to pass
|
||||
- [ ] Song renders without gaps — FX clips overlap/bridge sections
|
||||
95
.sdd/changes/transitions-fx/spec.md
Normal file
95
.sdd/changes/transitions-fx/spec.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Transition FX Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Glue sections together by placing audio FX clips at arrangement boundaries using existing `role="fx"` library samples.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: FX Track Existence
|
||||
|
||||
The system MUST create a dedicated "Transition FX" audio track with clips at 7 section boundaries.
|
||||
|
||||
#### Scenario: FX track present in arrangement
|
||||
|
||||
- GIVEN a 9-section song
|
||||
- WHEN `compose.py` runs
|
||||
- THEN a track named "Transition FX" exists with 7+ audio clips at boundary positions
|
||||
|
||||
### Requirement: Riser Before Climax
|
||||
|
||||
A riser/wash FX MUST start 2–4 beats before build→chorus and bridge→final boundaries, ending ON the boundary downbeat.
|
||||
|
||||
#### Scenario: Riser before chorus
|
||||
|
||||
- GIVEN build ends at beat 64 (bar 16)
|
||||
- WHEN FX is built
|
||||
- THEN a riser at position 60 (beat 60), length 4, `fade_in` ≥ 1.0s
|
||||
|
||||
#### Scenario: Riser before final
|
||||
|
||||
- GIVEN bridge ends at beat 176 (bar 44)
|
||||
- WHEN FX is built
|
||||
- THEN a riser at position 172, length 4, `fade_in` ≥ 1.0s
|
||||
|
||||
#### Scenario: Riser before chorus2
|
||||
|
||||
- GIVEN verse2 ends at beat 128 (bar 32)
|
||||
- WHEN FX is built
|
||||
- THEN a riser at position 124, length 4, `fade_in` ≥ 1.0s
|
||||
|
||||
### Requirement: Impact on Section Downbeat
|
||||
|
||||
An impact/stab FX MUST start on beat 1 of CHORUS (beat 64) and FINAL (beat 176).
|
||||
|
||||
#### Scenario: Impact on chorus beat 1
|
||||
|
||||
- GIVEN chorus starts at beat 64
|
||||
- WHEN FX is built
|
||||
- THEN an impact clip at position 64, length 1–2 beats, `fade_out` ≥ 0.2s
|
||||
|
||||
#### Scenario: Impact on final beat 1
|
||||
|
||||
- GIVEN final starts at beat 176
|
||||
- WHEN FX is built
|
||||
- THEN an impact clip at position 176, length 1–2 beats
|
||||
|
||||
### Requirement: Transition Sweeps Between Verses
|
||||
|
||||
Short transition FX MUST bridge chorus→verse2 (beat 96) and chorus2→bridge (beat 160).
|
||||
|
||||
#### Scenario: Sweep bridges chorus to verse2
|
||||
|
||||
- GIVEN chorus ends at beat 96
|
||||
- WHEN FX is built
|
||||
- THEN a transition clip at position 94, length 2 beats, `fade_in` and `fade_out` > 0
|
||||
|
||||
### Requirement: FX Sample Selection
|
||||
|
||||
The system SHALL select FX samples via `SampleSelector.select_one(role="fx")`, favoring short samples for impacts, long for risers.
|
||||
|
||||
#### Scenario: FX role returns candidates
|
||||
|
||||
- GIVEN 57 FX samples in library with ATONAL_ROLES including "fx"
|
||||
- WHEN `select(role="fx")` is called
|
||||
- THEN non-empty result returned; key scoring skipped (neutral 0.5)
|
||||
|
||||
### Requirement: Fade Curves
|
||||
|
||||
FX clips MUST use `fade_in`/`fade_out`. Risers: `fade_in` ≥ 0.3s. Impacts: `fade_out` ≥ 0.2s.
|
||||
|
||||
#### Scenario: Riser fades in, impact fades out
|
||||
|
||||
- GIVEN riser and impact clips defined
|
||||
- WHEN ClipDef is created
|
||||
- THEN riser.fade_in > 0 AND impact.fade_out > 0
|
||||
|
||||
### Requirement: FX Track Mixing
|
||||
|
||||
The FX track SHALL have volume ≤ 0.80 and send to Reverb/Delay returns.
|
||||
|
||||
#### Scenario: FX track has moderate volume and sends
|
||||
|
||||
- GIVEN "Transition FX" track created
|
||||
- WHEN track is defined
|
||||
- THEN volume = 0.72, send_level includes reverb (0.08) and delay (0.05)
|
||||
27
.sdd/changes/transitions-fx/tasks.md
Normal file
27
.sdd/changes/transitions-fx/tasks.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Tasks: Transitions FX
|
||||
|
||||
## Phase 1: FX Transition Map
|
||||
|
||||
- [x] 1.1 Add `FX_TRANSITIONS` dict to `scripts/compose.py`: `{boundary_index: (type, start_offset, length, fade_in, fade_out)}` with 8 entries matching design boundary map
|
||||
- [x] 1.2 Add `FX_ROLE = "fx"` constant referencing ATONAL_ROLES membership
|
||||
|
||||
## Phase 2: Build FX Track
|
||||
|
||||
- [x] 2.1 Implement `build_fx_track(sections, offsets, selector, seed=0)` — iterates `FX_TRANSITIONS`, computes clip positions from offsets, selects FX samples
|
||||
- [x] 2.2 For each boundary: call `selector.select_one(role="fx", seed=seed + idx)` to pick sample
|
||||
- [x] 2.3 Create `ClipDef(position, length, name, audio_path, fade_in, fade_out)` per boundary
|
||||
- [x] 2.4 Build `TrackDef("Transition FX", volume=0.72, clips=[...], send_level={reverb: 0.08, delay: 0.05})`
|
||||
- [x] 2.5 Add docstring explaining boundary map and FX types (riser/impact/sweep/transition)
|
||||
|
||||
## Phase 3: Integration
|
||||
|
||||
- [x] 3.1 Call `build_fx_track()` in `main()` after clap track, before pad track
|
||||
- [x] 3.2 Verify send wiring loop handles new track (existing code; confirm no regression)
|
||||
|
||||
## Phase 4: Testing & Verification
|
||||
|
||||
- [x] 4.1 Write unit test: `build_fx_track` returns TrackDef with exactly 8 clips
|
||||
- [x] 4.2 Write unit test: clip positions and fade values match design's boundary map
|
||||
- [x] 4.3 Write unit test: all clips have `audio_path` set (not None)
|
||||
- [x] 4.4 Write integration test: `compose.py --bpm 99 --key Am --output /tmp/test.rpp` produces valid .rpp with "Transition FX" track
|
||||
- [x] 4.5 Run full `pytest` suite — all 110 existing tests pass
|
||||
Reference in New Issue
Block a user