# Chords Specification ## Purpose Chord progression generation with voice leading, inversion selection, and emotion-aware patterns for reggaeton. Deterministic and testable. ## Requirements | # | Requirement | Strength | Key Scenarios | |---|------------|----------|---------------| | R1 | `ChordEngine(key, seed)` MUST produce identical progressions for same seed+key | MUST | Same seed → same notes; different seed → different notes | | R2 | Voice leading MUST minimize semitone movement between consecutive chords, capping at 4 semitones per voice | MUST | 2-chord transition ≤4 semitones per voice; 8-bar progression all leaps ≤4 | | R3 | SHALL support 3 inversion modes: `root`, `first`, `second` | SHALL | Root: lowest note = root; First: lowest = third; Second: lowest = fifth | | R4 | MUST support 4 emotion modes: `romantic`, `dark`, `club`, `classic` | MUST | Each emotion yields distinct degree sequence; unknown emotion → `classic` fallback | | R5 | `progression(bars, emotion, beats_per_chord, inversion)` SHALL return `list[list[int]]` — ordered chord voicings as MIDI note lists | SHALL | 8 bars @ 4 BpC → 8 chords; empty list for 0 bars | | R6 | Reggaeton progressions SHOULD use genre-appropriate cadences per emotion | SHOULD | Romantic: i-VI-III-VII; Dark: i-iv-V-v; Club: i-VII-VI-V; Classic: i-VI-III-VII | | R7 | `build_chords_track()` SHALL delegate to `ChordEngine` instead of hardcoded progression | SHALL | CLI `--emotion dark` → dark progression in output | ### Scenario: Deterministic reproducibility - GIVEN `ChordEngine("Am", seed=42)` - WHEN `progression(bars=8)` called twice - THEN both calls return identical `list[list[int]]` ### Scenario: Voice leading within bounds - GIVEN any 2 consecutive chords from a progression - WHEN computing voice leading - THEN no voice moves more than 4 semitones from its previous position ### Scenario: Emotion modes diverge - GIVEN `ChordEngine("Am", seed=0)` with emotions `romantic`, `dark`, `club`, `classic` - WHEN `progression(8)` called per emotion - THEN all 4 output sequences differ ### Scenario: Invalid emotion falls back - GIVEN `ChordEngine("Am")` with emotion `"angry"` (unknown) - WHEN `progression(8)` called - THEN defaults to `classic` progression, no error raised ### Scenario: Integration with compose.py - GIVEN `python scripts/compose.py --key Am --emotion dark --output test.rpp` - WHEN build completes - THEN Chords track contains voicings matching dark-emotion progression