Files
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

5.7 KiB
Raw Permalink Blame History

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,         # 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)

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.