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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user