- 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.
293 lines
11 KiB
Python
293 lines
11 KiB
Python
"""Tests for section builder — SectionDef, track builders, plugin helpers."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parents[1]))
|
|
|
|
import pytest
|
|
from src.core.schema import SectionDef, PluginDef
|
|
|
|
|
|
class TestSectionDef:
|
|
def test_section_def_instantiation(self):
|
|
section = SectionDef(name="chorus", bars=8, energy=0.9)
|
|
assert section.name == "chorus"
|
|
assert section.bars == 8
|
|
assert section.energy == 0.9
|
|
assert section.velocity_mult == 1.0
|
|
assert section.vol_mult == 1.0
|
|
|
|
def test_section_def_default_energy(self):
|
|
section = SectionDef(name="verse", bars=8)
|
|
assert section.energy == 0.5
|
|
assert section.velocity_mult == 1.0
|
|
assert section.vol_mult == 1.0
|
|
|
|
def test_section_def_custom_mults(self):
|
|
section = SectionDef(
|
|
name="intro", bars=4, energy=0.3,
|
|
velocity_mult=0.4, vol_mult=0.6,
|
|
)
|
|
assert section.velocity_mult == 0.4
|
|
assert section.vol_mult == 0.6
|
|
|
|
|
|
class TestBuildSectionStructureMultipliers:
|
|
"""Verify build_section_structure() populates velocity_mult and vol_mult."""
|
|
|
|
@staticmethod
|
|
def _get_sections():
|
|
from scripts.compose import build_section_structure
|
|
sections, _offsets = build_section_structure()
|
|
return {s.name: s for s in sections}
|
|
|
|
def test_intro_has_low_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["intro"].velocity_mult == 0.6
|
|
assert sections["intro"].vol_mult == 0.70
|
|
|
|
def test_verse_has_mid_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["verse"].velocity_mult == 0.7
|
|
assert sections["verse"].vol_mult == 0.85
|
|
|
|
def test_pre_chorus_has_high_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["pre-chorus"].velocity_mult == 0.85
|
|
assert sections["pre-chorus"].vol_mult == 0.95
|
|
|
|
def test_chorus_has_full_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["chorus"].velocity_mult == 1.0
|
|
assert sections["chorus"].vol_mult == 1.0
|
|
|
|
def test_verse2_same_as_verse(self):
|
|
sections = self._get_sections()
|
|
assert sections["verse2"].velocity_mult == 0.7
|
|
assert sections["verse2"].vol_mult == 0.85
|
|
|
|
def test_chorus2_same_as_chorus(self):
|
|
sections = self._get_sections()
|
|
assert sections["chorus2"].velocity_mult == 1.0
|
|
assert sections["chorus2"].vol_mult == 1.0
|
|
|
|
def test_bridge_has_low_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["bridge"].velocity_mult == 0.6
|
|
assert sections["bridge"].vol_mult == 0.75
|
|
|
|
def test_final_has_full_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["final"].velocity_mult == 1.0
|
|
assert sections["final"].vol_mult == 1.0
|
|
|
|
def test_outro_has_lowest_multipliers(self):
|
|
sections = self._get_sections()
|
|
assert sections["outro"].velocity_mult == 0.4
|
|
assert sections["outro"].vol_mult == 0.60
|
|
|
|
def test_all_sections_have_multipliers(self):
|
|
"""Every section name in SECTIONS has a corresponding entry in multipliers."""
|
|
sections = self._get_sections()
|
|
from scripts.compose import SECTIONS
|
|
expected_names = {name for name, _, _, _ in SECTIONS}
|
|
assert set(sections.keys()) == expected_names
|
|
|
|
|
|
class TestSectionActiveHelper:
|
|
"""Tests for _section_active() centralized activity helper."""
|
|
|
|
@staticmethod
|
|
def _get_activity():
|
|
from scripts.compose import TRACK_ACTIVITY
|
|
return TRACK_ACTIVITY
|
|
|
|
def test_intro_drumloop_active(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("intro", "drumloop", self._get_activity()) is True
|
|
|
|
def test_intro_bass_inactive(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("intro", "bass", self._get_activity()) is False
|
|
|
|
def test_intro_chords_inactive(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("intro", "chords", self._get_activity()) is False
|
|
|
|
def test_intro_lead_inactive(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("intro", "lead", self._get_activity()) is False
|
|
|
|
def test_chorus_all_active(self):
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
for role in ("drumloop", "perc", "bass", "chords", "lead", "clap", "pad"):
|
|
assert _section_active("chorus", role, activity) is True, f"chorus.{role} should be active"
|
|
|
|
def test_verse_only_drumloop_bass_chords_active(self):
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
assert _section_active("verse", "drumloop", activity) is True
|
|
assert _section_active("verse", "bass", activity) is True
|
|
assert _section_active("verse", "chords", activity) is True
|
|
assert _section_active("verse", "lead", activity) is False
|
|
assert _section_active("verse", "perc", activity) is False
|
|
assert _section_active("verse", "clap", activity) is False
|
|
|
|
def test_pre_chorus_section_name(self):
|
|
"""The section is named pre-chorus, not build."""
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
assert "pre-chorus" in activity, "pre-chorus must be a key in TRACK_ACTIVITY"
|
|
assert "build" not in activity, "build must NOT be a key in TRACK_ACTIVITY"
|
|
assert _section_active("pre-chorus", "bass", activity) is True
|
|
|
|
def test_unknown_section_returns_false(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("xyz", "bass", self._get_activity()) is False
|
|
|
|
def test_unknown_role_returns_false(self):
|
|
from scripts.compose import _section_active
|
|
assert _section_active("chorus", "banjo", self._get_activity()) is False
|
|
|
|
def test_outro_has_drumloop_and_pad(self):
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
assert _section_active("outro", "drumloop", activity) is True
|
|
assert _section_active("outro", "pad", activity) is True
|
|
assert _section_active("outro", "bass", activity) is False
|
|
|
|
def test_bridge_has_drumloop_pad_lead(self):
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
assert _section_active("bridge", "drumloop", activity) is True
|
|
assert _section_active("bridge", "pad", activity) is True
|
|
assert _section_active("bridge", "lead", activity) is True
|
|
assert _section_active("bridge", "bass", activity) is False
|
|
assert _section_active("bridge", "chords", activity) is False
|
|
|
|
def test_final_has_perc_bass_chords_lead_clap_pad(self):
|
|
from scripts.compose import _section_active
|
|
activity = self._get_activity()
|
|
assert _section_active("final", "drumloop", activity) is True
|
|
assert _section_active("final", "perc", activity) is True
|
|
assert _section_active("final", "bass", activity) is True
|
|
assert _section_active("final", "chords", activity) is True
|
|
assert _section_active("final", "lead", activity) is True
|
|
assert _section_active("final", "clap", activity) is True
|
|
assert _section_active("final", "pad", activity) is True
|
|
|
|
|
|
class TestPluginRegistry:
|
|
def test_plugins_in_registry(self):
|
|
from src.reaper_builder import PLUGIN_REGISTRY
|
|
assert "Decapitator" in PLUGIN_REGISTRY
|
|
assert "EchoBoy" in PLUGIN_REGISTRY
|
|
assert "Serum_2" in PLUGIN_REGISTRY
|
|
assert "Omnisphere" in PLUGIN_REGISTRY
|
|
assert "Pro-Q_3" in PLUGIN_REGISTRY
|
|
assert "Pro-C_2" in PLUGIN_REGISTRY
|
|
assert "Pro-L_2" in PLUGIN_REGISTRY
|
|
assert "FabFilter_Pro-R_2" in PLUGIN_REGISTRY
|
|
assert "ValhallaDelay" in PLUGIN_REGISTRY
|
|
assert "PhaseMistress" in PLUGIN_REGISTRY
|
|
assert "Tremolator" in PLUGIN_REGISTRY
|
|
assert "Radiator" in PLUGIN_REGISTRY
|
|
assert "Gullfoss_Master" in PLUGIN_REGISTRY
|
|
assert "VC_76" in PLUGIN_REGISTRY
|
|
|
|
|
|
class TestMakePlugin:
|
|
def test_make_plugin_known_key(self):
|
|
from scripts.compose import make_plugin
|
|
p = make_plugin("Decapitator", 0)
|
|
assert p.name == "Decapitator"
|
|
assert p.index == 0
|
|
|
|
def test_make_plugin_unknown_key(self):
|
|
from scripts.compose import make_plugin
|
|
p = make_plugin("NonExistent", 2)
|
|
assert p.name == "NonExistent"
|
|
assert p.index == 2
|
|
|
|
|
|
class TestBuildFxChain:
|
|
def test_build_fx_chain_returns_list(self):
|
|
from scripts.compose import build_fx_chain
|
|
assert build_fx_chain() == []
|
|
|
|
def test_build_fx_chain_with_args(self):
|
|
from scripts.compose import build_fx_chain
|
|
assert build_fx_chain("drums", {}, []) == []
|
|
|
|
|
|
class TestCreateReturnTracks:
|
|
def test_create_return_tracks_returns_two(self):
|
|
from scripts.compose import create_return_tracks
|
|
tracks = create_return_tracks()
|
|
assert len(tracks) == 2
|
|
assert tracks[0].name == "Reverb"
|
|
assert tracks[1].name == "Delay"
|
|
|
|
def test_reverb_track_has_pro_r2(self):
|
|
from scripts.compose import create_return_tracks
|
|
tracks = create_return_tracks()
|
|
reverb = tracks[0]
|
|
assert len(reverb.plugins) == 1
|
|
assert "Pro-R" in reverb.plugins[0].name
|
|
|
|
def test_delay_track_has_valhalla(self):
|
|
from scripts.compose import create_return_tracks
|
|
tracks = create_return_tracks()
|
|
delay = tracks[1]
|
|
assert len(delay.plugins) == 1
|
|
assert "Valhalla" in delay.plugins[0].name
|
|
|
|
def test_return_tracks_have_volume_0_7(self):
|
|
from scripts.compose import create_return_tracks
|
|
tracks = create_return_tracks()
|
|
for t in tracks:
|
|
assert t.volume == 0.7
|
|
|
|
|
|
class TestMusicTheory:
|
|
def test_parse_key_minor(self):
|
|
from scripts.compose import parse_key
|
|
root, minor = parse_key("Am")
|
|
assert root == "A"
|
|
assert minor is True
|
|
|
|
def test_parse_key_major(self):
|
|
from scripts.compose import parse_key
|
|
root, minor = parse_key("C")
|
|
assert root == "C"
|
|
assert minor is False
|
|
|
|
def test_root_to_midi(self):
|
|
from scripts.compose import NOTE_TO_MIDI
|
|
assert NOTE_TO_MIDI["A"] + (4 + 1) * 12 == 69
|
|
assert NOTE_TO_MIDI["C"] + (4 + 1) * 12 == 60
|
|
|
|
def test_build_chord_major(self):
|
|
from scripts.compose import build_chord
|
|
chord = build_chord(60, "major")
|
|
assert chord == [60, 64, 67]
|
|
|
|
def test_build_chord_minor(self):
|
|
from scripts.compose import build_chord
|
|
chord = build_chord(60, "minor")
|
|
assert chord == [60, 63, 67]
|
|
|
|
def test_pentatonic_minor(self):
|
|
from scripts.compose import get_pentatonic
|
|
notes = get_pentatonic("A", True, 4)
|
|
assert notes[0] == 69 # A4
|
|
assert len(notes) == 5
|
|
|
|
def test_pentatonic_major(self):
|
|
from scripts.compose import get_pentatonic
|
|
notes = get_pentatonic("C", False, 4)
|
|
assert notes[0] == 60 # C4
|
|
assert len(notes) == 5
|