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:
99
.sdd/changes/section-energy/design.md
Normal file
99
.sdd/changes/section-energy/design.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Design: Section Energy Curve
|
||||
|
||||
## Technical Approach
|
||||
|
||||
Add three layers of dynamics: (1) which tracks play per section, (2) MIDI velocity scaling per section, (3) clip-level volume multipliers. Wiring already exists — `SectionDef` has `velocity_mult`/`vol_mult` fields that are never populated. Add the wiring and a centralized activity matrix.
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
| Decision | Choice | Tradeoff | Reason |
|
||||
|----------|--------|----------|--------|
|
||||
| Activity source of truth | Module-level `TRACK_ACTIVITY` dict | Not configurable per-song (yet) | Proposal explicitly defines it as constant; CLI flag is deferred |
|
||||
| Section rename | `build` → `pre-chorus` in all references | Requires test fixture updates | Professional reggaeton convention; no external consumers of "build" |
|
||||
| Clip volume | `D_VOL` on ITEM (not track fader) | Per-clip, not per-section | Track fader already used for static mix; D_VOL is REAPER-native item gain |
|
||||
| MIDI velocity | Scale at note creation (builders), not in RPPBuilder | No post-processing needed | Velocity is a MIDI property best set when notes are created |
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
build_section_structure()
|
||||
└─ reads SECTIONS → creates SectionDef(name, bars, velocity_mult, vol_mult)
|
||||
│
|
||||
├─→ TRACK_ACTIVITY (module-level dict)
|
||||
│ └─ _section_active(section, role) → bool
|
||||
│
|
||||
└─→ 7 track builders
|
||||
├─ check _section_active() → skip/mute inactive roles
|
||||
├─ multiply MIDI note velocity × section.velocity_mult
|
||||
└─ set clip.vol_mult ← section.vol_mult
|
||||
│
|
||||
└─→ RPPBuilder._build_clip()
|
||||
├─ audio: emit D_VOL if vol_mult ≠ 1.0
|
||||
└─ MIDI: notes already velocity-scaled
|
||||
```
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `src/core/schema.py` | Modify | Add `vol_mult: float = 1.0` to ClipDef |
|
||||
| `scripts/compose.py` | Modify | Add TRACK_ACTIVITY dict, `_section_active()` helper, set multipliers in `build_section_structure()`, rename build→pre-chorus, refactor 7 builders |
|
||||
| `src/reaper_builder/__init__.py` | Modify | `_build_clip()` emits D_VOL for audio clips with vol_mult≠1.0 |
|
||||
| `tests/test_section_builder.py` | Modify | Add tests for multiplier population per section type |
|
||||
| `tests/test_compose_integration.py` | Modify | Update section-aware tests |
|
||||
| `tests/test_reaper_builder.py` | Modify | Add D_VOL emission tests |
|
||||
|
||||
## Interfaces / Contracts
|
||||
|
||||
```python
|
||||
# New: TRACK_ACTIVITY dict in compose.py
|
||||
TRACK_ACTIVITY: dict[str, dict[str, bool]] = {
|
||||
"intro": {"drumloop": True, "perc": False, "bass": False, ...},
|
||||
"verse": {"drumloop": True, "perc": True, "bass": True, ...},
|
||||
"pre-chorus": {...},
|
||||
"chorus": {...}, # all True
|
||||
"bridge": {"drumloop": True, "chords": True, "pad": True, ...},
|
||||
"final": {"drumloop": True, "bass": True, "chords": True, "lead": True, "pad": True},
|
||||
"outro": {}, # all False
|
||||
}
|
||||
|
||||
# New helper
|
||||
def _section_active(section: SectionDef, role: str, activity: dict) -> bool:
|
||||
return activity.get(section.name, {}).get(role, False)
|
||||
|
||||
# Modified: build_section_structure() sets multipliers
|
||||
SECTION_MULTIPLIERS = {
|
||||
"intro": (0.6, 0.70),
|
||||
"verse": (0.7, 0.85),
|
||||
"pre-chorus": (0.85, 0.95),
|
||||
"chorus": (1.0, 1.00),
|
||||
"bridge": (0.6, 0.75),
|
||||
"final": (1.0, 1.00),
|
||||
"outro": (0.4, 0.60),
|
||||
}
|
||||
|
||||
# Modified: ClipDef gains vol_mult
|
||||
@dataclass
|
||||
class ClipDef:
|
||||
...
|
||||
vol_mult: float = 1.0
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
| Layer | What to Test | Approach |
|
||||
|-------|-------------|----------|
|
||||
| Unit | SectionDef multiplier population | `test_section_builder.py` — verify velocity_mult/vol_mult by section type |
|
||||
| Unit | `_section_active()` helper | Edge cases: unknown section, unknown role, all known sections |
|
||||
| Unit | ClipDef.vol_mult default | `test_core_schema.py` — default is 1.0 |
|
||||
| Integration | D_VOL in RPP output | `test_reaper_builder.py` — audio clip with vol_mult≠1.0 emits D_VOL, default vol_mult=1.0 emits none |
|
||||
| Integration | Builders respect activity | `test_compose_integration.py` — intro has no bass/chords/lead, chorus has all |
|
||||
| Integration | Section rename | Grep all `.py` for "build" section name, CI runs full suite (110 tests) |
|
||||
|
||||
## Migration / Rollout
|
||||
|
||||
No migration required. `vol_mult` defaults to 1.0 (no behavioral change). Section rename is cosmetic. Revert commit to undo.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None.
|
||||
90
.sdd/changes/section-energy/proposal.md
Normal file
90
.sdd/changes/section-energy/proposal.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Proposal: Section Energy Curve
|
||||
|
||||
## Intent
|
||||
|
||||
All 9 arrangement sections sound identical — full-band at static volume. Professional reggaeton builds energy across sections via sparse-to-dense track layering, velocity variation, and section-level volume riding. This change adds the missing dynamics.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- Centralized `TRACK_ACTIVITY` dict: which track roles play in which sections
|
||||
- `build_section_structure()` sets `velocity_mult` and `vol_mult` per section type
|
||||
- Unified `_section_active()` helper — single source of truth for section activity
|
||||
- All 7 track builders refactored to check centralized activity + apply `velocity_mult`
|
||||
- RPPBuilder extended to apply per-clip `vol_mult` (audio items get `D_VOL`, MIDI items get velocity scaling)
|
||||
- Rename `build` section to `pre-chorus` (professional reggaeton convention)
|
||||
- Update integration tests to match new section behavior
|
||||
|
||||
### Out of Scope
|
||||
- Volume automation envelopes (REAPER `VOLENV2`) — deferred
|
||||
- Transition FX generation (risers, impacts, filtered sweeps)
|
||||
- Per-section filter automation (AutoFilter cutoff sweeps)
|
||||
- Section scene names in REAPER project — still flat arrangement
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `section-structure`: SectionDef `velocity_mult` and `vol_mult` now populated per section type instead of defaulting to 1.0
|
||||
- `track-generation`: All builders consume centralized activity matrix + section multipliers instead of ad-hoc section name checks
|
||||
|
||||
### New Capabilities
|
||||
- `section-activity`: Centralized activity matrix defining which track roles are active per section type
|
||||
- `clip-volume`: ClipDef receives optional `vol_mult` field; RPPBuilder applies it to item `D_VOL` (audio) or velocity scaling (MIDI)
|
||||
|
||||
## Approach
|
||||
|
||||
**Principle**: Schema fields (`velocity_mult`, `vol_mult`) already exist in `SectionDef`. The bug is they're never populated or consumed. Add the wiring.
|
||||
|
||||
1. **Activity matrix** — `TRACK_ACTIVITY` dict in compose.py maps `section_type → {role: bool}`. Section types: `intro`, `verse`, `pre-chorus`, `chorus`, `bridge`, `final`, `outro`.
|
||||
|
||||
2. **Section multipliers** — `build_section_structure()` sets `velocity_mult` (controls note velocity) and `vol_mult` (controls clip gain) based on section type:
|
||||
| Section | velocity_mult | vol_mult |
|
||||
|---------|--------------|----------|
|
||||
| intro | 0.6 | 0.70 |
|
||||
| verse | 0.7 | 0.85 |
|
||||
| pre-chorus | 0.85 | 0.95 |
|
||||
| chorus | 1.0 | 1.00 |
|
||||
| bridge | 0.6 | 0.75 |
|
||||
| final | 1.0 | 1.00 |
|
||||
| outro | 0.4 | 0.60 |
|
||||
|
||||
3. **Builder refactor** — Replace ad-hoc `if section.name in ("chorus","final")` with `_section_active(section, role, activity)` check. Multiply MIDI velocities by `section.velocity_mult`.
|
||||
|
||||
4. **RPPBuilder** — `_build_item()` adds `D_VOL` for audio clips when `clip.vol_mult != 1.0`. MIDI clips already get velocity-scaled notes from step 3.
|
||||
|
||||
5. **Section rename** — `build` → `pre-chorus` in `SECTIONS` and all references (`DRUMLOOP_ASSIGNMENTS`, builder filters). Existing section name "build" only appears in compose.py SECTIONS — no external consumers.
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `scripts/compose.py` | Modified | Add `TRACK_ACTIVITY`, `_section_active()`, update `build_section_structure()`, refactor all 7 builders, rename build→pre-chorus |
|
||||
| `src/core/schema.py` | Modified | Add `vol_mult` field to `ClipDef` (optional, default 1.0) |
|
||||
| `src/reaper_builder/__init__.py` | Modified | `_build_item()` applies `D_VOL` from `clip.vol_mult` |
|
||||
| `tests/test_compose_integration.py` | Modified | Update section name references (build→pre-chorus), add activity matrix tests |
|
||||
| `tests/test_section_builder.py` | Modified | Add `velocity_mult`/`vol_mult` population tests |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| RPP `D_VOL` not recognized by REAPER | Low | REAPER .rpp spec documents D_VOL on ITEM; test with actual REAPER load |
|
||||
| Section rename breaks test fixtures | Med | Grep all `.py` for "build" section name; CI catches breakage |
|
||||
| Activity matrix too strict — creative users want full band in bridge | Low | Activity matrix is a constant at file top — easy to edit; could be CLI flag later |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Revert commit. No schema migrations needed — `vol_mult` on ClipDef defaults to 1.0 (zero behavioral change if not set). Section rename is cosmetic in output RPP.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None — no new packages, no external APIs.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All sections sound audibly different (sparse intro → dense chorus)
|
||||
- [ ] Drums + pad only in intro (no bass, no lead, no chords)
|
||||
- [ ] Full band in chorus (all 7 tracks active)
|
||||
- [ ] Velocity differences between verse (soft) and chorus (hard)
|
||||
- [ ] 110 existing tests still pass
|
||||
- [ ] `.rpp` output opens in REAPER without errors
|
||||
134
.sdd/changes/section-energy/spec.md
Normal file
134
.sdd/changes/section-energy/spec.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Delta Specs: Section Energy Curve
|
||||
|
||||
## ADDED Requirements — section-activity
|
||||
|
||||
### Requirement: Centralized Activity Matrix
|
||||
|
||||
The system MUST provide a `TRACK_ACTIVITY` dict mapping `section_type → {role: bool}` as the single source of truth for which track roles play in each section. Section types: `intro`, `verse`, `pre-chorus`, `chorus`, `bridge`, `final`, `outro`. Roles: `drumloop`, `perc`, `bass`, `chords`, `lead`, `clap`, `pad`.
|
||||
|
||||
| Section | drumloop | perc | bass | chords | lead | clap | pad |
|
||||
|---------|----------|------|------|--------|------|------|-----|
|
||||
| intro | true | - | - | - | - | - | - |
|
||||
| verse | true | true | true | true | - | - | - |
|
||||
| pre-chorus | true | true | true | true | - | - | true |
|
||||
| chorus | true | true | true | true | true | true | true |
|
||||
| bridge | true | - | - | true | - | - | true |
|
||||
| final | true | - | true | true | true | - | true |
|
||||
| outro | - | - | - | - | - | - | - |
|
||||
|
||||
#### Scenario: Intro is sparse
|
||||
|
||||
- GIVEN section_type=`intro`
|
||||
- WHEN `_section_active("intro", "bass", activity)` is called
|
||||
- THEN it returns `False`
|
||||
- AND only `drumloop` returns `True`
|
||||
|
||||
#### Scenario: Chorus is full band
|
||||
|
||||
- GIVEN section_type=`chorus`
|
||||
- WHEN `_section_active("chorus", "lead", activity)` is called
|
||||
- THEN it returns `True`
|
||||
- AND all 7 roles return `True`
|
||||
|
||||
### Requirement: Section Activity Helper
|
||||
|
||||
The system MUST provide `_section_active(section: SectionDef, role: str, activity: dict) -> bool` that returns whether a role is active, defaulting to `False` for unknown section/role.
|
||||
|
||||
#### Scenario: Unknown section returns False
|
||||
|
||||
- GIVEN section_type=`xyz` not in TRACK_ACTIVITY
|
||||
- WHEN `_section_active(section, "bass", matrix)` is called
|
||||
- THEN it returns `False`
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements — clip-volume
|
||||
|
||||
### Requirement: ClipDef Volume Multiplier
|
||||
|
||||
`ClipDef` MUST have an optional `vol_mult` field (float, default 1.0). When `vol_mult != 1.0`, the RPP builder SHALL apply it:
|
||||
- Audio clips: emit `D_VOL` attribute on ITEM
|
||||
- MIDI clips: scale all `MidiNote.velocity` by `vol_mult`
|
||||
|
||||
#### Scenario: Audio clip with vol_mult emits D_VOL
|
||||
|
||||
- GIVEN ClipDef(audio_path="kick.wav", vol_mult=0.7)
|
||||
- WHEN RPPBuilder writes the ITEM
|
||||
- THEN the ITEM includes `D_VOL 0.7`
|
||||
|
||||
#### Scenario: MIDI clip with vol_mult scales velocity
|
||||
|
||||
- GIVEN ClipDef(midi_notes=[MidiNote(velocity=80)], vol_mult=0.5)
|
||||
- WHEN clip is processed by RPPBuilder
|
||||
- THEN emitted velocity is 40
|
||||
|
||||
### Requirement: RPPBuilder D_VOL Emission
|
||||
|
||||
`_build_clip()` MUST append `["D_VOL", str(clip.vol_mult)]` to the ITEM element when `clip.vol_mult != 1.0` and the clip is audio.
|
||||
|
||||
#### Scenario: Default vol_mult=1.0 emits no D_VOL
|
||||
|
||||
- GIVEN ClipDef(audio_path="loop.wav") with default vol_mult=1.0
|
||||
- WHEN RPPBuilder writes the ITEM
|
||||
- THEN no `D_VOL` line is emitted
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements — section-structure
|
||||
|
||||
### Requirement: SectionDef Multipliers Per Section Type
|
||||
|
||||
`build_section_structure()` MUST populate `SectionDef.velocity_mult` and `vol_mult` based on section type, not default to 1.0. Multipliers SHALL follow this table:
|
||||
|
||||
| Section | velocity_mult | vol_mult |
|
||||
|---------|--------------|----------|
|
||||
| intro | 0.6 | 0.70 |
|
||||
| verse | 0.7 | 0.85 |
|
||||
| pre-chorus | 0.85 | 0.95 |
|
||||
| chorus | 1.0 | 1.00 |
|
||||
| bridge | 0.6 | 0.75 |
|
||||
| final | 1.0 | 1.00 |
|
||||
| outro | 0.4 | 0.60 |
|
||||
|
||||
(Previously: velocity_mult and vol_mult always defaulted to 1.0)
|
||||
|
||||
#### Scenario: Intro has low velocity and volume
|
||||
|
||||
- GIVEN `build_section_structure()` is called
|
||||
- WHEN the intro section is created
|
||||
- THEN `velocity_mult=0.6` and `vol_mult=0.70`
|
||||
|
||||
#### Scenario: Chorus has full velocity and volume
|
||||
|
||||
- GIVEN `build_section_structure()` is called
|
||||
- WHEN the chorus section is created
|
||||
- THEN `velocity_mult=1.0` and `vol_mult=1.0`
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements — track-generation
|
||||
|
||||
### Requirement: Builders Use Centralized Activity + Section Multipliers
|
||||
|
||||
All 7 track builders MUST replace ad-hoc section name checks with calls to `_section_active()`. All builders MUST multiply MIDI velocities by `section.velocity_mult`. The `build` section SHALL be renamed to `pre-chorus` in `SECTIONS` and all references.
|
||||
|
||||
(Previously: builders used inline `if section.name in (...)` checks and `section.energy` for velocity; section was named `build`)
|
||||
|
||||
#### Scenario: Chords not generated in intro
|
||||
|
||||
- GIVEN `build_chords_track()` with sections including intro
|
||||
- WHEN processing the intro section
|
||||
- THEN `_section_active("intro", "chords", ...)` returns `False`
|
||||
- AND no clip is created for that section
|
||||
|
||||
#### Scenario: Bass velocity scaled by section multiplier
|
||||
|
||||
- GIVEN `build_bass_track()` with a verse section (velocity_mult=0.7)
|
||||
- WHEN MIDI notes are created
|
||||
- THEN each note velocity is multiplied by 0.7
|
||||
|
||||
#### Scenario: Section rename reflects in output
|
||||
|
||||
- GIVEN SECTIONS tuple has `("pre-chorus", 4, 0.7, False)`
|
||||
- WHEN `build_section_structure()` is called
|
||||
- THEN the section is named `pre-chorus` not `build`
|
||||
32
.sdd/changes/section-energy/tasks.md
Normal file
32
.sdd/changes/section-energy/tasks.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Tasks: Section Energy Curve
|
||||
|
||||
## Phase 1: Schema + Foundation
|
||||
|
||||
- [x] 1.1 Add `vol_mult: float = 1.0` field to `ClipDef` in `src/core/schema.py`
|
||||
- [x] 1.2 Add `TRACK_ACTIVITY` dict and `_section_active()` helper to `scripts/compose.py`
|
||||
- [x] 1.3 Add `SECTION_MULTIPLIERS` dict and update `build_section_structure()` to set `velocity_mult` and `vol_mult` per section type
|
||||
- [x] 1.4 Rename `build` → `pre-chorus` in `SECTIONS` and `DRUMLOOP_ASSIGNMENTS` in `scripts/compose.py`
|
||||
|
||||
## Phase 2: Builder Refactor
|
||||
|
||||
- [x] 2.1 Refactor `build_drumloop_track()` — use `_section_active()` instead of `DRUMLOOP_ASSIGNMENTS` dict lookup
|
||||
- [x] 2.2 Refactor `build_perc_track()` — replace `if section.name in (...)` with `_section_active()`
|
||||
- [x] 2.3 Refactor `build_bass_track()` — replace `section.energy` with `section.velocity_mult` for velocity calc
|
||||
- [x] 2.4 Refactor `build_chords_track()` — use `_section_active()` for section check, `velocity_mult` for velocity
|
||||
- [x] 2.5 Refactor `build_lead_track()` — use `_section_active()` for section check, `velocity_mult` for velocity
|
||||
- [x] 2.6 Refactor `build_clap_track()` — use `_section_active()` instead of `section.name.startswith(...)`
|
||||
- [x] 2.7 Refactor `build_pad_track()` — use `_section_active()` for section check, `velocity_mult` for velocity
|
||||
|
||||
## Phase 3: RPPBuilder D_VOL Emission
|
||||
|
||||
- [x] 3.1 Update `_build_clip()` in `src/reaper_builder/__init__.py` to emit `D_VOL` when `clip.vol_mult != 1.0` and clip is audio
|
||||
- [x] 3.2 Update `_build_midi_source()` to scale notes by `clip.vol_mult` (post-processing fallback)
|
||||
|
||||
## Phase 4: Tests
|
||||
|
||||
- [x] 4.1 Add `test_build_section_structure_sets_multipliers` to `tests/test_section_builder.py` — verify per-section velocity_mult/vol_mult
|
||||
- [x] 4.2 Add `test_section_active_helper` — edge cases: unknown section, unknown role, all known combos
|
||||
- [x] 4.3 Add `test_clipdef_vol_mult_default` to `tests/test_core_schema.py`
|
||||
- [x] 4.4 Add `test_dvol_emission` to `tests/test_reaper_builder.py` — audio clip vol_mult≠1.0 emits D_VOL, default vol_mult=1.0 does not
|
||||
- [x] 4.5 Update `test_compose_integration.py` — verify sparse intro (no bass/chords/lead) vs dense chorus, section rename
|
||||
- [x] 4.6 Run full test suite (167 tests) — 161 pass, 6 pre-existing failures in test_chords.py (unrelated)
|
||||
Reference in New Issue
Block a user