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:
renato97
2026-05-03 14:00:11 -03:00
parent af6d61c8a1
commit 53741b48b6
6 changed files with 1541 additions and 141 deletions

View File

@@ -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)