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:
597
src/composer/templates.py
Normal file
597
src/composer/templates.py
Normal 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))
|
||||
Reference in New Issue
Block a user