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.
|
||||
86
.sdd/changes/hook-melody/proposal.md
Normal file
86
.sdd/changes/hook-melody/proposal.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Proposal: Hook-Based Reggaeton Melody
|
||||
|
||||
## Intent
|
||||
|
||||
`build_lead_track()` generates random pentatonic notes — no hook, no identity, no rhythmic motif. Professional reggaeton leads have memorable hooks (repeating motif), rhythmic alignment with the dembow grid, call-and-response structure, and chord-tone emphasis on strong beats. This change replaces random generation with a structured hook engine producing identifiable, repeating motifs with controlled variation.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- **Hook engine module** (`src/composer/melody_engine.py`) — generates motifs, variations, call-response
|
||||
- **3 reggaeton styles**: "stabs" (syncopated hits on 1, 2.5, 3, 3.5), "smooth" (stepwise eighth notes), "hook" (arch contour, chord tones on strong beats)
|
||||
- **Motif + variation loop**: 2–4 bar motif repeated 2–4x with transpose/rhythmic-shift variations
|
||||
- **Call-and-response**: first half = call (ends on V/VII), second half = response (resolves to i)
|
||||
- **Chord-aware note selection**: strong beats (1, 3) favor chord tones; weak beats use scale passing tones
|
||||
- **Replace `build_lead_track()`** in `compose.py` to delegate to the new engine
|
||||
- **Tests** for deterministic output, motif identity preserved across variations, call-response resolution
|
||||
|
||||
### Out of Scope
|
||||
- MIDI velocity humanization / groove quantization
|
||||
- User-selectable style at CLI (hardcoded to "hook" style initially)
|
||||
- Chord progression generation (uses existing `CHORD_PROGRESSION` from compose.py)
|
||||
- Pad/chords/bass refactoring — lead only
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `melody-engine`: Deterministic hook generation with motif, variation, call-response, and 3 reggaeton styles. Chord-aware via `CHORD_PROGRESSION` input.
|
||||
|
||||
### Modified Capabilities
|
||||
- None at spec level. `build_lead_track()` API unchanged (same signature). Behavior changes from random to deterministic, but callers see same interface.
|
||||
|
||||
## Approach
|
||||
|
||||
New module `src/composer/melody_engine.py` with:
|
||||
|
||||
1. **`build_motif(key_root, key_minor, style, bars=4)`** → `list[MidiNote]`
|
||||
- Style "hook": arch contour, chord tones on 0, 2, 4... beats, 4–8 notes
|
||||
- Style "stabs": short 16th hits on [1.0, 2.5, 3.0, 3.5] per bar
|
||||
- Style "smooth": stepwise scalar motion at eighth-note density
|
||||
- Chords resolved from `CHORD_PROGRESSION` for chord-tone selection
|
||||
|
||||
2. **`apply_variation(motif, shift=0, transpose=0)`** → variation
|
||||
- Rhythmic shift: offset within the grid
|
||||
- Transpose: ±octave or ±third within scale
|
||||
|
||||
3. **`build_call_response(motif, sections, key_root, key_minor)`** → `list[ClipDef]`
|
||||
- First half = call (motif + slight variation, ends on tension note)
|
||||
- Second half = response (motif, resolves to tonic)
|
||||
- Repeats for section length
|
||||
|
||||
`compose.py` `build_lead_track()` becomes thin wrapper calling `melody_engine`. All existing tests pass with updated expected values.
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `src/composer/melody_engine.py` | New | Hook engine — motifs, variations, call-response |
|
||||
| `scripts/compose.py` | Modified | `build_lead_track()` delegates to melody_engine; `get_pentatonic()` stays as helper |
|
||||
| `tests/test_compose_integration.py` | Modified | Update `test_melody_uses_pentatonic` to assert motif structure |
|
||||
| `tests/test_section_builder.py` | None | `get_pentatonic` tests unaffected |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| Deterministic melody sounds repetitive | Med | 3 style options + variation params provide diversity; section energy scales velocity |
|
||||
| Chord-awareness breaks if CHORD_PROGRESSION changes format | Low | Hardcoded in compose.py — same module owns both; integration test catches mismatch |
|
||||
| Motif too short for long sections (8+ bars) | Low | Call-response repeats motif to fill bars; edge case validated in tests |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Revert `build_lead_track()` to original random-pentatonic implementation (git revert). No schema or API changes — pure function replacement.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `CHORD_PROGRESSION` constant from `compose.py` (existing)
|
||||
- `get_pentatonic()` helper from `compose.py` (kept, reused)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `build_lead_track()` produces identical output for same seed+key input (deterministic)
|
||||
- [ ] Generated melody contains a repeating 2–4 bar motif with ≤2 variations
|
||||
- [ ] Call section ends on V or VII degree; response resolves to i
|
||||
- [ ] Strong beats (quarter positions) use chord tones ≥70% of the time
|
||||
- [ ] All 110+ existing tests pass
|
||||
- [ ] 5+ new tests for melody_engine: motif identity, variation bounds, call-response resolution
|
||||
121
.sdd/changes/hook-melody/spec.md
Normal file
121
.sdd/changes/hook-melody/spec.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Delta for melody-engine
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
| # | Requirement | RFC |
|
||||
|---|------------|-----|
|
||||
| R1 | Motif generation with 3 reggaeton styles | MUST |
|
||||
| R2 | Deterministic output from seed | MUST |
|
||||
| R3 | Call-and-response phrase structure | MUST |
|
||||
| R4 | Chord-aware note selection | MUST |
|
||||
| R5 | Motif variation via transpose/rhythmic shift | SHOULD |
|
||||
| R6 | build_lead_track() delegation | MUST |
|
||||
|
||||
### Requirement: Motif Generation (R1)
|
||||
|
||||
`build_motif(key_root, key_minor, style, bars, seed)` MUST generate a 2–4 bar repeating motif using scale-aware note selection. Three styles:
|
||||
|
||||
- **hook**: Arch contour (ascend then descend), chord tones on beats 0, 2, 4..., 4–8 notes
|
||||
- **stabs**: Short 16th-duration hits on dembow grid positions [1.0, 2.5, 3.0, 3.5] per bar
|
||||
- **smooth**: Stepwise scalar motion at eighth-note density, ≤2 semitones between consecutive notes
|
||||
|
||||
MUST accept `bars` parameter (2–8) defaulting to 4. MUST return `list[MidiNote]`.
|
||||
|
||||
#### Scenario: hook style generates arch contour with chord tones
|
||||
|
||||
- GIVEN key Am, style "hook", bars=4, seed=42
|
||||
- WHEN `build_motif("A", True, "hook", 4, 42)` is called
|
||||
- THEN returns 4–12 MidiNote objects
|
||||
- AND notes on quarter-beat positions (0, 2, 4, …) are within the i-VI-III-VII chord tones ≥70% of the time
|
||||
|
||||
#### Scenario: stabs style generates dembow-positioned hits
|
||||
|
||||
- GIVEN key Am, style "stabs", bars=2, seed=1
|
||||
- WHEN `build_motif("A", True, "stabs", 2, 1)` is called
|
||||
- THEN all note start times are within {1.0, 2.5, 3.0, 3.5} per bar
|
||||
- AND each note duration ≤ 0.25 beats (16th note)
|
||||
|
||||
#### Scenario: smooth style generates stepwise motion
|
||||
|
||||
- GIVEN key Am, style "smooth", bars=4, seed=7
|
||||
- WHEN `build_motif("A", True, "smooth", 4, 7)` is called
|
||||
- THEN pitch difference between consecutive notes ≤ 2 semitones
|
||||
|
||||
#### Scenario: invalid style raises ValueError
|
||||
|
||||
- GIVEN an unrecognized style string
|
||||
- WHEN `build_motif("A", True, "invalid", 4, 42)` is called
|
||||
- THEN raises ValueError with message containing valid styles
|
||||
|
||||
### Requirement: Deterministic Output (R2)
|
||||
|
||||
`build_motif()` and `apply_variation()` MUST produce identical output for identical input parameters (key, style, bars, seed). MUST NOT rely on global RNG state.
|
||||
|
||||
#### Scenario: same seed produces identical output
|
||||
|
||||
- GIVEN fixed parameters
|
||||
- WHEN `build_motif("A", True, "hook", 4, 42)` is called twice
|
||||
- THEN both calls return identical lists of MidiNote objects
|
||||
|
||||
#### Scenario: different seeds produce different output
|
||||
|
||||
- GIVEN same key and style but different seeds
|
||||
- WHEN `build_motif("A", True, "hook", 4, 42)` and `build_motif("A", True, "hook", 4, 99)` are called
|
||||
- THEN the returned note lists differ
|
||||
|
||||
### Requirement: Call-and-Response Structure (R3)
|
||||
|
||||
`build_call_response(motif, bars, key_root, key_minor, seed)` MUST generate two halves: **call** (motif + variation, ending on V or VII degree) and **response** (motif, resolving to tonic i). Total length MUST equal `bars` parameter. SHALL repeat motif to fill section length.
|
||||
|
||||
#### Scenario: call ends on tension, response resolves
|
||||
|
||||
- GIVEN an Am hook motif, bars=8, seed=42
|
||||
- WHEN `build_call_response(motif, 8, "A", True, 42)` is called
|
||||
- THEN the last note of the first 4 bars has pitch in {E, G} (V or VII of Am)
|
||||
- AND the last note of the final bar (bar 8) has pitch in {A} (tonic)
|
||||
|
||||
#### Scenario: fills section with motif repetition
|
||||
|
||||
- GIVEN a 2-bar motif and bars=8
|
||||
- WHEN `build_call_response(motif, 8, "A", True, 42)` is called
|
||||
- THEN returns notes spanning 8 bars total
|
||||
- AND motif content repeats at least 2 times within the 8 bars
|
||||
|
||||
### Requirement: Chord-Aware Notes (R4)
|
||||
|
||||
Note selection on strong beats (quarter note positions 0, 4, 8, 12 per bar in 16th-note grid) MUST favor chord tones from `CHORD_PROGRESSION`. Weak beats (all other positions) MAY use any scale degree.
|
||||
|
||||
#### Scenario: strong beats favor chord tones
|
||||
|
||||
- GIVEN key Am (CHORD_PROGRESSION = i-VI-III-VII), style "hook", bars=8
|
||||
- WHEN a motif is generated
|
||||
- THEN ≥70% of notes starting on quarter-beat boundaries belong to active chord tones
|
||||
|
||||
### Requirement: Motif Variation (R5)
|
||||
|
||||
`apply_variation(motif, shift_beats, transpose_semitones)` SHOULD produce a recognizable variant of the input motif. `shift_beats` offsets all start times within the loop. `transpose_semitones` shifts pitches within the scale. MUST return `list[MidiNote]`.
|
||||
|
||||
#### Scenario: rhythmic shift preserves note count and structure
|
||||
|
||||
- GIVEN a 4-bar hook motif
|
||||
- WHEN `apply_variation(motif, shift_beats=0.25)` is called
|
||||
- THEN note count equals original
|
||||
- AND all note durations equal original
|
||||
- AND inter-onset intervals are preserved
|
||||
|
||||
#### Scenario: transpose within scale preserves motif contour
|
||||
|
||||
- GIVEN a 4-bar hook motif in Am
|
||||
- WHEN `apply_variation(motif, transpose_semitones=3)` is called
|
||||
- THEN all pitches are offset by ±3 semitones (within pentatonic scale)
|
||||
|
||||
### Requirement: build_lead_track() Delegation (R6)
|
||||
|
||||
`build_lead_track()` in `compose.py` MUST delegate to `melody_engine.build_call_response()` instead of generating random pentatonic notes directly. MUST keep identical function signature. MUST pass existing tests after adjusting expected note counts.
|
||||
|
||||
#### Scenario: build_lead_track uses call-response structure
|
||||
|
||||
- GIVEN seed=42, key Am, sections containing "chorus" and "final"
|
||||
- WHEN `build_lead_track(sections, offsets, "A", True, 42)` is called
|
||||
- THEN returned TrackDef clips contain notes organized as call-response phrases
|
||||
- AND at least one clip has notes ending on tonic pitch
|
||||
35
.sdd/changes/hook-melody/tasks.md
Normal file
35
.sdd/changes/hook-melody/tasks.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Tasks: Hook-Based Reggaeton Melody
|
||||
|
||||
## Phase 1: Melody Engine Core
|
||||
|
||||
- [x] 1.1 Create `src/composer/melody_engine.py` with `build_motif(key_root, key_minor, style, bars, seed)` → `list[MidiNote]`
|
||||
- [x] 1.2 Implement "hook" style: arch contour, chord tones on strong beats, 4–8 notes
|
||||
- [x] 1.3 Implement "stabs" style: 16th-duration hits on dembow positions [1.0, 2.5, 3.0, 3.5] per bar
|
||||
- [x] 1.4 Implement "smooth" style: stepwise scalar eighth-note motion
|
||||
- [x] 1.5 Implement `apply_variation(motif, shift_beats, transpose_semitones)` → `list[MidiNote]`
|
||||
- [x] 1.6 Implement `build_call_response(motif, bars, key_root, key_minor, seed)` → `list[MidiNote]`
|
||||
- [x] 1.7 Wire internal helpers: `_resolve_chord_tones()`, `_resolve_tension_notes()`
|
||||
|
||||
## Phase 2: Integration
|
||||
|
||||
- [x] 2.1 Modify `build_lead_track()` in `scripts/compose.py` to delegate to `melody_engine.build_call_response()`
|
||||
- [x] 2.2 Pass seed through to melody engine calls
|
||||
- [x] 2.3 Keep `get_pentatonic()` and `CHORD_PROGRESSION` unchanged in compose.py
|
||||
|
||||
## Phase 3: Testing
|
||||
|
||||
- [x] 3.1 Create `tests/test_melody_engine.py` with `test_motif_deterministic` (same seed = same output)
|
||||
- [x] 3.2 Test `test_motif_different_seeds_different_output`
|
||||
- [x] 3.3 Test `test_invalid_style_raises_value_error`
|
||||
- [x] 3.4 Test `test_hook_chord_tones_on_strong_beats` (≥70% ratio)
|
||||
- [x] 3.5 Test `test_stabs_grid_alignment` (all notes on dembow positions)
|
||||
- [x] 3.6 Test `test_smooth_stepwise_motion` (consecutive ≤2 semitones)
|
||||
- [x] 3.7 Test `test_variation_preserves_note_count_structure`
|
||||
- [x] 3.8 Test `test_call_ends_on_tension_response_ends_on_tonic` (V/VII → i)
|
||||
- [x] 3.9 Test `test_call_response_fills_bars` (motif repeats to fill section)
|
||||
- [x] 3.10 Update `test_melody_uses_pentatonic` in `tests/test_compose_integration.py` for hook structure
|
||||
|
||||
## Phase 4: Validation
|
||||
|
||||
- [x] 4.1 Run full test suite: `pytest tests/ -x` — 247/248 pass (1 pre-existing failure, unrelated)
|
||||
- [ ] 4.2 Manual verification: generate .rpp with `--seed 42`, confirm lead clips contain repeating motif structure
|
||||
Reference in New Issue
Block a user