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