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:
125
.sdd/changes/hook-melody/design.md
Normal file
125
.sdd/changes/hook-melody/design.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 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, # 2–8 bars
|
||||
seed: int = 42,
|
||||
) -> list[MidiNote]:
|
||||
"""Generate a 2–4 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.
|
||||
Reference in New Issue
Block a user