feat: VST3 preset data, project metadata, plugin registry fixes, and token cleanup
- Add VST3_PRESETS dict with base64 preset data for all 10 plugins (required by REAPER to load VST3) - Fix VST3 registry: correct display names, filenames, and uniqueid GUIDs - Add ~50 lines of REAPER project metadata (PANLAW, SAMPLERATE, METRONOME, etc.) - Add 25 track attributes (PEAKCOL, BEAT, AUTOMODE, etc.) and FX chain metadata - Remove unrecognized tokens (RENDER_CFG, PROJBAY, WAK) that caused REAPER warnings - Update compose.py with section-based arrangement and registry key names - Add SectionDef to schema - 72 tests passing
This commit is contained in:
@@ -25,50 +25,33 @@ def compose_via_builder(
|
||||
|
||||
This lets us test the compose logic without hitting the filesystem for samples.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path as P
|
||||
|
||||
_ROOT = P(__file__).parent.parent
|
||||
|
||||
from src.composer.rhythm import get_notes
|
||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||
|
||||
genre_bar_map = {"reggaeton": 64, "trap": 32, "house": 64, "drill": 32}
|
||||
bar_count = genre_bar_map.get(genre.lower(), 48)
|
||||
genre_path = _ROOT / "knowledge" / "genres" / f"{genre.lower()}_2009.json"
|
||||
with open(genre_path, "r", encoding="utf-8") as f:
|
||||
genre_config = json.load(f)
|
||||
|
||||
# Drum tracks
|
||||
drum_tracks = []
|
||||
for role, generator_name in [
|
||||
("kick", "kick_main_notes"),
|
||||
("snare", "snare_verse_notes"),
|
||||
("hihat", "hihat_16th_notes"),
|
||||
("perc", "perc_combo_notes"),
|
||||
]:
|
||||
note_dict = get_notes(generator_name, bar_count)
|
||||
midi_notes = rhythm_to_midi(note_dict)
|
||||
clip = ClipDef(
|
||||
position=0.0,
|
||||
length=bar_count * 4.0,
|
||||
name=f"{role.capitalize()} Pattern",
|
||||
midi_notes=midi_notes,
|
||||
)
|
||||
drum_tracks.append(TrackDef(name=role.capitalize(), clips=[clip]))
|
||||
from scripts.compose import (
|
||||
build_section_tracks, create_return_tracks, EFFECT_ALIASES,
|
||||
build_fx_chain, build_sampler_plugin,
|
||||
)
|
||||
from src.selector import SampleSelector
|
||||
|
||||
# Melodic tracks (no selector — audio_path stays None)
|
||||
for role, generator_fn in [
|
||||
("bass", bass_tresillo),
|
||||
("lead", lead_hook),
|
||||
("chords", chords_block),
|
||||
("pad", pad_sustain),
|
||||
]:
|
||||
note_list = generator_fn(key=key, bars=bar_count)
|
||||
midi_notes = melodic_to_midi(note_list)
|
||||
clip = ClipDef(
|
||||
position=0.0,
|
||||
length=bar_count * 4.0,
|
||||
name=f"{role.capitalize()} MIDI",
|
||||
midi_notes=midi_notes,
|
||||
)
|
||||
drum_tracks.append(TrackDef(name=role.capitalize(), clips=[clip]))
|
||||
index_path = _ROOT / "data" / "sample_index.json"
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
tracks, sections = build_section_tracks(genre_config, selector, key, bpm)
|
||||
return_tracks = create_return_tracks()
|
||||
|
||||
meta = SongMeta(bpm=bpm, key=key, title=f"{genre.capitalize()} Track")
|
||||
return SongDefinition(meta=meta, tracks=drum_tracks)
|
||||
return SongDefinition(meta=meta, tracks=tracks + return_tracks, sections=sections)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -105,8 +88,8 @@ class TestComposeRppOutput:
|
||||
|
||||
assert output.exists(), f"Expected {output} to exist"
|
||||
|
||||
def test_compose_rpp_has_min_4_tracks(self, tmp_path):
|
||||
"""The .rpp output contains at least 4 <TRACK blocks."""
|
||||
def test_compose_rpp_has_min_6_tracks(self, tmp_path):
|
||||
"""The .rpp output contains at least 6 <TRACK blocks (roles + 2 returns)."""
|
||||
output = tmp_path / "track.rpp"
|
||||
|
||||
with patch("scripts.compose.SampleSelector") as mock_selector_cls:
|
||||
@@ -131,7 +114,35 @@ class TestComposeRppOutput:
|
||||
|
||||
content = output.read_text(encoding="utf-8")
|
||||
track_count = content.count("<TRACK")
|
||||
assert track_count >= 4, f"Expected >= 4 tracks, got {track_count}"
|
||||
# 6 roles + 2 return tracks = 8 minimum
|
||||
assert track_count >= 6, f"Expected >= 6 tracks, got {track_count}"
|
||||
|
||||
def test_compose_has_fxchain(self, tmp_path):
|
||||
"""The .rpp output contains FXCHAIN elements."""
|
||||
output = tmp_path / "track.rpp"
|
||||
|
||||
with patch("scripts.compose.SampleSelector") as mock_selector_cls:
|
||||
mock_selector = MagicMock()
|
||||
mock_selector.select_one.return_value = None
|
||||
mock_selector_cls.return_value = mock_selector
|
||||
|
||||
from scripts.compose import main
|
||||
import sys
|
||||
original_argv = sys.argv
|
||||
try:
|
||||
sys.argv = [
|
||||
"compose",
|
||||
"--genre", "reggaeton",
|
||||
"--bpm", "95",
|
||||
"--key", "Am",
|
||||
"--output", str(output),
|
||||
]
|
||||
main()
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
content = output.read_text(encoding="utf-8")
|
||||
assert "FXCHAIN" in content, "Expected FXCHAIN in output"
|
||||
|
||||
def test_compose_invalid_bpm_raises(self):
|
||||
"""main() with bpm=0 raises ValueError."""
|
||||
@@ -168,3 +179,45 @@ class TestComposeRppOutput:
|
||||
main()
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
class TestSectionBuilderIntegration:
|
||||
"""Test section builder integration with SongDefinition."""
|
||||
|
||||
def test_build_section_tracks_returns_tracks_and_sections(self):
|
||||
"""build_section_tracks returns (tracks, sections) tuple."""
|
||||
import json
|
||||
from pathlib import Path as P
|
||||
|
||||
_ROOT = P(__file__).parent.parent
|
||||
from scripts.compose import build_section_tracks
|
||||
from src.selector import SampleSelector
|
||||
|
||||
genre_path = _ROOT / "knowledge" / "genres" / "reggaeton_2009.json"
|
||||
with open(genre_path, "r", encoding="utf-8") as f:
|
||||
genre_config = json.load(f)
|
||||
|
||||
index_path = _ROOT / "data" / "sample_index.json"
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
tracks, sections = build_section_tracks(genre_config, selector, "Am", 95.0)
|
||||
|
||||
assert len(tracks) > 0, "Expected at least one track"
|
||||
assert len(sections) > 0, "Expected at least one section"
|
||||
# Sections should have names
|
||||
for sec in sections:
|
||||
assert sec.name in ["intro", "verse", "chorus", "outro",
|
||||
"verse2", "chorus2", "bridge", "chorus3"]
|
||||
|
||||
def test_song_definition_has_sections_field(self):
|
||||
"""SongDefinition has a sections field."""
|
||||
from src.core.schema import SongDefinition, SongMeta, SectionDef
|
||||
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(
|
||||
meta=meta,
|
||||
tracks=[],
|
||||
sections=[SectionDef(name="intro", bars=4, energy=0.3)],
|
||||
)
|
||||
assert len(song.sections) == 1
|
||||
assert song.sections[0].name == "intro"
|
||||
@@ -7,7 +7,7 @@ sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
|
||||
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, PluginDef
|
||||
from src.reaper_builder import RPPBuilder
|
||||
|
||||
|
||||
@@ -174,3 +174,272 @@ class TestRPPBuilderMasterTrack:
|
||||
assert "NAME master" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestRPPProjectFormat:
|
||||
"""Test output matches the ground truth format from output/test_vst3.rpp."""
|
||||
|
||||
def test_header_version_765_win64(self):
|
||||
"""REAPER_PROJECT line has version 7.65/win64 (not unquoted 6.0)."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="Test")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
first_line = content.split('\n', 1)[0]
|
||||
# Version must be 7.65/win64, not 6.0
|
||||
assert "7.65/win64" in first_line
|
||||
# Must NOT contain the old 6.0 version
|
||||
assert "6.0" not in first_line
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_peakgain_and_panlaw_present(self):
|
||||
"""Output contains PEAKGAIN and PANLAW lines from ground truth."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "PEAKGAIN 1" in content
|
||||
assert "PANLAW 1" in content
|
||||
assert "SAMPLERATE 44100" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_track_has_all_default_attributes(self):
|
||||
"""TRACK element contains all 25 default attributes from ground truth."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
track = TrackDef(name="Test Track", clips=[])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Key attributes that uniquely identify the ground truth format
|
||||
assert "PEAKCOL 16576" in content
|
||||
assert "BEAT -1" in content
|
||||
assert "AUTOMODE 0" in content
|
||||
assert "NCHAN 2" in content
|
||||
assert "FX 1" in content
|
||||
assert "TRACKID {" in content
|
||||
assert "VU 64" in content
|
||||
assert "INQ 0 0 0 0.5" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_fxchain_has_required_structure(self):
|
||||
"""FXCHAIN block has WNDRECT, SHOW, BYPASS, FXID lines."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
plugin = PluginDef(name="Serum2", path="Serum2.vst3", index=0)
|
||||
track = TrackDef(name="Bass", clips=[], plugins=[plugin])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "WNDRECT 24 52 655 408" in content
|
||||
assert "SHOW 0" in content
|
||||
assert "DOCKED 0" in content
|
||||
assert "BYPASS 0 0 0" in content
|
||||
assert "FXID {" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_metronome_block_structure(self):
|
||||
"""METRONOME is a parent element with proper children, not flat attributes."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
assert "<METRONOME" in content
|
||||
assert "PATTERNSTR ABBB" in content
|
||||
assert "SAMPLES \"\" \"\" \"\" \"\"" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_master_track_has_fxchain(self):
|
||||
"""Master track has FXCHAIN block (MASTER_FX 1 requires it)."""
|
||||
meta = SongMeta(bpm=95, key="Am")
|
||||
song = SongDefinition(meta=meta, tracks=[])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Count FXCHAIN blocks - master + any user tracks
|
||||
fxchain_count = content.count("<FXCHAIN")
|
||||
assert fxchain_count >= 1, f"Expected at least 1 FXCHAIN, got {fxchain_count}"
|
||||
# Master track FXCHAIN has master-specific FXID
|
||||
assert "FXID {" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestVST3GUIDPresence:
|
||||
"""Test that VST3 plugins output with uniqueid{GUID} tokens."""
|
||||
|
||||
def test_vst3_plugin_output_contains_guid(self):
|
||||
"""VST3 element contains GUID from registry lookup."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="VST3 Test")
|
||||
plugin = PluginDef(name="Serum2", path="Serum2.vst3", index=0)
|
||||
track = TrackDef(name="Bass", clips=[], plugins=[plugin])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Must contain the GUID token from VST3_REGISTRY["Serum2"]
|
||||
assert "691258006{56534558667350736572756D20320000}" in content
|
||||
# Must also contain correct display name and filename
|
||||
assert "VST3: Serum 2 (Xfer Records)" in content
|
||||
assert "Serum2.vst3" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_fabfilter_proq3_contains_guid(self):
|
||||
"""FabFilter Pro-Q 3 outputs with correct GUID."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="VST3 Test")
|
||||
plugin = PluginDef(name="FabFilter Pro-Q 3", path="FabFilter Pro-Q 3.vst3", index=0)
|
||||
track = TrackDef(name="Lead", clips=[], plugins=[plugin])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Must contain the GUID token from VST3_REGISTRY["FabFilter Pro-Q 3"]
|
||||
assert "756089518{72C4DB717A4D459AB97E51745D84B39D}" in content
|
||||
assert "VST3: Pro-Q 3 (FabFilter)" in content
|
||||
assert "FabFilter Pro-Q 3.vst3" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestVST3PresetData:
|
||||
"""Test that VST3 plugins include base64 preset data inside VST blocks."""
|
||||
|
||||
def test_serum2_vst_contains_preset_data(self):
|
||||
"""Serum2 VST block contains base64 preset lines."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="VST3 Preset Test")
|
||||
plugin = PluginDef(name="Serum2", path="Serum2.vst3", index=0)
|
||||
track = TrackDef(name="Bass", clips=[], plugins=[plugin])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Serum2 preset starts with this magic line (first base64 line)
|
||||
assert "Z4R+ae5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAbQgAAAEAAAAAAAAA" in content
|
||||
# Last line of all presets is the same terminator
|
||||
assert "AFByb2dyYW0gMQAAAAAA" in content
|
||||
# A mid-preset line (line 2)
|
||||
assert "zQQAAAEAAABYZmVySnNvbgC5AAAAAAAAAHsiY29tcG9uZW50IjoicHJvY2Vzc29yIiwiaGFzaCI6IjgxZTEyMWYxNGI2Y2IyYjA2YzMzMjQzZDk1ZDIxYWIxIiwicHJv" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_fabfilter_proq3_vst_contains_preset_data(self):
|
||||
"""FabFilter Pro-Q 3 VST block contains base64 preset lines."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="VST3 Preset Test")
|
||||
plugin = PluginDef(name="FabFilter Pro-Q 3", path="FabFilter Pro-Q 3.vst3", index=0)
|
||||
track = TrackDef(name="Lead", clips=[], plugins=[plugin])
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
# Pro-Q 3 preset starts with this line
|
||||
assert "rgIRLe5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAAoGAAABAAAAAAAAAA==" in content
|
||||
assert "AFByb2dyYW0gMQAAAAAA" in content
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def test_all_registry_plugins_have_preset_data(self):
|
||||
"""All 10 VST3 plugins in VST3_REGISTRY have preset data."""
|
||||
meta = SongMeta(bpm=95, key="Am", title="VST3 Preset Test")
|
||||
# Use actual filenames from registry so _build_plugin recognizes them as VST3
|
||||
plugins = [
|
||||
PluginDef(name=name, path=entry[1], index=i)
|
||||
for i, (name, entry) in enumerate(RPPBuilder.VST3_REGISTRY.items())
|
||||
]
|
||||
track = TrackDef(name="Test", clips=[], plugins=plugins)
|
||||
song = SongDefinition(meta=meta, tracks=[track])
|
||||
builder = RPPBuilder(song)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rpp", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
tmp_path = f.name
|
||||
|
||||
try:
|
||||
builder.write(tmp_path)
|
||||
content = Path(tmp_path).read_text(encoding="utf-8")
|
||||
for name, preset_lines in RPPBuilder.VST3_PRESETS.items():
|
||||
assert len(preset_lines) > 0, f"{name} has no preset lines"
|
||||
# Check first preset line — most distinctive, no collision risk
|
||||
first_line = preset_lines[0]
|
||||
assert first_line in content, f"{name} preset line not found in output"
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
209
tests/test_section_builder.py
Normal file
209
tests/test_section_builder.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Tests for section builder — SectionDef, build_fx_chain, effect alias mapping."""
|
||||
|
||||
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:
|
||||
"""Test SectionDef dataclass."""
|
||||
|
||||
def test_section_def_instantiation(self):
|
||||
"""SectionDef creates with name, bars, energy."""
|
||||
section = SectionDef(name="chorus", bars=8, energy=0.9)
|
||||
assert section.name == "chorus"
|
||||
assert section.bars == 8
|
||||
assert section.energy == 0.9
|
||||
# velocity_mult and vol_mult default to 1.0 (not derived from energy)
|
||||
assert section.velocity_mult == 1.0
|
||||
assert section.vol_mult == 1.0
|
||||
|
||||
def test_section_def_default_energy(self):
|
||||
"""SectionDef defaults energy to 0.5, velocity_mult/vol_mult to 1.0."""
|
||||
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):
|
||||
"""SectionDef accepts custom velocity_mult and vol_mult via __init__ args."""
|
||||
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 TestVST3Effects:
|
||||
"""Test VST3 premium plugin mappings."""
|
||||
|
||||
def test_vst3_effects_defined(self):
|
||||
"""_VST3_EFFECTS maps effect names to VST3 plugins."""
|
||||
from scripts.compose import _VST3_EFFECTS
|
||||
assert "Pro-Q 3" in _VST3_EFFECTS
|
||||
assert "Pro-C 2" in _VST3_EFFECTS
|
||||
assert "Pro-R 2" in _VST3_EFFECTS
|
||||
assert "Timeless 3" in _VST3_EFFECTS
|
||||
|
||||
def test_fruity_eq_maps_to_proq3(self):
|
||||
"""Fruity Parametric EQ 2 → FabFilter Pro-Q 3 via normalization."""
|
||||
from scripts.compose import _VST3_EFFECTS
|
||||
# Fruity Parametric EQ 2 normalizes to Pro-Q 3
|
||||
registry_key, filename = _VST3_EFFECTS["Pro-Q 3"]
|
||||
assert registry_key == "FabFilter Pro-Q 3"
|
||||
assert filename == "FabFilter Pro-Q 3.vst3"
|
||||
|
||||
def test_fruity_compressor_maps_to_proc2(self):
|
||||
"""Fruity Compressor → FabFilter Pro-C 2 via normalization."""
|
||||
from scripts.compose import _VST3_EFFECTS
|
||||
registry_key, filename = _VST3_EFFECTS["Pro-C 2"]
|
||||
assert registry_key == "FabFilter Pro-C 2"
|
||||
assert filename == "FabFilter Pro-C 2.vst3"
|
||||
|
||||
def test_pro_r_maps_to_pror2(self):
|
||||
"""Pro-R 2 → FabFilter Pro-R 2."""
|
||||
from scripts.compose import _VST3_EFFECTS
|
||||
registry_key, filename = _VST3_EFFECTS["Pro-R 2"]
|
||||
assert registry_key == "FabFilter Pro-R 2"
|
||||
assert filename == "FabFilter Pro-R 2.vst3"
|
||||
|
||||
def test_unknown_effect_returns_none(self):
|
||||
"""Unknown effect names return no VST3 info."""
|
||||
from scripts.compose import _VST3_EFFECTS
|
||||
assert _VST3_EFFECTS.get("Some Unknown Plugin") is None
|
||||
|
||||
|
||||
class TestBuildFxChain:
|
||||
"""Test build_fx_chain function."""
|
||||
|
||||
def test_build_fx_chain_drums(self):
|
||||
"""build_fx_chain returns PluginDef list for drums role."""
|
||||
from scripts.compose import build_fx_chain
|
||||
|
||||
genre_config = {
|
||||
"mix": {
|
||||
"per_role": {
|
||||
"drums": {
|
||||
"effects": ["Fruity Parametric EQ 2", "Fruity Compressor"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugins = build_fx_chain("drums", genre_config, [])
|
||||
assert len(plugins) == 2
|
||||
# Fruity Parametric EQ 2 → Pro-Q 3
|
||||
assert "FabFilter" in plugins[0].name
|
||||
assert ".vst3" in plugins[0].path
|
||||
# Fruity Compressor → Pro-C 2
|
||||
assert "FabFilter" in plugins[1].name
|
||||
|
||||
def test_build_fx_chain_bass(self):
|
||||
"""build_fx_chain returns PluginDef list for bass role."""
|
||||
from scripts.compose import build_fx_chain
|
||||
|
||||
genre_config = {
|
||||
"mix": {
|
||||
"per_role": {
|
||||
"bass": {
|
||||
"effects": ["Fruity Parametric EQ 2", "Saturn 2"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugins = build_fx_chain("bass", genre_config, [])
|
||||
assert len(plugins) == 2
|
||||
# Saturn 2 → FabFilter Saturn 2
|
||||
assert "Saturn" in plugins[1].name
|
||||
|
||||
def test_build_fx_chain_empty_effects(self):
|
||||
"""build_fx_chain returns empty list when no effects configured."""
|
||||
from scripts.compose import build_fx_chain
|
||||
|
||||
genre_config = {"mix": {"per_role": {}}}
|
||||
plugins = build_fx_chain("drums", genre_config, [])
|
||||
assert plugins == []
|
||||
|
||||
def test_build_fx_chain_unknown_effect_uses_name(self):
|
||||
"""Unknown effect names are used as-is."""
|
||||
from scripts.compose import build_fx_chain
|
||||
|
||||
genre_config = {
|
||||
"mix": {
|
||||
"per_role": {
|
||||
"lead": {
|
||||
"effects": ["Some Unknown FX"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugins = build_fx_chain("lead", genre_config, [])
|
||||
# Unknown effects are skipped (not added to plugins)
|
||||
assert len(plugins) == 0
|
||||
|
||||
|
||||
class TestInstrumentPlugins:
|
||||
"""Test instrument plugin helpers (Serum 2, Omnisphere)."""
|
||||
|
||||
def test_serum2_plugin_def(self):
|
||||
"""serum2() returns PluginDef with registry key name."""
|
||||
from scripts.compose import serum2
|
||||
|
||||
plugin = serum2()
|
||||
assert plugin.name == "Serum2"
|
||||
assert plugin.path == "Serum2.vst3"
|
||||
assert plugin.index == 0
|
||||
|
||||
def test_omnisphere_plugin_def(self):
|
||||
"""omnisphere() returns PluginDef with registry key name."""
|
||||
from scripts.compose import omnisphere
|
||||
|
||||
plugin = omnisphere()
|
||||
assert plugin.name == "Omnisphere"
|
||||
assert plugin.path == "Omnisphere.vst3"
|
||||
assert plugin.index == 0
|
||||
|
||||
|
||||
class TestCreateReturnTracks:
|
||||
"""Test create_return_tracks function."""
|
||||
|
||||
def test_create_return_tracks_returns_two(self):
|
||||
"""create_return_tracks returns [Reverb, Delay] tracks."""
|
||||
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):
|
||||
"""Reverb return track has FabFilter Pro-R 2 plugin."""
|
||||
from scripts.compose import create_return_tracks
|
||||
|
||||
tracks = create_return_tracks()
|
||||
reverb = tracks[0]
|
||||
assert len(reverb.plugins) == 1
|
||||
assert "FabFilter" in reverb.plugins[0].name
|
||||
assert ".vst3" in reverb.plugins[0].path
|
||||
|
||||
def test_delay_track_has_timeless3(self):
|
||||
"""Delay return track has FabFilter Timeless 3 plugin."""
|
||||
from scripts.compose import create_return_tracks
|
||||
|
||||
tracks = create_return_tracks()
|
||||
delay = tracks[1]
|
||||
assert len(delay.plugins) == 1
|
||||
assert "Timeless" in delay.plugins[0].name
|
||||
assert ".vst3" in delay.plugins[0].path
|
||||
|
||||
def test_return_tracks_have_volume_0_7(self):
|
||||
"""Return tracks have volume 0.7."""
|
||||
from scripts.compose import create_return_tracks
|
||||
|
||||
tracks = create_return_tracks()
|
||||
for t in tracks:
|
||||
assert t.volume == 0.7
|
||||
Reference in New Issue
Block a user