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

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