- 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.
5.7 KiB
5.7 KiB
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
# 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)
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.