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