Files
reaper-control/.sdd/changes/hook-melody/design.md
renato97 014e636889 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.
2026-05-03 23:54:29 -03:00

126 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: Hook-Based Reggaeton Melody
## Technical Approach
Replace `build_lead_track()`'s random pentatonic generation with a deterministic hook engine (`melody_engine.py`) producing identifiable repeating motifs with call-response structure and chord-aware note selection. The engine is pure functions — no I/O, no global state — operating on `list[MidiNote]` using `random.Random(seed)` for reproducibility.
## Architecture Decisions
| Decision | Choice | Rejected | Rationale |
|----------|--------|----------|-----------|
| Module location | `src/composer/melody_engine.py` | `scripts/compose.py` inline | Composer pattern already used by `rhythm.py`, `variation.py` |
| RNG strategy | `random.Random(seed)` per-call | Global `random.seed()` | Isolated RNG prevents cross-call interference; `rhythm.py` already uses this pattern |
| Note format | `list[MidiNote]` (existing schema) | New dict/tuple format | Zero adapter code; direct ClipDef compatibility |
| Scale source | `get_pentatonic()` from `compose.py` | Inline scale calc | Reuses proven helper; no duplication |
| Chord source | `CHORD_PROGRESSION` from `compose.py` | New chord dict | Single source of truth for i-VI-III-VII |
| Variation approach | Clone + mutate lists | Decorator/lazy | Simple, testable, matches motif identity requirement |
| Lead track integration | `build_lead_track()` becomes thin wrapper | Full rewrite | Minimizes compose.py diff; preserves section logic |
| Style selection | Hardcoded to "hook" initially | CLI flag | Proposal scope limitation; extensible via param later |
## Data Flow
```
compose.py::build_lead_track(sections, offsets, key_root, key_minor, seed)
├─► melody_engine.build_motif(key_root, key_minor, "hook", bars=4)
│ │
│ ├── get_pentatonic(key_root, key_minor, octave) → scale notes
│ ├── CHORD_PROGRESSION → chord tones per bar
│ ├── random.Random(seed) → deterministic RNG
│ └── returns list[MidiNote] (arch contour, chord-tone emphasis)
├─► melody_engine.apply_variation(motif, shift=0.25)
│ └── returns list[MidiNote] (same structure, offset timing)
└─► melody_engine.build_call_response(motif, bars, key_root, key_minor)
├── First half: call (motif + variation, end on V/VII)
├── Second half: response (motif, end on i)
└── returns list[MidiNote] (full section)
ClipDef(midi_notes=..., position=..., length=...) → TrackDef
```
## File Changes
| File | Action | Description |
|------|--------|-------------|
| `src/composer/melody_engine.py` | Create | `build_motif()`, `apply_variation()`, `build_call_response()` |
| `scripts/compose.py` | Modify | `build_lead_track()` delegates to `melody_engine`; pass seed |
| `tests/test_compose_integration.py` | Modify | Update `test_melody_uses_pentatonic` expectations |
| `tests/test_melody_engine.py` | Create | Unit tests for motif, variation, call-response, determinism |
## Interfaces / Contracts
```python
# src/composer/melody_engine.py
def build_motif(
key_root: str, # "A", "D", etc.
key_minor: bool, # True = minor, False = major
style: str, # "hook" | "stabs" | "smooth"
bars: int = 4, # 28 bars
seed: int = 42,
) -> list[MidiNote]:
"""Generate a 24 bar repeating motif using chord-aware scale selection."""
...
def apply_variation(
motif: list[MidiNote],
shift_beats: float = 0.0,
transpose_semitones: int = 0,
) -> list[MidiNote]:
"""Apply rhythmic shift and/or pitch transpose to motif. Returns new list."""
...
def build_call_response(
motif: list[MidiNote],
bars: int = 8,
key_root: str = "A",
key_minor: bool = True,
seed: int = 42,
) -> list[MidiNote]:
"""Build call-and-response structure: call (V/VII end) + response (i end)."""
...
# compose.py retains exact signature:
def build_lead_track(
sections, offsets, key_root, key_minor, seed=0
) -> TrackDef:
# Sections with lead: chorus, chorus2, final (unchanged)
# Clips built via melody_engine.build_call_response()
...
```
### Scale & Chord Helpers (internal to melody_engine)
```python
def _resolve_chord_tones(root: str, is_minor: bool, bar: int) -> set[int]:
"""Return MIDI pitches for active chord at given bar index (from CHORD_PROGRESSION)."""
def _resolve_tension_notes(root: str, is_minor: bool, degree: str) -> int:
"""Return V or VII pitch for call-resolution scheme."""
```
## Testing Strategy
| Layer | What to Test | Approach |
|-------|-------------|----------|
| Unit | `build_motif()` determinism | Same seed → identical output, different seed → different |
| Unit | `build_motif()` style validation | Invalid style → ValueError with message |
| Unit | `build_motif()` chord-tone ratio | Count notes on strong beats, assert ≥70% chord tones |
| Unit | `apply_variation()` identity | Note count preserved, durations preserved, IOIs preserved |
| Unit | `build_call_response()` resolution | Last note of call half = V/VII, last note overall = tonic |
| Unit | `build_call_response()` length | Notes span exactly `bars` parameter worth of beats |
| Integration | `build_lead_track()` delegation | Returns TrackDef with clips using call-response structure |
| Regression | Existing 110+ tests | All pass after updating melody assertion |
## Migration / Rollout
No migration required. `build_lead_track()` signature unchanged. Rollback = `git revert`.
## Open Questions
- None. All blocking decisions resolved above.