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:
renato97
2026-05-03 23:54:29 -03:00
parent 48bc271afc
commit 014e636889
51 changed files with 11394 additions and 113 deletions

View File

@@ -7,7 +7,7 @@ from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parents[1]))
import pytest
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, CCEvent
from src.reaper_builder import RPPBuilder
from src.composer.drum_analyzer import DrumLoopAnalysis, Transient, BeatGrid
@@ -152,7 +152,7 @@ class TestDrumloopFirstTracks:
def test_all_tracks_created(self, tmp_path):
output = _mock_main(tmp_path)
content = output.read_text(encoding="utf-8")
for name in ("Drumloop", "Bass", "Chords", "Lead", "Clap", "Pad", "Reverb", "Delay"):
for name in ("Drumloop", "808 Bass", "Chords", "Lead", "Clap", "Pad", "Reverb", "Delay"):
assert name in content, f"Expected track '{name}' in output"
def test_clap_on_dembow_beats(self, tmp_path):
@@ -197,24 +197,48 @@ class TestDrumloopFirstTracks:
for s in starts:
assert s % 4.0 == 0.0, f"Chord change at beat {s} — should be on downbeat"
def test_melody_uses_pentatonic(self):
from scripts.compose import build_melody_track
def test_melody_uses_hook_structure(self):
"""Lead melody should use hook-based call-response from melody_engine."""
from scripts.compose import build_melody_track, get_pentatonic
from src.core.schema import SectionDef
from src.composer.melody_engine import _resolve_chord_tones
sections = [SectionDef(name="chorus", bars=4, energy=1.0)]
offsets = [0.0]
track = build_melody_track(sections, offsets, "A", True, seed=42)
assert len(track.clips) > 0, "Melody should have clips"
pitches = {n.pitch for n in track.clips[0].midi_notes}
notes = track.clips[0].midi_notes
pitches = {n.pitch for n in notes}
assert len(pitches) > 1, "Melody should use multiple notes"
# Verify chord-tone emphasis: quarter-position notes should favor chord tones
penta = get_pentatonic("A", True, 4) + get_pentatonic("A", True, 5)
chord_tones = _resolve_chord_tones("A", True, 0, 4)
quarter_notes = [n for n in notes if abs(n.start % 1.0) < 0.001]
if quarter_notes:
chord_count = sum(
1 for n in quarter_notes
if any(abs(n.pitch - ct) % 12 == 0 for ct in chord_tones)
)
ratio = chord_count / len(quarter_notes)
assert ratio >= 0.5, (
f"Hook chord tone ratio {ratio:.1%} below 50%"
)
# Verify notes span the section length
max_end = max(n.start + n.duration for n in notes)
assert max_end <= 16.0 + 0.001, "Notes should fit within 4 bars"
def test_master_chain_present(self, tmp_path):
output = _mock_main(tmp_path)
content = output.read_text(encoding="utf-8")
assert "Pro-Q" in content, "Expected Pro-Q 3 in master chain"
assert "Pro-C" in content, "Expected Pro-C 2 in master chain"
assert "Pro-L" in content, "Expected Pro-L 2 in master chain"
# After calibration, master chain uses Ozone 12 triplet (with spaces in RPP)
assert "Ozone 12" in content or "Pro-Q" in content, (
"Expected Ozone 12 or Pro-Q in master chain"
)
def test_sends_wired(self, tmp_path):
output = _mock_main(tmp_path)
@@ -243,3 +267,192 @@ class TestBackwardCompat:
assert tracks[1].name == "Delay"
assert len(tracks[0].plugins) > 0
assert len(tracks[1].plugins) > 0
class TestSectionEnergy:
"""Integration tests for section energy curve — sparse vs dense sections."""
def test_section_rename_pre_chorus_not_build(self):
"""SECTIONS uses 'pre-chorus' not 'build'."""
from scripts.compose import SECTIONS
names = {name for name, _, _, _ in SECTIONS}
assert "pre-chorus" in names, "Expected 'pre-chorus' section"
assert "build" not in names, "'build' must be renamed to 'pre-chorus'"
def test_intro_has_no_bass(self):
"""Intro section should NOT have bass (sparse)."""
from scripts.compose import build_bass_track, build_section_structure
sections, offsets = build_section_structure()
intro_section = sections[0]
assert intro_section.name == "intro"
track = build_bass_track(sections, offsets, "A", True)
# Find clips whose position matches the intro offset
intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
assert len(intro_positions) == 0, "Intro should have no bass clips"
def test_intro_has_no_chords(self):
"""Intro section should NOT have chords (sparse)."""
from scripts.compose import build_chords_track, build_section_structure
sections, offsets = build_section_structure()
track = build_chords_track(sections, offsets, "A", True)
intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
assert len(intro_positions) == 0, "Intro should have no chord clips"
def test_intro_has_no_lead(self):
"""Intro section should NOT have lead (sparse)."""
from scripts.compose import build_lead_track, build_section_structure
sections, offsets = build_section_structure()
track = build_lead_track(sections, offsets, "A", True, seed=42)
intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
assert len(intro_positions) == 0, "Intro should have no lead clips"
def test_chorus_has_bass_chords_lead(self):
"""Chorus section should have bass, chords, and lead (full band)."""
from scripts.compose import (
build_bass_track, build_chords_track, build_lead_track,
build_section_structure,
)
sections, offsets = build_section_structure()
# Find the chorus section index (first "chorus")
chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
chorus_offset = offsets[chorus_idx] * 4.0
bass = build_bass_track(sections, offsets, "A", True)
chords = build_chords_track(sections, offsets, "A", True)
lead = build_lead_track(sections, offsets, "A", True, seed=42)
assert any(c.position == chorus_offset for c in bass.clips), "Chorus should have bass"
assert any(c.position == chorus_offset for c in chords.clips), "Chorus should have chords"
assert any(c.position == chorus_offset for c in lead.clips), "Chorus should have lead"
def test_chorus_clips_have_vol_mult(self):
"""Clips in chorus sections should have vol_mult set from section."""
from scripts.compose import build_drumloop_track, build_bass_track, build_section_structure
sections, offsets = build_section_structure()
chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
chorus_offset = offsets[chorus_idx] * 4.0
drumloop_track = build_drumloop_track(sections, offsets, seed=0)
bass_track = build_bass_track(sections, offsets, "A", True)
# Audio clips get vol_mult
dl_clips = [c for c in drumloop_track.clips if c.position == chorus_offset]
if dl_clips:
assert dl_clips[0].vol_mult == 1.0, "Chorus drumloop vol_mult should be 1.0"
# MIDI clips get vol_mult
bass_clips = [c for c in bass_track.clips if c.position == chorus_offset]
if bass_clips:
assert bass_clips[0].vol_mult == 1.0, "Chorus bass vol_mult should be 1.0"
def test_velocity_scaled_in_intro_vs_chorus(self):
"""Verse has lower velocity notes than chorus (velocity_mult 0.7 vs 1.0)."""
from scripts.compose import build_bass_track, build_section_structure
sections, offsets = build_section_structure()
verse_idx = next(i for i, s in enumerate(sections) if s.name == "verse")
verse_offset = offsets[verse_idx] * 4.0
chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
chorus_offset = offsets[chorus_idx] * 4.0
track = build_bass_track(sections, offsets, "A", True)
verse_clip = next(c for c in track.clips if c.position == verse_offset)
chorus_clip = next(c for c in track.clips if c.position == chorus_offset)
verse_vel = verse_clip.midi_notes[0].velocity if verse_clip.midi_notes else 0
chorus_vel = chorus_clip.midi_notes[0].velocity if chorus_clip.midi_notes else 0
assert verse_vel < chorus_vel, \
f"Verse velocity ({verse_vel}) should be less than chorus ({chorus_vel})"
def test_drumloop_assignments_no_break_key(self):
"""DRUMLOOP_ASSIGNMENTS has no 'break' key — replaced by activity matrix."""
from scripts.compose import DRUMLOOP_ASSIGNMENTS
assert "break" not in DRUMLOOP_ASSIGNMENTS
assert "pre-chorus" in DRUMLOOP_ASSIGNMENTS
class TestSidechainBassCC:
"""Integration tests for CC11 sidechain ducking on 808 bass."""
def test_bass_track_populates_midi_cc_with_kick_cache(self):
"""build_bass_track populates midi_cc when kick cache present."""
from scripts.compose import build_bass_track
from src.core.schema import SectionDef
sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
offsets = [0.0]
kick_cache = {"fake_drumloop.wav": [1.0, 3.0, 5.0, 7.0]}
track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
assert len(track.clips) > 0, "Bass should have clips"
verse_clip = track.clips[0]
# With 4 kicks in range, each generates 3 CC events
assert len(verse_clip.midi_cc) == 12, f"Expected 12 CC events (4 kicks × 3), got {len(verse_clip.midi_cc)}"
# Check first kick's duck events
cc_times = [(cc.time, cc.value) for cc in verse_clip.midi_cc[:3]]
assert (1.0, 50) in cc_times, f"Expected CC dip at 1.0, got {cc_times}"
assert (1.02, 50) in cc_times, f"Expected CC hold at 1.02, got {cc_times}"
assert (1.18, 127) in cc_times, f"Expected CC release at 1.18, got {cc_times}"
def test_bass_track_no_kick_cache_empty_cc(self):
"""build_bass_track produces empty midi_cc when no kick cache provided."""
from scripts.compose import build_bass_track
from src.core.schema import SectionDef
sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
offsets = [0.0]
track = build_bass_track(sections, offsets, "A", True)
assert len(track.clips) > 0
verse_clip = track.clips[0]
assert verse_clip.midi_cc == [], "midi_cc should be empty without kick cache"
def test_bass_track_no_kicks_in_range(self):
"""build_bass_track produces empty midi_cc when no kicks in clip range."""
from scripts.compose import build_bass_track
from src.core.schema import SectionDef
sections = [SectionDef(name="verse", bars=2, energy=0.5, velocity_mult=0.7)]
offsets = [0.0]
# Kicks at beats far outside the clip range (0-8 beats)
kick_cache = {"fake_drumloop.wav": [100.0, 200.0]}
track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
verse_clip = track.clips[0]
assert verse_clip.midi_cc == [], "midi_cc should be empty when kicks are outside clip range"
def test_bass_track_preserves_notes_with_cc(self):
"""build_bass_track preserves existing note generation when CC added."""
from scripts.compose import build_bass_track
from src.core.schema import SectionDef
sections = [SectionDef(name="verse", bars=4, energy=0.5, velocity_mult=0.7)]
offsets = [0.0]
kick_cache = {"fake_drumloop.wav": [2.0]}
track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
verse_clip = track.clips[0]
# Should still have bass notes (i - iv pattern for 4 bars)
assert len(verse_clip.midi_notes) > 0, "Bass notes should still be generated"
assert len(verse_clip.midi_cc) == 3, "Should have 3 CC events for 1 kick in range"
def test_bass_track_kicks_relative_to_clip(self):
"""build_bass_track produces CC times relative to clip start, not absolute."""
from scripts.compose import build_bass_track
from src.core.schema import SectionDef
# Section at offset 2 bars (8 beats)
sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
offsets = [2.0] # starts at bar 2 = beat 8
# Kick at absolute beat 9.0 → relative beat 1.0
kick_cache = {"fake_drumloop.wav": [9.0]}
track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
verse_clip = track.clips[0]
assert len(verse_clip.midi_cc) == 3
# CC times should be relative to clip start (9.0 - 8.0 = 1.0)
first_cc_time = verse_clip.midi_cc[0].time
assert first_cc_time == 1.0, f"CC time should be 1.0 (relative), got {first_cc_time}"