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:
@@ -1,12 +1,60 @@
|
||||
"""Tests for src/core/schema.py — SongDefinition, TrackDef, ClipDef, MidiNote."""
|
||||
"""Tests for src/core/schema.py — SongDefinition, TrackDef, ClipDef, MidiNote, CCEvent."""
|
||||
|
||||
import dataclasses
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestCCEvent:
|
||||
"""Test CCEvent dataclass (Phase 1: schema)."""
|
||||
|
||||
def test_ccevent_round_trip(self):
|
||||
"""CCEvent round-trips correctly through dataclass construction."""
|
||||
evt = CCEvent(controller=11, time=0.5, value=50)
|
||||
assert evt.controller == 11
|
||||
assert evt.time == 0.5
|
||||
assert evt.value == 50
|
||||
|
||||
def test_ccevent_defaults(self):
|
||||
"""CCEvent has no default fields — all are required."""
|
||||
from dataclasses import fields as dc_fields
|
||||
field_names = [f.name for f in dc_fields(CCEvent)]
|
||||
no_default = []
|
||||
for f in dc_fields(CCEvent):
|
||||
if f.default is f.default_factory is dataclasses.MISSING:
|
||||
no_default.append(f.name)
|
||||
assert "controller" in no_default
|
||||
assert "time" in no_default
|
||||
assert "value" in no_default
|
||||
|
||||
def test_clipdef_with_midi_cc(self):
|
||||
"""ClipDef with midi_cc field."""
|
||||
cc = [CCEvent(11, 0.0, 50), CCEvent(11, 0.18, 127)]
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Test", midi_cc=cc)
|
||||
assert len(clip.midi_cc) == 2
|
||||
assert clip.midi_cc[0].controller == 11
|
||||
assert clip.midi_cc[0].time == 0.0
|
||||
assert clip.midi_cc[0].value == 50
|
||||
|
||||
def test_clipdef_midi_cc_default_is_empty(self):
|
||||
"""ClipDef.midi_cc defaults to empty list."""
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Test")
|
||||
assert clip.midi_cc == []
|
||||
|
||||
def test_song_validate_empty_midi_cc(self):
|
||||
"""Song.validate() passes with empty midi_cc (no regression)."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Test", midi_notes=[note], midi_cc=[])
|
||||
track = TrackDef(name="Test", clips=[clip])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
errors = song.validate()
|
||||
assert errors == []
|
||||
|
||||
|
||||
class TestSongDefinitionInstantiation:
|
||||
@@ -135,3 +183,30 @@ class TestMidiNote:
|
||||
"""MidiNote does NOT clamp — accepts any int (caller's responsibility)."""
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=200)
|
||||
assert note.velocity == 200
|
||||
|
||||
|
||||
class TestClipDefVolMult:
|
||||
"""Test ClipDef.vol_mult default and behavior."""
|
||||
|
||||
def test_vol_mult_default_is_one(self):
|
||||
"""ClipDef.vol_mult defaults to 1.0."""
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Test")
|
||||
assert clip.vol_mult == 1.0
|
||||
|
||||
def test_vol_mult_custom_value(self):
|
||||
"""ClipDef.vol_mult accepts custom value."""
|
||||
clip = ClipDef(position=0.0, length=16.0, name="Test", vol_mult=0.7)
|
||||
assert clip.vol_mult == 0.7
|
||||
|
||||
def test_audio_clip_vol_mult_default_is_one(self):
|
||||
"""Audio clip with default vol_mult=1.0 has no D_VOL side effect."""
|
||||
clip = ClipDef(position=0.0, length=16.0, audio_path="test.wav", vol_mult=1.0)
|
||||
assert clip.is_audio
|
||||
assert clip.vol_mult == 1.0
|
||||
|
||||
def test_midi_clip_vol_mult_default_is_one(self):
|
||||
"""MIDI clip with default vol_mult=1.0 has no velocity scaling."""
|
||||
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
|
||||
clip = ClipDef(position=0.0, length=16.0, midi_notes=[note], vol_mult=1.0)
|
||||
assert clip.is_midi
|
||||
assert clip.vol_mult == 1.0
|
||||
|
||||
Reference in New Issue
Block a user