fix: real preset data for all VST2/VST3 plugins, template system with ground-truth registry

- Extracted preset data from all_plugins_v2.rpp for 14 previously broken plugins
- Fixed PLUGIN_REGISTRY entries: Kontakt 7, Gullfoss, ValhallaDelay, VC 160/76, The Glue
- Template parser falls back to PLUGIN_PRESETS when source RPP has fake data
- Substitute Transient Master (not installed) with FabFilter Pro-C 2
- All 25 plugins now load correctly in REAPER
- Added template generator scripts and ground truth references
- Cleaned up temp/debug files from output/
This commit is contained in:
renato97
2026-05-03 18:54:40 -03:00
parent 3444006411
commit 8562bfbed1
23 changed files with 99316 additions and 688 deletions

597
src/composer/templates.py Normal file
View File

@@ -0,0 +1,597 @@
"""RPP template extraction and generation.
Extracts track/FX chain structures from professionally-built .rpp files,
fixes GUIDs with real values from reaper-vstplugins64.ini, and generates
working .rpp files.
Usage:
from src.composer.templates import extract_template, generate_rpp
template = extract_template("ejemplos/TU_DIABLO_ITHAN_NY.rpp")
generate_rpp(template, "output/tu_diablo_fixed.rpp")
"""
from __future__ import annotations
import re
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from rpp import Element, dumps
from src.reaper_builder import PLUGIN_REGISTRY, ALIAS_MAP, vst2_element, vst3_element, VST2_REGISTRY, VST3_REGISTRY, PLUGIN_PRESETS
# ---------------------------------------------------------------------------
# Template data classes
# ---------------------------------------------------------------------------
@dataclass
class PluginTemplate:
"""A plugin (instrument or effect) within a track FX chain."""
name: str # Registry key (e.g. "Serum2", "Decapitator")
path: str # Filename on disk (e.g. "Serum2.vst3", "Decapitator.dll")
display_name: str # Full REAPER display name
uniqueid_guid: str # uniqueid{GUID} from registry
preset_data: list[str] | None = None
is_vst2: bool = False # True for SoundToys .dll plugins
index: int = 0 # Position in FX chain
def build_element(self) -> Element:
"""Build a VST Element for this plugin."""
if self.is_vst2:
return vst2_element(self.display_name, self.path, self.uniqueid_guid, self.preset_data)
return vst3_element(self.display_name, self.path, self.uniqueid_guid, self.preset_data)
@dataclass
class TrackTemplate:
"""A track with its FX chain, extracted from an RPP template."""
name: str
volume: float = 0.85
pan: float = 0.0
color: int = 0
is_bus: bool = False
plugins: list[PluginTemplate] = field(default_factory=list)
@dataclass
class TemplateProject:
"""A complete project template extracted from an RPP file."""
name: str
bpm: float
key: str = "Am"
time_sig_num: int = 4
time_sig_den: int = 4
tracks: list[TrackTemplate] = field(default_factory=list)
def generate_rpp(self, output_path: str | Path) -> None:
"""Generate a working .rpp file from this template.
Uses RPPBuilder to produce REAPER 7.65 format with proper GUIDs,
FXCHAIN structure, and base64 preset data.
"""
from src.core.schema import SongDefinition, SongMeta, TrackDef, PluginDef
from src.reaper_builder import RPPBuilder
meta = SongMeta(
bpm=self.bpm,
key=self.key,
title=self.name,
time_sig_num=self.time_sig_num,
time_sig_den=self.time_sig_den,
)
# Convert TrackTemplate → TrackDef
tracks: list[TrackDef] = []
for tt in self.tracks:
# Build PluginDef list for RPPBuilder
plugin_defs: list[PluginDef] = []
for pt in tt.plugins:
plugin_defs.append(PluginDef(
name=pt.name,
path=pt.path,
index=pt.index,
preset_data=pt.preset_data,
))
track_def = TrackDef(
name=tt.name,
volume=tt.volume,
pan=tt.pan,
color=tt.color,
clips=[],
plugins=plugin_defs,
)
tracks.append(track_def)
song = SongDefinition(meta=meta, tracks=tracks)
builder = RPPBuilder(song)
builder.write(str(output_path))
# ---------------------------------------------------------------------------
# Plugin substitution map (for non-existent plugins in source RPPs)
# ---------------------------------------------------------------------------
# Maps plugin names that appear in source RPPs but don't exist → substitute plugin key
SUBSTITUTE: dict[str, str] = {
# Transient Master not installed — replace with FabFilter Pro-C 2 (similar transient shaping)
"Transient Master.dll": "FabFilter_Pro-C_2",
"NI Transient Master": "FabFilter_Pro-C_2",
"Transient Master (Native Instruments)": "FabFilter_Pro-C_2",
# The Glue standalone .dll doesn't exist — use VST2 version from registry
"Cytomic.dll": "The_Glue",
# ValhallaDelay_x64.dll not in scan — use ValhallaDelay (VST2)
"ValhallaDelay_x64.dll": "ValhallaDelay",
# Pigments.exe wrong extension
"Pigments.exe": "Pigments",
# Gullfoss VST2 .dll — use registry keys
"Gullfoss.dll": "Gullfoss",
"Gullfoss Master.dll": "Gullfoss_Master",
"Gullfoss Live.dll": "Gullfoss_Live",
# VC 160/76 — fix display names from "VC 160 FX (Audified)" → registry keys
"VC 160.vst3": "VC_160",
"VC 76.vst3": "VC_76",
"VC 2A.vst3": "VC_2A",
# Kontakt 7 — fix display name
"Kontakt 7.vst3": "Kontakt_7",
# Source RPP display name → registry key (for non-matching display names)
"VST3: VC 160 FX (Audified)": "VC_160",
"VST3: VC 76 FX (Audified)": "VC_76",
"VST3: VC 2A FX (Audified)": "VC_2A",
"VST: TheGlue (Cytomic)": "The_Glue",
"VST: ValhallaDelay x64 (Valhalla DSP)": "ValhallaDelay",
"VST3: Kontakt 7 (Native Instruments)": "Kontakt_7",
"VST: Gullfoss Master (Soundtheory)": "Gullfoss_Master",
"VST: Gullfoss (Soundtheory)": "Gullfoss",
"VST: Gullfoss Live (Soundtheory)": "Gullfoss_Live",
}
# ---------------------------------------------------------------------------
# GUID lookup helpers
# ---------------------------------------------------------------------------
def _lookup_vst3(name: str) -> tuple[str, str, str] | None:
"""Look up VST3 plugin by registry key or filename."""
# Try exact key match
if name in VST3_REGISTRY:
return VST3_REGISTRY[name]
# Try filename match
for display_name, filename, uid_guid in VST3_REGISTRY.values():
if filename == name:
return (display_name, filename, uid_guid)
return None
def _lookup_vst2(name: str) -> tuple[str, str, str] | None:
"""Look up VST2/SoundToys plugin by registry key or filename."""
# Try exact key match
if name in VST2_REGISTRY:
return VST2_REGISTRY[name]
# Try filename match
for display_name, filename, uid_guid in VST2_REGISTRY.values():
if filename == name:
return (display_name, filename, uid_guid)
return None
def _make_plugin_template(
name: str,
path: str,
index: int = 0,
) -> PluginTemplate | None:
"""Create a PluginTemplate from plugin name + path, with GUID lookup.
Handles plugin substitution for non-existent plugins.
Returns None if the plugin cannot be resolved.
"""
# Apply substitution if needed
resolved_name = SUBSTITUTE.get(name, name)
resolved_path = SUBSTITUTE.get(path, path)
# Try direct registry key lookup first (covers substituted names)
direct_entry = PLUGIN_REGISTRY.get(resolved_name) or PLUGIN_REGISTRY.get(resolved_path)
if direct_entry:
is_vst2 = direct_entry[2].startswith("<")
entry = direct_entry
else:
# Fall back to path-based detection
is_vst2 = path.endswith(".dll")
if is_vst2:
entry = _lookup_vst2(resolved_path) or _lookup_vst2(resolved_name)
else:
entry = _lookup_vst3(resolved_path) or _lookup_vst3(resolved_name)
if entry:
display_name, filename, uid_guid = entry
preset_data = PLUGIN_PRESETS.get(resolved_name) if is_vst2 else PLUGIN_PRESETS.get(resolved_name)
else:
# Unresolved — use name/path as display name with empty GUID
display_name = f"VST3: {resolved_name}" if not is_vst2 else f"VST: {resolved_name}"
filename = resolved_path
uid_guid = ""
return PluginTemplate(
name=resolved_name,
path=filename,
display_name=display_name,
uniqueid_guid=uid_guid,
preset_data=preset_data,
is_vst2=is_vst2,
index=index,
)
# ---------------------------------------------------------------------------
# RPP parser (regex-based, handles both RPP library format and raw text)
# ---------------------------------------------------------------------------
# Regex patterns for parsing RPP files
_TRACK_RE = re.compile(r'<TRACK\s+\{[^}]+\}', re.IGNORECASE)
_TRACK_CLOSE_RE = re.compile(r'^\s*</TRACK>', re.IGNORECASE)
_NAME_RE = re.compile(r'^\s*NAME\s+"([^"]*)"', re.IGNORECASE)
_VOLPAN_RE = re.compile(r'^\s*VOLPAN\s+([\d.e+-]+)\s+([\d.e+-]+)', re.IGNORECASE)
_ISBUS_RE = re.compile(r'^\s*ISBUS\s+(\d+)', re.IGNORECASE)
_PEAKCOL_RE = re.compile(r'^\s*PEAKCOL\s+(\d+)', re.IGNORECASE)
_FXCHAIN_RE = re.compile(r'^\s*<FXCHAIN', re.IGNORECASE)
_FXCHAIN_CLOSE_RE = re.compile(r'^\s*</FXCHAIN>', re.IGNORECASE)
# VST element: <VST "display name" "filename" 0 "" uniqueid{GUID} "" [preset lines] >
# The header line ends with "" (closing of last string arg) followed by optional preset lines
# and finally a > closing tag on its own line or after preset content.
# The GUID portion (inside {} or <>) may be prefixed with an id number.
# Examples from real RPPs:
# <VST "VST: Decapitator (SoundToys)" "Decapitator.dll" 0 "" 1145980753<GUID> ""
# <VST "VST3: Serum 2" "Serum2.vst3" 0 "" 0<> ""
_VST_RE = re.compile(
r'<VST\s+"([^"]+)"\s+"([^"]+)"\s+0\s+""\s*([\d<>{}a-fA-F0-9]+)\s*""',
re.IGNORECASE,
)
_VST_CLOSE_RE = re.compile(r'^\s*</VST>', re.IGNORECASE)
_PARAM_RE = re.compile(r'^\s*PARAM\s+\d+\s+"[^"]*"\s+', re.IGNORECASE)
_TEMPO_RE = re.compile(r'^\s*TEMPO\s+([\d.e+-]+)\s+(\d+)\s+(\d+)', re.IGNORECASE)
def _strip_invalid_params(lines: list[str]) -> list[str]:
"""Remove invalid PARAM lines from preset data lines.
Claude's RPPs contain lines like:
PARAM 0 "Drive" 0.35
which are NOT valid REAPER tokens. REAPER stores parameter values in
base64 preset data, not as named PARAM lines.
"""
result: list[str] = []
for line in lines:
stripped = line.strip()
# Skip PARAM lines that match our pattern
if _PARAM_RE.match(line):
continue
result.append(line)
return result
def _extract_guid(raw: str, filename: str, display_name: str) -> str:
"""Extract clean GUID from raw GUID string.
Handles formats like:
- "0<>" → lookup from registry (empty/fake GUID)
- "1145980753<56535444454341>" → extract <GUID>
- "691258006{56534558...}" → extract {GUID}
- plain "0<>" or "0{}" → lookup from registry
"""
import re as re_module
raw = raw.strip()
# Try extract <GUID> pattern (angle brackets)
m = re_module.search(r'<([^<>]+)>', raw)
if m:
guid = m.group(1)
# Check if it's a real GUID (not just angle bracket chars)
if guid and guid not in ('<>', '{}', ''):
return guid
# Try extract {GUID} pattern (curly braces)
m = re_module.search(r'\{([^}]+)\}', raw)
if m:
guid = m.group(1)
if guid and guid not in ('{}', ''):
return guid
# Empty or fake GUID — lookup from registry
is_vst2 = filename.endswith(".dll")
key = _resolve_registry_key(display_name, filename, is_vst2)
if key:
if is_vst2:
entry = VST2_REGISTRY.get(key)
else:
entry = VST3_REGISTRY.get(key)
if entry:
return entry[2] # Return the uid_guid from registry
return raw # Return as-is if no lookup found
def _parse_vst_block(lines: list[str], start_idx: int) -> tuple[PluginTemplate | None, int]:
"""Parse a VST element starting at start_idx in lines.
Returns (PluginTemplate, index_of_close_tag) or (None, start_idx) if not a VST block.
"""
line = lines[start_idx].strip()
m = _VST_RE.match(line)
if not m:
return None, start_idx
display_name = m.group(1)
filename = m.group(2)
raw_guid = m.group(3).strip()
# Extract clean GUID from various formats:
# - "0<>" (empty/fake GUID) → replace with registry lookup
# - "1315270729<>" → registry lookup
# - "1145980753<56535444454341>" → extract <GUID> portion
# - "691258006{56534558667350736572756D20320000}" → extract {GUID} portion
uid_guid = _extract_guid(raw_guid, filename, display_name)
# Collect preset data lines until closing >VST tag
preset_lines: list[str] = []
idx = start_idx + 1
while idx < len(lines):
l = lines[idx]
ls = l.strip()
if '</VST' in l or ls == '>' or ls.startswith('>'):
break
if ls.startswith('<VST') or ls.startswith('<TRACK') or ls.startswith('</TRACK') or ls.startswith('</FXCHAIN'):
# Hit next element — not a preset line
break
# Collect line (strip PARAM lines later)
preset_lines.append(l)
idx += 1
preset_lines = _strip_invalid_params(preset_lines)
is_vst2 = filename.endswith(".dll")
registry_key = _resolve_registry_key(display_name, filename, is_vst2)
if registry_key:
if is_vst2:
entry = VST2_REGISTRY.get(registry_key)
else:
entry = VST3_REGISTRY.get(registry_key)
if entry:
disp, fn, guid = entry
uid_guid = guid
display_name = disp
filename = fn
else:
sub_key = SUBSTITUTE.get(filename) or SUBSTITUTE.get(display_name)
if sub_key:
sub_entry = PLUGIN_REGISTRY.get(sub_key)
if sub_entry:
display_name, filename, uid_guid = sub_entry
is_vst2 = uid_guid.startswith("<")
registry_key = sub_key
is_fake_preset = (
not preset_lines
or all(pl.strip() in ("0 0", "0", "") for pl in preset_lines)
)
if is_fake_preset and registry_key:
registry_preset = PLUGIN_PRESETS.get(registry_key)
if registry_preset:
preset_lines = registry_preset
return PluginTemplate(
name=registry_key or display_name,
path=filename,
display_name=display_name,
uniqueid_guid=uid_guid,
preset_data=preset_lines if preset_lines else None,
is_vst2=is_vst2,
index=0,
), idx
def _resolve_registry_key(display_name: str, filename: str, is_vst2: bool) -> str | None:
"""Resolve a plugin display name/filename to a registry key."""
if is_vst2:
for key, (disp, fn, guid) in VST2_REGISTRY.items():
if disp == display_name or fn == filename:
return key
else:
for key, (disp, fn, guid) in VST3_REGISTRY.items():
if disp == display_name or fn == filename:
return key
return None
def _parse_track_block(lines: list[str], start_idx: int) -> tuple[TrackTemplate | None, int]:
"""Parse a TRACK element starting at start_idx in lines.
Returns (TrackTemplate, index_after_close_tag) or (None, start_idx).
"""
line = lines[start_idx].strip()
if not _TRACK_RE.match(line):
return None, start_idx
name = ""
volume = 0.85
pan = 0.0
color = 0
is_bus = False
plugins: list[PluginTemplate] = []
plugin_index = 0
idx = start_idx + 1
in_fxchain = False
while idx < len(lines):
l = lines[idx]
ls = l.strip()
if ls.startswith('</TRACK>') or ls.startswith('</TRACK'):
break
if in_fxchain:
if _FXCHAIN_CLOSE_RE.match(ls):
in_fxchain = False
idx += 1
continue
if _VST_RE.match(ls):
plugin, new_idx = _parse_vst_block(lines, idx)
if plugin:
plugin.index = plugin_index
plugin_index += 1
plugins.append(plugin)
idx = new_idx + 1
continue
else:
if _FXCHAIN_RE.match(ls):
in_fxchain = True
idx += 1
continue
m = _NAME_RE.match(l)
if m:
name = m.group(1)
idx += 1
continue
m = _VOLPAN_RE.match(l)
if m:
try:
volume = float(m.group(1))
except ValueError:
pass
try:
pan = float(m.group(2))
except ValueError:
pass
idx += 1
continue
m = _PEAKCOL_RE.match(l)
if m:
try:
color = int(m.group(1))
except ValueError:
pass
idx += 1
continue
m = _ISBUS_RE.match(l)
if m:
try:
is_bus = int(m.group(1)) == 1
except Value:
pass
idx += 1
continue
idx += 1
if not name:
name = "Unnamed Track"
return TrackTemplate(
name=name,
volume=volume,
pan=pan,
color=color,
is_bus=is_bus,
plugins=plugins,
), idx
def extract_template(rpp_path: str | Path) -> TemplateProject:
"""Extract a TemplateProject from an existing .rpp file.
Parses track structures, FX chains, and plugin configurations,
applying GUID corrections and stripping invalid PARAM lines.
Args:
rpp_path: Path to source .rpp file
Returns:
TemplateProject with all tracks and plugins resolved to registry entries
Raises:
FileNotFoundError: If rpp_path doesn't exist
ValueError: If file can't be parsed as RPP
"""
path = Path(rpp_path)
if not path.exists():
raise FileNotFoundError(f"RPP file not found: {path}")
content = path.read_text(encoding="utf-8")
lines = content.split('\n')
# Extract tempo, key from header comments or TEMPO line
bpm = 120.0
key = "Am"
time_sig_num, time_sig_den = 4, 4
project_name = path.stem
# Parse TEMPO line
for i, line in enumerate(lines):
m = _TEMPO_RE.match(line.strip())
if m:
try:
bpm = float(m.group(1))
except ValueError:
pass
try:
time_sig_num = int(m.group(2))
time_sig_den = int(m.group(3))
except ValueError:
pass
break
# Find project name from comments
for line in lines:
if 'PROYECTO:' in line:
# Extract project name from comment
m = re.search(r'PROYECTO:\s*(.+?)(?:\n|$)', line)
if m:
project_name = m.group(1).strip()
break
# Extract tracks
tracks: list[TrackTemplate] = []
idx = 0
while idx < len(lines):
if _TRACK_RE.match(lines[idx].strip()):
track, new_idx = _parse_track_block(lines, idx)
if track:
tracks.append(track)
idx = new_idx + 1
else:
idx += 1
return TemplateProject(
name=project_name,
bpm=bpm,
key=key,
time_sig_num=time_sig_num,
time_sig_den=time_sig_den,
tracks=tracks,
)
def generate_rpp(template: TemplateProject, output_path: str | Path) -> None:
"""Generate a working .rpp file from a TemplateProject.
Uses RPPBuilder to produce REAPER 7.65 format with correct GUIDs,
proper FXCHAIN structure, and base64 preset data. All plugins are
resolved via VST3_REGISTRY/VST2_REGISTRY.
Args:
template: TemplateProject to generate from
output_path: Destination .rpp path
"""
# Ensure output directory exists
p = Path(output_path)
p.parent.mkdir(parents=True, exist_ok=True)
template.generate_rpp(str(p))