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:
@@ -1,16 +1,17 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Compose a REAPER .rpp project from the sample library.
|
"""Compose a REAPER .rpp project from the sample library.
|
||||||
|
|
||||||
Single entrypoint: loads sample index, builds a SongDefinition from the selector/composer,
|
Single entrypoint: loads genre config, builds a SongDefinition from sections,
|
||||||
and writes a .rpp file.
|
and writes a .rpp file.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/compose.py --genre reggaeton --bpm 95 --key Am
|
python scripts/compose.py --genre reggaeton --bpm 95 --key Am
|
||||||
python scripts/compose.py --genre trap --bpm 140 --key Cm --output output/my_track.rpp
|
python scripts/compose.py --genre reggaeton --bpm 95 --key Am --output output/my_track.rpp
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -18,8 +19,11 @@ from pathlib import Path
|
|||||||
_ROOT = Path(__file__).parent.parent
|
_ROOT = Path(__file__).parent.parent
|
||||||
sys.path.insert(0, str(_ROOT))
|
sys.path.insert(0, str(_ROOT))
|
||||||
|
|
||||||
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote
|
from src.core.schema import (
|
||||||
from src.composer.rhythm import get_notes
|
SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote,
|
||||||
|
PluginDef, SectionDef,
|
||||||
|
)
|
||||||
|
from src.composer.rhythm import get_notes, GENERATORS as RHYTHM_GENERATORS
|
||||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||||
from src.selector import SampleSelector
|
from src.selector import SampleSelector
|
||||||
@@ -28,67 +32,298 @@ from src.reaper_builder.render import render_project
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Track builders
|
# VST3 plugin builder helpers (premium plugins)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def build_drum_track(
|
# Premium VST3 plugins available:
|
||||||
role: str,
|
# Serum 2 (Xfer Records), Omnisphere (Spectrasonics)
|
||||||
generator_name: str,
|
# FabFilter Pro-Q 3, Pro-C 2, Pro-R 2, Pro-L 2, Saturn 2, Timeless 3
|
||||||
bars: int,
|
# The Glue (Cytomic)
|
||||||
) -> TrackDef:
|
# Valhalla Delay
|
||||||
"""Build a drum MIDI track from a rhythm generator.
|
|
||||||
|
def serum2() -> PluginDef:
|
||||||
|
"""Serum 2 synth — used for bass, lead, harmony tracks."""
|
||||||
|
return PluginDef(
|
||||||
|
name="Serum2",
|
||||||
|
path="Serum2.vst3",
|
||||||
|
index=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def omnisphere() -> PluginDef:
|
||||||
|
"""Omnisphere — used for pad tracks."""
|
||||||
|
return PluginDef(
|
||||||
|
name="Omnisphere",
|
||||||
|
path="Omnisphere.vst3",
|
||||||
|
index=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ROLE_MELODIC_GENERATORS = {
|
||||||
|
"bass": bass_tresillo,
|
||||||
|
"lead": lead_hook,
|
||||||
|
"harmony": chords_block,
|
||||||
|
"pad": pad_sustain,
|
||||||
|
}
|
||||||
|
|
||||||
|
ROLE_RHYTHM_GENERATORS = {
|
||||||
|
"drums": "kick_main_notes",
|
||||||
|
"snare": "snare_verse_notes",
|
||||||
|
"hihat": "hihat_16th_notes",
|
||||||
|
"perc": "perc_combo_notes",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Roles that use audio items per hit instead of MIDI pattern
|
||||||
|
AUDIO_ROLES = {"drums", "snare", "hihat", "perc"}
|
||||||
|
|
||||||
|
# Role → sample key (used for SampleSelector)
|
||||||
|
ROLE_TO_SAMPLE_ROLE = {
|
||||||
|
"drums": "kick",
|
||||||
|
"snare": "snare",
|
||||||
|
"hihat": "hihat",
|
||||||
|
"perc": "perc",
|
||||||
|
"bass": "bass",
|
||||||
|
"lead": "lead",
|
||||||
|
"harmony": "keys",
|
||||||
|
"pad": "pad",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Effect chain builder
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Mapping of effect names to VST3 plugin entries
|
||||||
|
# Format: (registry_key, filename) tuples
|
||||||
|
# registry_key must match a key in VST3_REGISTRY for _build_plugin() lookup
|
||||||
|
_VST3_EFFECTS: dict[str, tuple[str, str]] = {
|
||||||
|
"Pro-Q 3": ("FabFilter Pro-Q 3", "FabFilter Pro-Q 3.vst3"),
|
||||||
|
"Pro-C 2": ("FabFilter Pro-C 2", "FabFilter Pro-C 2.vst3"),
|
||||||
|
"Pro-R 2": ("FabFilter Pro-R 2", "FabFilter Pro-R 2.vst3"),
|
||||||
|
"Timeless 3": ("FabFilter Timeless 3", "FabFilter Timeless 3.vst3"),
|
||||||
|
"Saturn 2": ("FabFilter Saturn 2", "FabFilter Saturn 2.vst3"),
|
||||||
|
"Pro-L 2": ("FabFilter Pro-L 2", "FabFilter Pro-L 2.vst3"),
|
||||||
|
"The Glue": ("The Glue", "The Glue.vst3"),
|
||||||
|
"Valhalla Delay": ("Valhalla Delay", "ValhallaDelay.vst3"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_fx_chain(role: str, genre_config: dict, track_plugins: list[PluginDef]) -> list[PluginDef]:
|
||||||
|
"""Build a plugin chain for a role from genre config mix settings.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
role: Track name (e.g. "kick", "snare")
|
role: Track role (e.g. "drums", "bass", "lead")
|
||||||
generator_name: Name from rhythm.GENERATORS (e.g. "kick_main_notes")
|
genre_config: Loaded genre JSON dict
|
||||||
bars: Number of bars
|
track_plugins: Already-added plugins (instruments) to skip
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PluginDef for the FX chain (effects only, no instruments).
|
||||||
"""
|
"""
|
||||||
note_dict = get_notes(generator_name, bars)
|
mix = genre_config.get("mix", {})
|
||||||
midi_notes = rhythm_to_midi(note_dict)
|
per_role = mix.get("per_role", {}).get(role, {})
|
||||||
clip = ClipDef(
|
|
||||||
position=0.0,
|
plugins: list[PluginDef] = []
|
||||||
length=bars * 4.0,
|
effects = per_role.get("effects", [])
|
||||||
name=f"{role.capitalize()} Pattern",
|
for idx, effect_name in enumerate(effects):
|
||||||
midi_notes=midi_notes,
|
key = effect_name
|
||||||
|
# Normalize Fruity* aliases
|
||||||
|
if key == "Fruity Parametric EQ 2":
|
||||||
|
key = "Pro-Q 3"
|
||||||
|
elif key == "Fruity Compressor":
|
||||||
|
key = "Pro-C 2"
|
||||||
|
elif key == "Fruity Delay 3":
|
||||||
|
key = "Timeless 3"
|
||||||
|
elif key == "Fruity Reverb 2":
|
||||||
|
key = "Pro-R 2"
|
||||||
|
|
||||||
|
vst3_info = _VST3_EFFECTS.get(key)
|
||||||
|
if vst3_info:
|
||||||
|
registry_key, filename = vst3_info
|
||||||
|
plugins.append(PluginDef(
|
||||||
|
name=registry_key,
|
||||||
|
path=filename,
|
||||||
|
index=idx,
|
||||||
|
))
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Return track builders
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_return_tracks() -> list[TrackDef]:
|
||||||
|
"""Create reverb and delay return tracks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[Reverb return TrackDef (FabFilter Pro-R 2),
|
||||||
|
Delay return TrackDef (FabFilter Timeless 3)]
|
||||||
|
"""
|
||||||
|
reverb_track = TrackDef(
|
||||||
|
name="Reverb",
|
||||||
|
volume=0.7,
|
||||||
|
pan=0.0,
|
||||||
|
clips=[],
|
||||||
|
plugins=[PluginDef(
|
||||||
|
name="FabFilter Pro-R 2",
|
||||||
|
path="FabFilter_Pro_R_2.vst3",
|
||||||
|
index=0,
|
||||||
|
)],
|
||||||
|
send_reverb=0.0,
|
||||||
|
send_delay=0.0,
|
||||||
)
|
)
|
||||||
return TrackDef(name=role.capitalize(), clips=[clip])
|
delay_track = TrackDef(
|
||||||
|
name="Delay",
|
||||||
|
volume=0.7,
|
||||||
|
pan=0.0,
|
||||||
|
clips=[],
|
||||||
|
plugins=[PluginDef(
|
||||||
|
name="FabFilter Timeless 3",
|
||||||
|
path="FabFilter_Timeless_3.vst3",
|
||||||
|
index=0,
|
||||||
|
)],
|
||||||
|
send_reverb=0.0,
|
||||||
|
send_delay=0.0,
|
||||||
|
)
|
||||||
|
return [reverb_track, delay_track]
|
||||||
|
|
||||||
|
|
||||||
def build_melodic_track(
|
# ---------------------------------------------------------------------------
|
||||||
role: str,
|
# Section track builder
|
||||||
generator_fn,
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_section_tracks(
|
||||||
|
genre_config: dict,
|
||||||
|
selector: SampleSelector,
|
||||||
key: str,
|
key: str,
|
||||||
bpm: float,
|
bpm: float,
|
||||||
bars: int,
|
) -> tuple[list[TrackDef], list[SectionDef]]:
|
||||||
selector: SampleSelector | None = None,
|
"""Build all tracks from genre config sections.
|
||||||
) -> TrackDef:
|
|
||||||
"""Build a melodic MIDI track from a generator function.
|
Creates one set of tracks per role, with clips per section placed at
|
||||||
|
cumulative bar offsets. Applies section energy via velocity_mult and vol_mult.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
role: Track name (e.g. "bass", "lead")
|
genre_config: Loaded genre JSON dict
|
||||||
generator_fn: Callable from melodic.py (e.g. bass_tresillo)
|
selector: SampleSelector for sample queries
|
||||||
key: Musical key (e.g. "Am")
|
key: Musical key (e.g. "Am")
|
||||||
bpm: Tempo for sample selection
|
bpm: BPM for sample selection
|
||||||
bars: Number of bars
|
|
||||||
selector: Optional SampleSelector; if provided, sets audio_path on ClipDef
|
Returns:
|
||||||
|
(tracks, sections)
|
||||||
"""
|
"""
|
||||||
note_list = generator_fn(key=key, bars=bars)
|
structure = genre_config.get("structure", {})
|
||||||
midi_notes = melodic_to_midi(note_list)
|
sections_raw = structure.get("sections", [])
|
||||||
|
roles = genre_config.get("roles", {})
|
||||||
|
|
||||||
audio_path: str | None = None
|
# Parse sections into SectionDef list
|
||||||
if selector is not None:
|
sections: list[SectionDef] = []
|
||||||
match = selector.select_one(role=role, key=key, bpm=bpm)
|
for s in sections_raw:
|
||||||
if match:
|
sections.append(SectionDef(
|
||||||
audio_path = match.get("original_path", None)
|
name=s.get("name", "unknown"),
|
||||||
|
bars=s.get("bars", 4),
|
||||||
|
energy=s.get("energy", 0.5),
|
||||||
|
))
|
||||||
|
|
||||||
clip = ClipDef(
|
# Compute cumulative bar offsets for section positions
|
||||||
position=0.0,
|
section_offsets: list[float] = []
|
||||||
length=bars * 4.0,
|
offset = 0.0
|
||||||
name=f"{role.capitalize()} MIDI",
|
for sec in sections:
|
||||||
audio_path=audio_path,
|
section_offsets.append(offset)
|
||||||
midi_notes=midi_notes,
|
offset += sec.bars
|
||||||
)
|
|
||||||
return TrackDef(name=role.capitalize(), clips=[clip])
|
# Build one track per role
|
||||||
|
tracks: list[TrackDef] = []
|
||||||
|
|
||||||
|
for role, role_cfg in roles.items():
|
||||||
|
sample_role = ROLE_TO_SAMPLE_ROLE.get(role, role)
|
||||||
|
generator_name = role_cfg.get("notes_template", "")
|
||||||
|
|
||||||
|
# Select sample for this role
|
||||||
|
sample_match = selector.select_one(role=sample_role, key=key, bpm=bpm)
|
||||||
|
sample_path = sample_match.get("original_path") if sample_match else None
|
||||||
|
|
||||||
|
# Collect clips for each section
|
||||||
|
section_clips: list[ClipDef] = []
|
||||||
|
|
||||||
|
for sec_idx, (section, sec_offset) in enumerate(zip(sections, section_offsets)):
|
||||||
|
# Derive velocity and volume multipliers from section energy
|
||||||
|
vel_mult = section.energy
|
||||||
|
vol_mult = section.energy
|
||||||
|
|
||||||
|
if role in ROLE_RHYTHM_GENERATORS:
|
||||||
|
gen_name = ROLE_RHYTHM_GENERATORS[role]
|
||||||
|
note_dict = get_notes(gen_name, section.bars, velocity_mult=vel_mult)
|
||||||
|
|
||||||
|
# Audio roles: one clip per hit (one-shot samples placed at beat positions)
|
||||||
|
if role in AUDIO_ROLES:
|
||||||
|
for bar_offset, bar_notes in note_dict.items():
|
||||||
|
for note_data in bar_notes:
|
||||||
|
note_pos = note_data.get("pos", 0.0)
|
||||||
|
audio_clip = ClipDef(
|
||||||
|
position=sec_offset * 4.0 + bar_offset * 4.0 + note_pos,
|
||||||
|
length=0.5, # one-shot duration
|
||||||
|
name=f"{section.name.capitalize()} {role.capitalize()}",
|
||||||
|
audio_path=sample_path,
|
||||||
|
)
|
||||||
|
section_clips.append(audio_clip)
|
||||||
|
else:
|
||||||
|
# MIDI roles: single clip with all notes
|
||||||
|
midi_notes = rhythm_to_midi(note_dict)
|
||||||
|
clip = ClipDef(
|
||||||
|
position=sec_offset * 4.0, # bars → beats
|
||||||
|
length=section.bars * 4.0,
|
||||||
|
name=f"{section.name.capitalize()} {role.capitalize()}",
|
||||||
|
midi_notes=midi_notes,
|
||||||
|
)
|
||||||
|
section_clips.append(clip)
|
||||||
|
elif role in ROLE_MELODIC_GENERATORS:
|
||||||
|
gen_fn = ROLE_MELODIC_GENERATORS[role]
|
||||||
|
note_list = gen_fn(key=key, bars=section.bars, velocity_mult=vel_mult)
|
||||||
|
midi_notes = melodic_to_midi(note_list)
|
||||||
|
clip = ClipDef(
|
||||||
|
position=sec_offset * 4.0,
|
||||||
|
length=section.bars * 4.0,
|
||||||
|
name=f"{section.name.capitalize()} {role.capitalize()}",
|
||||||
|
midi_notes=midi_notes,
|
||||||
|
audio_path=sample_path,
|
||||||
|
)
|
||||||
|
section_clips.append(clip)
|
||||||
|
|
||||||
|
if not section_clips:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build plugins: instrument (if melodic) + FX chain
|
||||||
|
plugins: list[PluginDef] = []
|
||||||
|
|
||||||
|
# Melodic tracks get instrument plugins (Serum 2 or Omnisphere)
|
||||||
|
if role in ("bass", "lead", "harmony"):
|
||||||
|
plugins.append(serum2())
|
||||||
|
elif role == "pad":
|
||||||
|
plugins.append(omnisphere())
|
||||||
|
|
||||||
|
# FX chain from genre config (effects only, instruments already added above)
|
||||||
|
fx_chain = build_fx_chain(role, genre_config, plugins)
|
||||||
|
plugins.extend(fx_chain)
|
||||||
|
|
||||||
|
# Send levels from per_role config
|
||||||
|
per_role_cfg = genre_config.get("mix", {}).get("per_role", {}).get(role, {})
|
||||||
|
send_reverb = 0.3 if per_role_cfg.get("reverb_on_lead") or per_role_cfg.get("reverb_on_snare") else 0.0
|
||||||
|
send_delay = 0.0
|
||||||
|
|
||||||
|
track = TrackDef(
|
||||||
|
name=role.capitalize(),
|
||||||
|
volume=0.85 * vol_mult,
|
||||||
|
pan=0.0,
|
||||||
|
color=0,
|
||||||
|
clips=section_clips,
|
||||||
|
plugins=plugins,
|
||||||
|
send_reverb=send_reverb,
|
||||||
|
send_delay=send_delay,
|
||||||
|
)
|
||||||
|
tracks.append(track)
|
||||||
|
|
||||||
|
return tracks, sections
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -97,7 +332,7 @@ def build_melodic_track(
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Compose a REAPER .rpp project from the sample library."
|
description="Compose a REAPER .rpp project from the genre config."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--genre",
|
"--genre",
|
||||||
@@ -107,8 +342,8 @@ def main() -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--bpm",
|
"--bpm",
|
||||||
type=float,
|
type=float,
|
||||||
default=95.0,
|
default=96.0,
|
||||||
help="BPM (default: 95)",
|
help="BPM (default: 96)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key",
|
"--key",
|
||||||
@@ -128,11 +363,11 @@ def main() -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--render-output",
|
"--render-output",
|
||||||
default=None,
|
default=None,
|
||||||
help="Output WAV path for rendering. Defaults to <output>.wav with .rpp extension replaced.",
|
help="Output WAV path for rendering.",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Validate BPM before any writes
|
# Validate BPM
|
||||||
if args.bpm <= 0:
|
if args.bpm <= 0:
|
||||||
raise ValueError(f"bpm must be > 0, got {args.bpm}")
|
raise ValueError(f"bpm must be > 0, got {args.bpm}")
|
||||||
|
|
||||||
@@ -140,7 +375,16 @@ def main() -> None:
|
|||||||
output_path = Path(args.output)
|
output_path = Path(args.output)
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Load sample index (for melodic tracks that use audio samples)
|
# Load genre config
|
||||||
|
genre_path = _ROOT / "knowledge" / "genres" / f"{args.genre.lower()}_2009.json"
|
||||||
|
if not genre_path.exists():
|
||||||
|
print(f"ERROR: genre config not found at {genre_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(genre_path, "r", encoding="utf-8") as f:
|
||||||
|
genre_config = json.load(f)
|
||||||
|
|
||||||
|
# Load sample index
|
||||||
index_path = _ROOT / "data" / "sample_index.json"
|
index_path = _ROOT / "data" / "sample_index.json"
|
||||||
if not index_path.exists():
|
if not index_path.exists():
|
||||||
print(f"ERROR: sample index not found at {index_path}", file=sys.stderr)
|
print(f"ERROR: sample index not found at {index_path}", file=sys.stderr)
|
||||||
@@ -148,42 +392,32 @@ def main() -> None:
|
|||||||
|
|
||||||
selector = SampleSelector(str(index_path))
|
selector = SampleSelector(str(index_path))
|
||||||
|
|
||||||
# Determine bar count from genre
|
# Build tracks and sections from genre config
|
||||||
genre_bar_map = {
|
tracks, sections = build_section_tracks(genre_config, selector, args.key, args.bpm)
|
||||||
"reggaeton": 64,
|
|
||||||
"trap": 32,
|
|
||||||
"house": 64,
|
|
||||||
"drill": 32,
|
|
||||||
}
|
|
||||||
bar_count = genre_bar_map.get(args.genre.lower(), 48)
|
|
||||||
|
|
||||||
# Build drum tracks (no selector needed)
|
# Create return tracks
|
||||||
drum_tracks = [
|
return_tracks = create_return_tracks()
|
||||||
build_drum_track("kick", "kick_main_notes", bar_count),
|
|
||||||
build_drum_track("snare", "snare_verse_notes", bar_count),
|
|
||||||
build_drum_track("hihat", "hihat_16th_notes", bar_count),
|
|
||||||
build_drum_track("perc", "perc_combo_notes", bar_count),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Build melodic tracks (selector passed only to bass)
|
# Assemble SongDefinition
|
||||||
melodic_tracks = [
|
meta = SongMeta(
|
||||||
build_melodic_track("bass", bass_tresillo, args.key, args.bpm, bar_count, selector),
|
bpm=args.bpm,
|
||||||
build_melodic_track("lead", lead_hook, args.key, args.bpm, bar_count),
|
key=args.key,
|
||||||
build_melodic_track("chords", chords_block, args.key, args.bpm, bar_count),
|
title=f"{genre_config.get('display_name', args.genre.capitalize())}",
|
||||||
build_melodic_track("pad", pad_sustain, args.key, args.bpm, bar_count),
|
time_sig_num=genre_config.get("time_signature", [4, 4])[0],
|
||||||
]
|
time_sig_den=genre_config.get("time_signature", [4, 4])[1],
|
||||||
|
ppq=genre_config.get("ppq", 96),
|
||||||
|
)
|
||||||
|
|
||||||
# Assemble full track list
|
song = SongDefinition(
|
||||||
all_tracks = drum_tracks + melodic_tracks
|
meta=meta,
|
||||||
|
tracks=tracks + return_tracks,
|
||||||
# Build SongDefinition
|
sections=sections,
|
||||||
meta = SongMeta(bpm=args.bpm, key=args.key, title=f"{args.genre.capitalize()} Track")
|
)
|
||||||
song = SongDefinition(meta=meta, tracks=all_tracks)
|
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
errors = song.validate()
|
errors = song.validate()
|
||||||
if errors:
|
if errors:
|
||||||
print(f"WARNING: SongDefinition has validation errors:", file=sys.stderr)
|
print("WARNING: SongDefinition has validation errors:", file=sys.stderr)
|
||||||
for e in errors:
|
for e in errors:
|
||||||
print(f" - {e}", file=sys.stderr)
|
print(f" - {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,25 @@ class TrackDef:
|
|||||||
send_delay: float = 0.0
|
send_delay: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SectionDef:
|
||||||
|
"""A section in the song arrangement with energy and dynamics.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: Display name (e.g. "intro", "chorus", "verse")
|
||||||
|
bars: Length in bars
|
||||||
|
energy: Energy level 0.0–1.0 (controls velocity multiplier)
|
||||||
|
velocity_mult: Velocity multiplier applied to all notes in section
|
||||||
|
vol_mult: Volume multiplier applied to track in section
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
bars: int
|
||||||
|
energy: float = 0.5
|
||||||
|
velocity_mult: float = 1.0
|
||||||
|
vol_mult: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SongDefinition:
|
class SongDefinition:
|
||||||
"""Complete song definition — the source of truth for one .rpp file.
|
"""Complete song definition — the source of truth for one .rpp file.
|
||||||
@@ -184,6 +203,7 @@ class SongDefinition:
|
|||||||
progression_name: Chord progression name (e.g. "i-VII-VI-VII")
|
progression_name: Chord progression name (e.g. "i-VII-VI-VII")
|
||||||
section_template: Section template name (default "standard")
|
section_template: Section template name (default "standard")
|
||||||
samples: Sample file map (name → filename)
|
samples: Sample file map (name → filename)
|
||||||
|
sections: Section definitions in playback order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
meta: SongMeta
|
meta: SongMeta
|
||||||
@@ -193,6 +213,7 @@ class SongDefinition:
|
|||||||
progression_name: str = "i-VII-VI-VII"
|
progression_name: str = "i-VII-VI-VII"
|
||||||
section_template: str = "standard"
|
section_template: str = "standard"
|
||||||
samples: dict[str, str] = field(default_factory=dict)
|
samples: dict[str, str] = field(default_factory=dict)
|
||||||
|
sections: list[SectionDef] = field(default_factory=list)
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Validation
|
# Validation
|
||||||
|
|||||||
@@ -14,6 +14,168 @@ from rpp import Element, dumps
|
|||||||
from ..core.schema import SongDefinition, TrackDef, ClipDef, PluginDef
|
from ..core.schema import SongDefinition, TrackDef, ClipDef, PluginDef
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ground truth constants from output/test_vst3.rpp
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#: Lines 2-92 from test_vst3.rpp — static project metadata.
|
||||||
|
#: TEMPO (line 69) is replaced dynamically in _build_element().
|
||||||
|
#: Parent elements (<NOTES>, <METRONOME>, etc.) include their children directly.
|
||||||
|
#: Plain attribute lines are simple lists.
|
||||||
|
_PROJECT_HEADER: list[list[str] | Element] = [
|
||||||
|
Element("NOTES", ["0", "2"]),
|
||||||
|
[],
|
||||||
|
["RIPPLE", "0", "0"],
|
||||||
|
["GROUPOVERRIDE", "0", "0", "0", "0"],
|
||||||
|
["AUTOXFADE", "129"],
|
||||||
|
["ENVATTACH", "3"],
|
||||||
|
["POOLEDENVATTACH", "0"],
|
||||||
|
["TCPUIFLAGS", "0"],
|
||||||
|
["MIXERUIFLAGS", "11", "48"],
|
||||||
|
["ENVFADESZ10", "40"],
|
||||||
|
["PEAKGAIN", "1"],
|
||||||
|
["FEEDBACK", "0"],
|
||||||
|
["PANLAW", "1"],
|
||||||
|
["PROJOFFS", "0", "0", "0"],
|
||||||
|
["MAXPROJLEN", "0", "0"],
|
||||||
|
["GRID", "3199", "8", "1", "8", "1", "0", "0", "0"],
|
||||||
|
["TIMEMODE", "1", "5", "-1", "30", "0", "0", "-1", "0"],
|
||||||
|
["VIDEO_CONFIG", "0", "0", "65792"],
|
||||||
|
["PANMODE", "3"],
|
||||||
|
["PANLAWFLAGS", "3"],
|
||||||
|
["CURSOR", "0"],
|
||||||
|
["ZOOM", "100", "0", "0"],
|
||||||
|
["VZOOMEX", "6", "0"],
|
||||||
|
["USE_REC_CFG", "0"],
|
||||||
|
["RECMODE", "1"],
|
||||||
|
["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"],
|
||||||
|
["LOOP", "0"],
|
||||||
|
["LOOPGRAN", "0", "4"],
|
||||||
|
["RECORD_PATH", '"Media"', '""'],
|
||||||
|
Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]),
|
||||||
|
[],
|
||||||
|
Element("APPLYFX_CFG", [], children=[]),
|
||||||
|
[],
|
||||||
|
["RENDER_FILE", '""'],
|
||||||
|
["RENDER_PATTERN", '""'],
|
||||||
|
["RENDER_FMT", "0", "2", "0"],
|
||||||
|
["RENDER_1X", "0"],
|
||||||
|
["RENDER_RANGE", "1", "0", "0", "0", "1000"],
|
||||||
|
["RENDER_RESAMPLE", "3", "0", "1"],
|
||||||
|
["RENDER_ADDTOPROJ", "0"],
|
||||||
|
["RENDER_STEMS", "0"],
|
||||||
|
["RENDER_DITHER", "0"],
|
||||||
|
["RENDER_TRIM", "0.000001", "0.000001", "0", "0"],
|
||||||
|
["TIMELOCKMODE", "1"],
|
||||||
|
["TEMPOENVLOCKMODE", "1"],
|
||||||
|
["ITEMMIX", "1"],
|
||||||
|
["DEFPITCHMODE", "589824", "0"],
|
||||||
|
["TAKELANE", "1"],
|
||||||
|
["SAMPLERATE", "44100", "0", "0"],
|
||||||
|
[],
|
||||||
|
["LOCK", "1"],
|
||||||
|
Element("METRONOME", ["6", "2"],
|
||||||
|
children=[
|
||||||
|
["VOL", "0.25", "0.125"],
|
||||||
|
["BEATLEN", "4"],
|
||||||
|
["FREQ", "1760", "880", "1"],
|
||||||
|
["SAMPLES", "", "", "", ""],
|
||||||
|
["SPLIGNORE", "0", "0"],
|
||||||
|
["SPLDEF", "2", "660", "", "0", ""],
|
||||||
|
["SPLDEF", "3", "440", "", "0", ""],
|
||||||
|
["PATTERN", "0", "169"],
|
||||||
|
["PATTERNSTR", "ABBB"],
|
||||||
|
["MULT", "1"],
|
||||||
|
]),
|
||||||
|
[],
|
||||||
|
["GLOBAL_AUTO", "-1"],
|
||||||
|
# TEMPO line is injected dynamically — do not include static entry
|
||||||
|
["PLAYRATE", "1", "0", "0.25", "4"],
|
||||||
|
["SELECTION", "0", "0"],
|
||||||
|
["SELECTION2", "0", "0"],
|
||||||
|
["MASTERAUTOMODE", "0"],
|
||||||
|
["MASTERTRACKHEIGHT", "0", "0"],
|
||||||
|
["MASTERPEAKCOL", "16576"],
|
||||||
|
["MASTERMUTESOLO", "0"],
|
||||||
|
["MASTERTRACKVIEW", "0", "0.6667", "0.5", "0.5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"],
|
||||||
|
["MASTERHWOUT", "0", "0", "1", "0", "0", "0", "0", "-1"],
|
||||||
|
["MASTER_NCH", "2", "2"],
|
||||||
|
["MASTER_VOLUME", "1", "0", "-1", "-1", "1"],
|
||||||
|
["MASTER_PANMODE", "3"],
|
||||||
|
["MASTER_PANLAWFLAGS", "3"],
|
||||||
|
["MASTER_FX", "1"],
|
||||||
|
["MASTER_SEL", "0"],
|
||||||
|
Element("MASTERPLAYSPEEDENV", [],
|
||||||
|
children=[
|
||||||
|
["EGUID", "{DEF87440-E07C-4B72-B9F8-D2AC60A0D0AC}"],
|
||||||
|
["ACT", "0", "-1"],
|
||||||
|
["VIS", "0", "1", "1"],
|
||||||
|
["LANEHEIGHT", "0", "0"],
|
||||||
|
["ARM", "0"],
|
||||||
|
["DEFSHAPE", "0", "-1", "-1"],
|
||||||
|
]),
|
||||||
|
[],
|
||||||
|
Element("TEMPOENVEX", [],
|
||||||
|
children=[
|
||||||
|
["EGUID", "{15E58A72-7149-4783-9A04-838503786012}"],
|
||||||
|
["ACT", "1", "-1"],
|
||||||
|
["VIS", "1", "0", "1"],
|
||||||
|
["LANEHEIGHT", "0", "0"],
|
||||||
|
["ARM", "0"],
|
||||||
|
["DEFSHAPE", "1", "-1", "-1"],
|
||||||
|
]),
|
||||||
|
[],
|
||||||
|
["RULERHEIGHT", "86", "86"],
|
||||||
|
["RULERLANE", "1", "4", "", "0", "-1"],
|
||||||
|
["RULERLANE", "2", "8", "", "0", "-1"],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
|
||||||
|
#: Default attributes for every TRACK — from test_vst3.rpp lines 108-131.
|
||||||
|
_TRACK_DEFAULTS: list[list[str]] = [
|
||||||
|
["PEAKCOL", "16576"],
|
||||||
|
["BEAT", "-1"],
|
||||||
|
["AUTOMODE", "0"],
|
||||||
|
["PANLAWFLAGS", "3"],
|
||||||
|
["VOLPAN", "1", "0", "-1", "-1", "1"],
|
||||||
|
["MUTESOLO", "0", "0", "0"],
|
||||||
|
["IPHASE", "0"],
|
||||||
|
["PLAYOFFS", "0", "1"],
|
||||||
|
["ISBUS", "0", "0"],
|
||||||
|
["BUSCOMP", "0", "0", "0", "0", "0"],
|
||||||
|
["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"],
|
||||||
|
["FIXEDLANES", "9", "0", "0", "0", "0"],
|
||||||
|
["LANEREC", "-1", "-1", "-1", "0"],
|
||||||
|
["SEL", "0"],
|
||||||
|
["REC", "0", "0", "1", "0", "0", "0", "0", "0"],
|
||||||
|
["VU", "64"],
|
||||||
|
["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"],
|
||||||
|
["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"],
|
||||||
|
["NCHAN", "2"],
|
||||||
|
["FX", "1"],
|
||||||
|
["TRACKID", ""], # filled dynamically with same GUID as TRACK opening
|
||||||
|
["PERF", "0"],
|
||||||
|
["MIDIOUT", "-1"],
|
||||||
|
["MAINSEND", "1", "0"],
|
||||||
|
]
|
||||||
|
|
||||||
|
#: FXCHAIN header metadata — from test_vst3.rpp lines 133-137 and 159-162.
|
||||||
|
_FXCHAIN_HEADER: list[list[str]] = [
|
||||||
|
["WNDRECT", "24", "52", "655", "408"],
|
||||||
|
["SHOW", "0"],
|
||||||
|
["LASTSEL", "0"],
|
||||||
|
["DOCKED", "0"],
|
||||||
|
["BYPASS", "0", "0", "0"],
|
||||||
|
]
|
||||||
|
|
||||||
|
#: FXCHAIN footer metadata — from test_vst3.rpp lines 159-162.
|
||||||
|
_FXCHAIN_FOOTER: list[list[str]] = [
|
||||||
|
["PRESETNAME", "Program 1"],
|
||||||
|
["FLOATPOS", "0", "0", "0", "0"],
|
||||||
|
["FXID", ""], # filled dynamically
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -23,6 +185,40 @@ def _make_guid() -> str:
|
|||||||
return str(uuid.uuid4()).upper()
|
return str(uuid.uuid4()).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def vst3_element(display_name: str, filename: str, uid_guid: str = "", preset_data: list[str] | None = None) -> Element:
|
||||||
|
"""Build a VST3 Element for REAPER .rpp.
|
||||||
|
|
||||||
|
REAPER format (from real .rpp files):
|
||||||
|
<VST "VST3: PluginName (Vendor)" filename.vst3 0 "" uniqueid{GUID} "">
|
||||||
|
preset_line_1
|
||||||
|
preset_line_2
|
||||||
|
...
|
||||||
|
|
||||||
|
The uniqueid{GUID} is extracted from REAPER's reaper-vstplugins64.ini:
|
||||||
|
Filename.vst3=hash,uniqueid{GUID,DisplayName!!!Type}
|
||||||
|
|
||||||
|
REAPER REQUIRES base64 preset data inside VST blocks for VST3 plugins to load.
|
||||||
|
Without preset data, plugins show as "not available" even with correct name/filename/GUID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
display_name: Full REAPER display name, e.g. "VST3: Serum 2 (Xfer Records)"
|
||||||
|
filename: Plugin filename, e.g. "Serum2.vst3"
|
||||||
|
uid_guid: uniqueid{GUID} string from REAPER scan, e.g. "691258006{56534558...}"
|
||||||
|
preset_data: Optional list of base64 preset lines to include as children.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Element('VST', [display_name, filename, '0', '', uid_guid, '']) with preset lines as children
|
||||||
|
"""
|
||||||
|
if uid_guid:
|
||||||
|
elem = Element("VST", [display_name, filename, "0", "", uid_guid, ""])
|
||||||
|
else:
|
||||||
|
elem = Element("VST", [display_name, filename, "0", ""])
|
||||||
|
if preset_data:
|
||||||
|
for line in preset_data:
|
||||||
|
elem.append(line)
|
||||||
|
return elem
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# RPPBuilder
|
# RPPBuilder
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -47,23 +243,51 @@ class RPPBuilder:
|
|||||||
OSError: If the file cannot be written.
|
OSError: If the file cannot be written.
|
||||||
"""
|
"""
|
||||||
root = self._build_element()
|
root = self._build_element()
|
||||||
|
content = dumps(root)
|
||||||
|
# CRITICAL 1: quote the version string in the header
|
||||||
|
# rpp library produces <REAPER_PROJECT 0.1 7.65/win64 ...> but REAPER needs quotes
|
||||||
|
content = content.replace('<REAPER_PROJECT 0.1 7.65/win64', '<REAPER_PROJECT 0.1 "7.65/win64"')
|
||||||
p = Path(path)
|
p = Path(path)
|
||||||
p.write_text(dumps(root), encoding="utf-8")
|
p.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
def _build_element(self) -> Element:
|
def _build_element(self) -> Element:
|
||||||
"""Build the Element tree for the .rpp file."""
|
"""Build the Element tree for the .rpp file."""
|
||||||
m = self.song.meta
|
m = self.song.meta
|
||||||
|
|
||||||
# Project root
|
# Project root — version from test_vst3.rpp line 1
|
||||||
root = Element("REAPER_PROJECT", ["0.1", "6.0", str(int(uuid.uuid4().time))])
|
root = Element("REAPER_PROJECT", ["0.1", "7.65/win64", str(int(uuid.uuid4().time)), "0"])
|
||||||
|
|
||||||
# TEMPO is a flat attribute line, NOT a child element
|
# Add all static project header lines
|
||||||
|
for line in _PROJECT_HEADER:
|
||||||
|
if line is not None: # preserve all Elements (even empty) and non-empty lists
|
||||||
|
root.append(line)
|
||||||
|
|
||||||
|
# TEMPO is injected dynamically (overrides static header)
|
||||||
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den)])
|
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den)])
|
||||||
|
|
||||||
# Master track
|
# Master track
|
||||||
master = Element("TRACK", [_make_guid()])
|
master_guid = _make_guid()
|
||||||
|
master = Element("TRACK", [master_guid])
|
||||||
master.append(['NAME', "master"])
|
master.append(['NAME', "master"])
|
||||||
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
|
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
|
||||||
|
for line in _TRACK_DEFAULTS:
|
||||||
|
if line:
|
||||||
|
defaults_copy = [v for v in line]
|
||||||
|
if defaults_copy[0] == "TRACKID":
|
||||||
|
defaults_copy[1] = f"{{{master_guid}}}"
|
||||||
|
master.append(defaults_copy)
|
||||||
|
|
||||||
|
# Master track FXCHAIN (MASTER_FX 1 requires FXCHAIN)
|
||||||
|
master_fxchain = Element("FXCHAIN", [])
|
||||||
|
for line in _FXCHAIN_HEADER:
|
||||||
|
master_fxchain.append([v for v in line])
|
||||||
|
for line in _FXCHAIN_FOOTER:
|
||||||
|
if line:
|
||||||
|
footer_copy = [v for v in line]
|
||||||
|
if footer_copy[0] == "FXID":
|
||||||
|
footer_copy[1] = f"{{{_make_guid()}}}"
|
||||||
|
master_fxchain.append(footer_copy)
|
||||||
|
master.append(master_fxchain)
|
||||||
root.append(master)
|
root.append(master)
|
||||||
|
|
||||||
# User tracks
|
# User tracks
|
||||||
@@ -73,35 +297,425 @@ class RPPBuilder:
|
|||||||
return root
|
return root
|
||||||
|
|
||||||
def _build_track(self, track: TrackDef) -> Element:
|
def _build_track(self, track: TrackDef) -> Element:
|
||||||
"""Build a TRACK Element."""
|
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
|
||||||
track_elem = Element("TRACK", [_make_guid()])
|
track_guid = _make_guid()
|
||||||
|
track_elem = Element("TRACK", [f"{{{track_guid}}}"])
|
||||||
track_elem.append(["NAME", track.name])
|
track_elem.append(["NAME", track.name])
|
||||||
|
|
||||||
vol = track.volume
|
# Default attributes
|
||||||
pan = track.pan
|
for line in _TRACK_DEFAULTS:
|
||||||
track_elem.append([f"VOLPAN", f"{vol:.6f}", f"{pan:.6f}", "-1", "-1", "1"])
|
if line:
|
||||||
|
defaults_copy = [v for v in line]
|
||||||
|
if defaults_copy[0] == "TRACKID":
|
||||||
|
defaults_copy[1] = f"{{{track_guid}}}"
|
||||||
|
elif defaults_copy[0] == "VOLPAN":
|
||||||
|
vol = track.volume
|
||||||
|
pan = track.pan
|
||||||
|
defaults_copy = [f"VOLPAN", f"{vol:.6f}", f"{pan:.6f}", "-1", "-1", "1"]
|
||||||
|
elif defaults_copy[0] == "SEL":
|
||||||
|
defaults_copy = ["SEL", "1"] # user track is selected by default
|
||||||
|
track_elem.append(defaults_copy)
|
||||||
|
|
||||||
if track.color != 0:
|
# Override NCHAN based on track configuration
|
||||||
track_elem.append(["COLOR", str(track.color)])
|
# Find and update NCHAN if already set
|
||||||
|
nchan_found = False
|
||||||
|
for i, child in enumerate(track_elem.children):
|
||||||
|
if isinstance(child, list) and child[0] == "NCHAN":
|
||||||
|
child[1] = "2"
|
||||||
|
nchan_found = True
|
||||||
|
break
|
||||||
|
|
||||||
# Plugins (FXCHAIN)
|
# Plugins (FXCHAIN) — wrap VST elements inside proper FXCHAIN structure
|
||||||
if track.plugins:
|
if track.plugins:
|
||||||
fxchain = Element("FXCHAIN", [])
|
fxchain = Element("FXCHAIN", [])
|
||||||
|
for line in _FXCHAIN_HEADER:
|
||||||
|
fxchain.append([v for v in line])
|
||||||
for plugin in track.plugins:
|
for plugin in track.plugins:
|
||||||
fxchain.append(self._build_plugin(plugin))
|
fxchain.append(self._build_plugin(plugin))
|
||||||
|
fxid_guid = _make_guid()
|
||||||
|
fxchain.append(["PRESETNAME", "Program 1"])
|
||||||
|
fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
||||||
|
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
||||||
track_elem.append(fxchain)
|
track_elem.append(fxchain)
|
||||||
|
|
||||||
|
# Send effects
|
||||||
|
if track.send_reverb > 0:
|
||||||
|
track_elem.append(["AUXRECV", "0", f"{track.send_reverb:.6f}", "-1", "-1", "0"])
|
||||||
|
if track.send_delay > 0:
|
||||||
|
track_elem.append(["AUXRECV", "1", f"{track.send_delay:.6f}", "-1", "-1", "0"])
|
||||||
|
|
||||||
# Clips (items)
|
# Clips (items)
|
||||||
for clip in track.clips:
|
for clip in track.clips:
|
||||||
track_elem.append(self._build_clip(clip))
|
track_elem.append(self._build_clip(clip))
|
||||||
|
|
||||||
return track_elem
|
return track_elem
|
||||||
|
|
||||||
|
# VST3 plugin registry: short name → (display_name, filename_on_disk, uid{GUID})
|
||||||
|
# display_name and uid{GUID} from REAPER's reaper-vstplugins64.ini scan.
|
||||||
|
# filename_on_disk is the ACTUAL .vst3 filename as it exists on disk
|
||||||
|
# (with spaces, matching what REAPER writes in .rpp files).
|
||||||
|
VST3_REGISTRY: dict[str, tuple[str, str, str]] = {
|
||||||
|
"Serum2": (
|
||||||
|
"VST3: Serum 2 (Xfer Records)",
|
||||||
|
"Serum2.vst3",
|
||||||
|
"691258006{56534558667350736572756D20320000}",
|
||||||
|
),
|
||||||
|
"Omnisphere": (
|
||||||
|
"VST3: Omnisphere (Spectrasonics)",
|
||||||
|
"Omnisphere.vst3",
|
||||||
|
"103502701{84E8DE5F9255222296FAE4133C935A18}",
|
||||||
|
),
|
||||||
|
"FabFilter Pro-Q 3": (
|
||||||
|
"VST3: Pro-Q 3 (FabFilter)",
|
||||||
|
"FabFilter Pro-Q 3.vst3",
|
||||||
|
"756089518{72C4DB717A4D459AB97E51745D84B39D}",
|
||||||
|
),
|
||||||
|
"FabFilter Pro-C 2": (
|
||||||
|
"VST3: Pro-C 2 (FabFilter)",
|
||||||
|
"FabFilter Pro-C 2.vst3",
|
||||||
|
"1000537396{79F415E3C8E74807AD5DA3CF7024F618}",
|
||||||
|
),
|
||||||
|
"FabFilter Pro-R 2": (
|
||||||
|
"VST3: Pro-R 2 (FabFilter)",
|
||||||
|
"FabFilter Pro-R 2.vst3",
|
||||||
|
"585842631{6070873C802A4B078FC06AB5459154E9}",
|
||||||
|
),
|
||||||
|
"FabFilter Pro-L 2": (
|
||||||
|
"VST3: Pro-L 2 (FabFilter)",
|
||||||
|
"FabFilter Pro-L 2.vst3",
|
||||||
|
"1938458649{AFD92F729A0447B7B5E8D1D568DEA985}",
|
||||||
|
),
|
||||||
|
"FabFilter Saturn 2": (
|
||||||
|
"VST3: Saturn 2 (FabFilter)",
|
||||||
|
"FabFilter Saturn 2.vst3",
|
||||||
|
"1437095695{8D067533D8A0491DBAA36C064C6ABBFB}",
|
||||||
|
),
|
||||||
|
"FabFilter Timeless 3": (
|
||||||
|
"VST3: Timeless 3 (FabFilter)",
|
||||||
|
"FabFilter Timeless 3.vst3",
|
||||||
|
"2123585227{D2EE67F2C552402D902115931AFDAE6B}",
|
||||||
|
),
|
||||||
|
"The Glue": (
|
||||||
|
"VST3: The Glue (Cytomic)",
|
||||||
|
"The Glue.vst3",
|
||||||
|
"336504517{5653544379546774686520676C756500}",
|
||||||
|
),
|
||||||
|
"Valhalla Delay": (
|
||||||
|
"VST3: ValhallaDelay (Valhalla DSP, LLC)",
|
||||||
|
"ValhallaDelay.vst3",
|
||||||
|
"1674641571{565354644C617976616C68616C6C6164}",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# VST3 preset data — base64-encoded state blocks for each plugin.
|
||||||
|
# REAPER REQUIRES these preset lines inside VST blocks for VST3 plugins to load.
|
||||||
|
# Without preset data, plugins show as "not available" even with correct name/filename/GUID.
|
||||||
|
VST3_PRESETS: dict[str, list[str]] = {
|
||||||
|
"Serum2": [
|
||||||
|
"Z4R+ae5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAbQgAAAEAAAAAAAAA",
|
||||||
|
"zQQAAAEAAABYZmVySnNvbgC5AAAAAAAAAHsiY29tcG9uZW50IjoicHJvY2Vzc29yIiwiaGFzaCI6IjgxZTEyMWYxNGI2Y2IyYjA2YzMzMjQzZDk1ZDIxYWIxIiwicHJv",
|
||||||
|
"ZHVjdCI6IlNlcnVtMkZYIiwicHJvZHVjdFZlcnNpb24iOiIyLjAuMjIiLCJ1cmwiOiJodHRwczovL3hmZXJyZWNvcmRzLmNvbS8iLCJ2ZW5kb3IiOiJYZmVyIFJlY29y",
|
||||||
|
"ZHMiLCJ2ZXJzaW9uIjo4LjB96RgAAAIAAAAotS/9YOkXjR8AFjCaPFBr2gbodialcV1VbJoVgFuWWaflcoTUerVxc8k2222dIo/JNba20TbOHBIjAvyim4hIZLuYBkOj",
|
||||||
|
"Cn3dAo0AhgCNAJU243xMKYKe59pf2A+LmqTo/x96nrO24cwi/iPX+RFIteCpSYqg57kmc1uJ6dwYcHI4kGpBpK3xFIFrFDIqjYxjgS3JqK7n2rssy7I6IgZCuKZJYAGi",
|
||||||
|
"+eVmFxbKcakH4nUJTSMWapIi6HlOztkwdlIYM46d9MLeCplp1pbHC2N9O1JnU2u54NhbETMZAk392iaBAOxY3wrL6gSAqdfa8gLLqgWz4dSKHZQHFu2bodsJrTi1WE9m",
|
||||||
|
"ptnjIYbcplnti4B2OPdzUzohhliklM9FLXIiSU1SBD0PclCDHEhSkxRBz/Me97THeSQ1SRH0nveee+05T1KTFDnIPe4555pznKQmKYKe5+Zs+ekk/kNSkxRBz5stmc7K",
|
||||||
|
"4XwHEfQ81646c/YS6rnz5Pp1zZtd8vcr/XDHmsFsIpqIJqKx4LQhdg7n+oSZa2cNJMwz8hBDNLLJlXk+5MxWBT9jVoH9rbKCDRomcMAdCoS4n2FXXrPskXI8PJ/BzDzn",
|
||||||
|
"nHOutdZaw8AgwHNLcOXSESaw25YTCQwK7FSjKRJuMmCD8uApMnfELzMHTdNUgKg/L4nEdSuD3o/VLxYF9nACx/vQ73Vrg4KCALY5ywAb9GZQAv1IrICvzhHz1C8R4/lr",
|
||||||
|
"zODbl3MTzoSz6k983UHNqByWXZ/IDLP4115OAL9qS2J1C7cYBA+xHnCLVZprWvUv3GKzZpSKh6vO6xeIBT619zF2+NfOXL2+xR5Orx2xtIDlqMF0SERERCQpSFHSGEAj",
|
||||||
|
"kdkkHRLAUBiOwhAEQiCGYAglEQRBQAhmGlKCRUHN1UALEAczXz0gMEUMUg/I72Ha2X9lejEDdYR8KCu6i1QeBbuujg2PwzCHKfqM9VBSFBV7pMakjpL7SBcfeyuyPWYt",
|
||||||
|
"Cqw2yq3Id0jtT4kyOfYBHn7MBg4N6MWKARqYdIBRWEyo/VDzLp6K6f//makXGDLmd6CJvrRh7hF1+DVp8yXYKda8mvdtEh4MhwaiuUhrvQMBOS5SsQvD4++3eY8239eA",
|
||||||
|
"ae/p9fDb20eb7+dDu6/Xg8+v6yNgx01KhM8JMMEARg5VQTM2ajybsmMaNgU0dqsgywJ8Gq6pwH0xRwMpnIIA0qth5Hvg2QYF+OQZFwguah8wXodmcN3SEE18Amsu4KMa",
|
||||||
|
"jAPN4RXSjPlHjS5Y6MHBwwcLDGHkcFhrOWnbJKLUtgZP5zLRdzspCZtTajRtv5Zihl0n02exI+U3WW9mpXWTZY9OhGwkh2cHYoWmfCnGb2Fb8DC0GpADAAAAAAAAWGZl",
|
||||||
|
"ckpzb24A/QAAAAAAAAB7ImNvbXBvbmVudCI6ImNvbnRyb2xsZXIiLCJoYXNoIjoiYjg0YWYyZGUzOTA2MTM0NWVmNThkOTIzMDE4MjE2MjYiLCJwcmVzZXRBdXRob3Ii",
|
||||||
|
"OiIiLCJwcmVzZXREZXNjcmlwdGlvbiI6IiIsInByZXNldE5hbWUiOiIgLSBJbml0IC0gIiwicHJvZHVjdCI6IlNlcnVtMkZYIiwicHJvZHVjdFZlcnNpb24iOiIyLjAu",
|
||||||
|
"MjIiLCJ1cmwiOiJodHRwczovL3hmZXJyZWNvcmRzLmNvbS8iLCJ2ZW5kb3IiOiJYZmVyIFJlY29yZHMiLCJ2ZXJzaW9uIjo4LjB9SgcAAAIAAAAotS/9YEoGhRMA5h5p",
|
||||||
|
"OlBnnQMg2JmZmZnFzkQkPGZWs9ULakKkJC1FrrZjbdNK7pRrTLjfNDTBGtHcgdX3/V4On2ZNcaBmqu9aAFsAVgB8eQz0zIPp+0q3bJcF6o41JZbVusaZKi4y52MJaX1j",
|
||||||
|
"y84uRVHUyVIUAwInUvvXv+lLNVJSDp3sm46fN5gGTgAo8V1iuRmFE0kE8nA0Lqvy0Aqr6rJAfYpIwUGXO5+Ub+45Q6KibHp0mcVZyuj1KCSJBtFiMcSdth7NfH8DwJa1",
|
||||||
|
"48yv9DIznEgikIejcVnVKLTCyjXP+MREEoE8HI3LmqgwCwLtenViEnwqnwkiWuxZepP/wk0PHuW27a/yW0r6d9I4urMnrR2N7jLaCjd5oT4/QseDAJo6npY9edPRqQP3",
|
||||||
|
"dOuPPQHACIhoMcl/Y9eMw7RnuqDUB24EpuHSed+ZSJsM9/lmtNNRqsBO6gEURVFOX4ij+N4+8qk2+9sYHY0Lqz2N/d8byGAo2HqvCAAo34vXtagW1zap22+H+bFcN3oa",
|
||||||
|
"/RpSJCiuT4wiQTHqjjWjB1pf1xhm94JlAxZU/aPtygPoqegGWajBUKVMMDUTjARa0koHsCIqKtEDEgAycQqpemQCmmCCCSZM/o9piwHErCirVFLbEQqK8BhwC5MOQ/9H",
|
||||||
|
"0Yu+Iufi/Gt5t3SEzGHkM36itgXehxZmejg2h+br8O549C7nKLQAF4mkv+8A2Lc4kLZstX3rg5rS1Hi5O50i7rY6xtru7d4G4lWW7ulsMU50EzAHTkc5t0siNKeYo8yP",
|
||||||
|
"FoXiDr5ojXGIoH/6Azwj9ipFuaVeNO8IeHFxVyzVtVp4D8y6TcbLZLS7ic4H",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"Omnisphere": [
|
||||||
|
"nefcOe5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAwxIAAAEAAAAAAAAA",
|
||||||
|
"sxIAAAEAAAD/yZo7AAAAAAEAAAAAAAAAeBIAAAAAAAA8U3ludGhNYXN0ZXIgdmVycz0iMy4wLjFjIj4KPEVOVFJZREVTQ1IgbmFtZT0iIiBsaWJyYXJ5PSIiIEFUVFJJ",
|
||||||
|
"Ql9WQUxVRV9EQVRBPSIiPgo8L0VOVFJZREVTQ1I+CiA8U3ludGhNYXN0ZXJFbmdpbmVQYXJhbUJsb2NrPgo8TWFzdGVyRW5naW5lQmFzZVBhcmFtQmxvY2sgc2NhbGVO",
|
||||||
|
"YW1lPSIiIEhXcHJvZmlsZT0iIiB2ZXJzaW9uPSIxIiAgaW5MZXZlbD0iM2Y0MDAwMDAiICBnYWluPSIzZjQwMDAwMCIgIG1hc3Rlck1peD0iM2Y4MDAwMDAiICBtYXN0",
|
||||||
|
"ZXJCeXA9IjAiICBhdXRvTGRQYXRjaD0iMCIgIHBhbmljPSIwIiAgdHVuVj0iNDNkYzAwMDAiICBwUGFuMD0iM2YwMDAwMDAiICBwUGFuMT0iM2YwMDAwMDAiICBwUGFu",
|
||||||
|
"Mj0iM2YwMDAwMDAiICBwUGFuMz0iM2YwMDAwMDAiICBwUGFuND0iM2YwMDAwMDAiICBwUGFuNT0iM2YwMDAwMDAiICBwUGFuNj0iM2YwMDAwMDAiICBwUGFuNz0iM2Yw",
|
||||||
|
"MDAwMDAiICBwTGV2ZWwwPSIzZjQwMDAwMCIgIHBMZXZlbDE9IjNmNDAwMDAwIiAgcExldmVsMj0iM2Y0MDAwMDAiICBwTGV2ZWwzPSIzZjQwMDAwMCIgIHBMZXZlbDQ9",
|
||||||
|
"IjNmNDAwMDAwIiAgcExldmVsNT0iM2Y0MDAwMDAiICBwTGV2ZWw2PSIzZjQwMDAwMCIgIHBMZXZlbDc9IjNmNDAwMDAwIiAgcExhdGNoMD0iMCIgIHBMYXRjaDE9IjAi",
|
||||||
|
"ICBwTGF0Y2gyPSIwIiAgcExhdGNoMz0iMCIgIHBMYXRjaDQ9IjAiICBwTGF0Y2g1PSIwIiAgcExhdGNoNj0iMCIgIHBMYXRjaDc9IjAiICBwVHJpZ2dlcjA9IjAiICBw",
|
||||||
|
"VHJpZ2dlcjE9IjAiICBwVHJpZ2dlcjI9IjAiICBwVHJpZ2dlcjM9IjAiICBwVHJpZ2dlcjQ9IjAiICBwVHJpZ2dlcjU9IjAiICBwVHJpZ2dlcjY9IjAiICBwVHJpZ2dl",
|
||||||
|
"cjc9IjAiICBwU3VzRW4wPSIzZjgwMDAwMCIgIHBTdXNFbjE9IjNmODAwMDAwIiAgcFN1c0VuMj0iM2Y4MDAwMDAiICBwU3VzRW4zPSIzZjgwMDAwMCIgIHBTdXNFbjQ9",
|
||||||
|
"IjNmODAwMDAwIiAgcFN1c0VuNT0iM2Y4MDAwMDAiICBwU3VzRW42PSIzZjgwMDAwMCIgIHBTdXNFbjc9IjNmODAwMDAwIiAgcE11dGUwPSIwIiAgcE11dGUxPSIwIiAg",
|
||||||
|
"cE11dGUyPSIwIiAgcE11dGUzPSIwIiAgcE11dGU0PSIwIiAgcE11dGU1PSIwIiAgcE11dGU2PSIwIiAgcE11dGU3PSIwIiAgcFNvbG8wPSIwIiAgcFNvbG8xPSIwIiAg",
|
||||||
|
"cFNvbG8yPSIwIiAgcFNvbG8zPSIwIiAgcFNvbG80PSIwIiAgcFNvbG81PSIwIiAgcFNvbG82PSIwIiAgcFNvbG83PSIwIiAgcEdBdHRlbjA9IjAiICBwR0F0dGVuMT0i",
|
||||||
|
"MCIgIHBHQXR0ZW4yPSIwIiAgcEdBdHRlbjM9IjAiICBwR0F0dGVuND0iMCIgIHBHQXR0ZW41PSIwIiAgcEdBdHRlbjY9IjAiICBwR0F0dGVuNz0iMCIgIHAwQXV4U25k",
|
||||||
|
"MD0iMCIgIHAwQXV4U25kMT0iMCIgIHAwQXV4U25kMj0iMCIgIHAwQXV4U25kMz0iMCIgIHAxQXV4U25kMD0iMCIgIHAxQXV4U25kMT0iMCIgIHAxQXV4U25kMj0iMCIg",
|
||||||
|
"IHAxQXV4U25kMz0iMCIgIHAyQXV4U25kMD0iMCIgIHAyQXV4U25kMT0iMCIgIHAyQXV4U25kMj0iMCIgIHAyQXV4U25kMz0iMCIgIHAzQXV4U25kMD0iMCIgIHAzQXV4",
|
||||||
|
"U25kMT0iMCIgIHAzQXV4U25kMj0iMCIgIHAzQXV4U25kMz0iMCIgIHA0QXV4U25kMD0iMCIgIHA0QXV4U25kMT0iMCIgIHA0QXV4U25kMj0iMCIgIHA0QXV4U25kMz0i",
|
||||||
|
"MCIgIHA1QXV4U25kMD0iMCIgIHA1QXV4U25kMT0iMCIgIHA1QXV4U25kMj0iMCIgIHA1QXV4U25kMz0iMCIgIHA2QXV4U25kMD0iMCIgIHA2QXV4U25kMT0iMCIgIHA2",
|
||||||
|
"U25kMj0iMCIgIHA2QXV4U25kMz0iMCIgIHA3QXV4U25kMD0iMCIgIHA3QXV4U25kMT0iMCIgIHA3QXV4U25kMj0iMCIgIHA3QXV4U25kMz0iMCIgIG91dDA9IjAi",
|
||||||
|
"ICBvdXQxPSIwIiAgb3V0Mj0iMCIgIG91dDM9IjAiICBvdXQ0PSIwIiAgb3V0NT0iMCIgIG91dDY9IjAiICBvdXQ3PSIwIiAgY2hhbjA9IjAiICBjaGFuMT0iMSIgIGNo",
|
||||||
|
"YW4yPSIyIiAgY2hhbjM9IjMiICBjaGFuND0iNCIgIGNoYW41PSI1IiAgY2hhbjY9IjYiICBjaGFuNz0iNyIgIG1nMD0iMCIgIG1nMT0iMCIgIG1nMj0iMCIgIG1nMz0i",
|
||||||
|
"MCIgIG1nND0iMCIgIG1nNT0iMCIgIG1nNj0iMCIgIG1nNz0iMCIgIHNwbjA9IjEiICBzcG4xPSIxIiAgc3BuMj0iMSIgIHNwbjM9IjEiICBzcG40PSIxIiAgc3BuNT0i",
|
||||||
|
"MSIgIHNwbjY9IjEiICBzcG43PSIxIiAgYnJvd3NlVXA9IjAiICBicm93c2VEbj0iMCIgIE9iamVjdFN0ZXA9ImJmODAwMDAwIiAgRmlsdGVyU3RlcDA9IjAiICBGaWx0",
|
||||||
|
"ZXJTdGVwMT0iMCIgIEZpbHRlclN0ZXAyPSIwIiAgRmlsdGVyU3RlcDM9IjAiICBGaWx0ZXJTdGVwND0iMCIgPgo8TUlESUVYUFJFU1NJT04gTXBlT25PZmY9IjAiICBN",
|
||||||
|
"cGVCZW5kUmFuZ2U9IjQ4IiAgTWlkaVNtb290aFJpc2UwPSIzZWRlYjg1MiIgIE1pZGlTbW9vdGhSaXNlMT0iM2YwMDAwMDAiICBNaWRpU21vb3RoUmlzZTI9IjAiICBN",
|
||||||
|
"aWRpU21vb3RoUmlzZTM9IjNmMDAwMDAwIiAgTWlkaVNtb290aFJpc2U0PSIzZjAwMDAwMCIgIE1pZGlTbW9vdGhSaXNlNT0iM2YwMDAwMDAiICBNaWRpU21vb3RoUmlz",
|
||||||
|
"ZTY9IjAiICBNaWRpU21vb3RoUmlzZTc9IjAiICBNaWRpU21vb3RoUmlzZTg9IjAiICBNaWRpU21vb3RoUmlzZTk9IjAiICBNaWRpU21vb3RoUmlzZTEwPSIwIiAgTWlk",
|
||||||
|
"aVNtb290aFJpc2UxMT0iMCIgIE1pZGlTbW9vdGhSaXNlMTI9IjNmMDAwMDAwIiAgTWlkaVNtb290aFJpc2UxMz0iM2YwMDAwMDAiICBNaWRpU21vb3RoRmFsbDA9IjNl",
|
||||||
|
"ZGViODUyIiAgTWlkaVNtb290aEZhbGwxPSIzZjAwMDAwMCIgIE1pZGlTbW9vdGhGYWxsMj0iMCIgIE1pZGlTbW9vdGhGYWxsMz0iM2YwMDAwMDAiICBNaWRpU21vb3Ro",
|
||||||
|
"RmFsbDQ9IjNmMDAwMDAwIiAgTWlkaVNtb290aEZhbGw1PSIzZjAwMDAwMCIgIE1pZGlTbW9vdGhGYWxsNj0iMCIgIE1pZGlTbW9vdGhGYWxsNz0iMCIgIE1pZGlTbW9v",
|
||||||
|
"dGhGYWxsOD0iMCIgIE1pZGlTbW9vdGhGYWxsOT0iMCIgIE1pZGlTbW9vdGhGYWxsMTA9IjAiICBNaWRpU21vb3RoRmFsbDExPSIwIiAgTWlkaVNtb290aEZhbGwxMj0i",
|
||||||
|
"M2YwMDAwMDAiICBNaWRpU21vb3RoRmFsbDEzPSIzZjAwMDAwMCIgPgo8L01JRElFWFBSRVNTSU9OPgogPE1FZmZSYWNrIFByZXNldD0iUmFjayBQcmVzZXRzIj4KPEVG",
|
||||||
|
"Rk1PRFVMRSBUeXBlPSJObyBFZmZlY3QiIFAwPSIwIiAgUDE9IjAiICBQMj0iMCIgIFAzPSIwIiAgUDQ9IjAiICBQNT0iMCIgIFA2PSIwIiAgUDc9IjAiICBQOD0iMCIg",
|
||||||
|
"IFA5PSIwIiAgUDEwPSIwIiAgUDExPSIwIiAgUDEyPSIwIiAgUDEzPSIwIiAgUDE0PSIwIiAgQWN0aXZlPSIwIiAgTWl4TG9jaz0iMCIgPgo8L0VGRk1PRFVMRT4KIDxF",
|
||||||
|
"RkZNT0RVTEUgVHlwZT0iTm8gRWZmZWN0IiBQMD0iMCIgIFAxPSIwIiAgUDI9IjAiICBQMz0iMCIgIFA0PSIwIiAgUDU9IjAiICBQNj0iMCIgIFA3PSIwIiAgUDg9IjAi",
|
||||||
|
"ICBQOT0iMCIgIFAxMD0iMCIgIFAxMT0iMCIgIFAxMj0iMCIgIFAxMz0iMCIgIFAxND0iMCIgIEFjdGl2ZT0iMCIgIE1peExvY2s9IjAiID4KPC9FRkZNT0RVTEU+CiA8",
|
||||||
|
"RUZGTU9EVUxFIFR5cGU9Ik5vIEVmZmVjdCIgUDA9IjAiICBQMT0iMCIgIFAyPSIwIiAgUDM9IjAiICBQND0iMCIgIFA1PSIwIiAgUDY9IjAiICBQNz0iMCIgIFA4PSIw",
|
||||||
|
"IiAgUDk9IjAiICBQMTA9IjAiICBQMTE9IjAiICBQMTI9IjAiICBQMTM9IjAiICBQMTQ9IjAiICBBY3RpdmU9IjAiICBNaXhMb2NrPSIwIiA+CjwvRUZGTU9EVUxFPgog",
|
||||||
|
"PEVGRk1PRFVMRSBUeXBlPSJObyBFZmZlY3QiIFAwPSIwIiAgUDE9IjAiICBQMj0iMCIgIFAzPSIwIiAgUDQ9IjAiICBQNT0iMCIgIFA2PSIwIiAgUDc9IjAiICBQOD0i",
|
||||||
|
"MCIgIFA5PSIwIiAgUDEwPSIwIiAgUDExPSIwIiAgUDEyPSIwIiAgUDEzPSIwIiAgUDE0PSIwIiAgQWN0aXZlPSIwIiAgTWl4TG9jaz0iMCIgPgo8L0VGRk1PRFVMRT4K",
|
||||||
|
"IDwvTUVmZlJhY2s+CiA8QUVmZlJhY2swPgo8RUZGTU9EVUxFIFR5cGU9Ik5vIEVmZmVjdCIgUDA9IjAiICBQMT0iMCIgIFAyPSIwIiAgUDM9IjAiICBQND0iMCIgIFA1",
|
||||||
|
"PSIwIiAgUDY9IjAiICBQNz0iMCIgIFA4PSIwIiAgUDk9IjAiICBQMTA9IjAiICBQMTE9IjAiICBQMTI9IjAiICBQMTM9IjAiICBQMTQ9IjAiICBBY3RpdmU9IjAiICBN",
|
||||||
|
"aXhMb2NrPSIwIiA+CjwvRUZGTU9EVUxFPgogPEVGRk1PRFVMRSBUeXBlPSJObyBFZmZlY3QiIFAwPSIwIiAgUDE9IjAiICBQMj0iMCIgIFAzPSIwIiAgUDQ9IjAiICBQ",
|
||||||
|
"NT0iMCIgIFA2PSIwIiAgUDc9IjAiICBQOD0iMCIgIFA5PSIwIiAgUDEwPSIwIiAgUDExPSIwIiAgUDEyPSIwIiAgUDEzPSIwIiAgUDE0PSIwIiAgQWN0aXZlPSIwIiAg",
|
||||||
|
"TWl4TG9jaz0iMCIgPgo8L0VGRk1PRFVMRT4KIDxFRkZNT0RVTEUgVHlwZT0iTm8gRWZmZWN0IiBQMD0iMCIgIFAxPSIwIiAgUDI9IjAiICBQMz0iMCIgIFA0PSIwIiAg",
|
||||||
|
"UDU9IjAiICBQNj0iMCIgIFA3PSIwIiAgUDg9IjAiICBQOT0iMCIgIFAxMD0iMCIgIFAxMT0iMCIgIFAxMj0iMCIgIFAxMz0iMCIgIFAxND0iMCIgIEFjdGl2ZT0iMCIg",
|
||||||
|
"IE1peExvY2s9IjAiID4KPC9FRkZNT0RVTEU+CiA8RUZGTU9EVUxFIFR5cGU9Ik5vIEVmZmVjdCIgUDA9IjAiICBQMT0iMCIgIFAyPSIwIiAgUDM9IjAiICBQND0iMCIg",
|
||||||
|
"IFA1PSIwIiAgUDY9IjAiICBQNz0iMCIgIFA4PSIwIiAgUDk9IjAiICBQMTA9IjAiICBQMTE9IjAiICBQMTI9IjAiICBQMTM9IjAiICBQMTQ9IjAiICBBY3RpdmU9IjAi",
|
||||||
|
"ICBNaXhMb2NrPSIwIiA+CjwvRUZGTU9EVUxFPgogPC9BRWZmUmFjazA+CiA8L01hc3RlckVuZ2luZUJhc2VQYXJhbUJsb2NrPgogPC9TeW50aE1hc3RlckVuZ2luZVBh",
|
||||||
|
"cmFtQmxvY2s+CiA8TUlESWxlYXJuMj4KPC9NSURJbGVhcm4yPgogPC9TeW50aE1hc3Rlcj4KIAAAAAAAAAAAAAAAAAAAAAAAAAAAAEpVQ0VQcml2YXRlRGF0YQAAAAAA",
|
||||||
|
"AAAA",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Pro-Q 3": [
|
||||||
|
"rgIRLe5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAAoGAAABAAAAAAAAAA==",
|
||||||
|
"sAUAAAEAAABGRkJTAQAAAGYBAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/",
|
||||||
|
"AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAP9pzH0EAAAAA",
|
||||||
|
"AAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/",
|
||||||
|
"2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAA",
|
||||||
|
"AAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABA",
|
||||||
|
"AACAPwAAAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAA",
|
||||||
|
"AACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/",
|
||||||
|
"AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAA",
|
||||||
|
"AACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9B",
|
||||||
|
"AAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAA",
|
||||||
|
"AACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/",
|
||||||
|
"AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAP9pzH0EAAAAAAAAAAAAAgD8AAIA/AAAAPwAAAAAAAIA/",
|
||||||
|
"AAAAQAAAgD8AAAAAAAAAAAAAgD/acx9BAAAAAAAAAAAAAIA/AACAPwAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAAAAAIA/2nMfQQAAAAAAAAAAAACAPwAAgD8AAAA/",
|
||||||
|
"AAAAAAAAgD8AAABAAACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgL8AAIA/AAAAQAAAAEAAAEBAAAAAAAAAgD8AAIA/",
|
||||||
|
"AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAARkZwcgEAAAAAAAAASgAAAAAAAABGUTNwAwAAAA8AAABEZWZhdWx0IFNldHRpbmf/////AQAAAAcAAABUcmFjayAxAAAAAEN1U1YBAAAAAAAAAEZG",
|
||||||
|
"ZWQAAAAAAACAPw==",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Pro-C 2": [
|
||||||
|
"NP2iO+5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAP8AAAABAAAAAAAAAA==",
|
||||||
|
"4wAAAAEAAABGYWJGAgAAAA8AAABEZWZhdWx0IFNldHRpbmcAAAAALgAAAAAAAAAAAJDBmpkZPwAAkEEAAHBCzczMPbaN0j4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAgL8A",
|
||||||
|
"AAAAAACAPwAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAHia1EAAAEBAAACAPwAAgD/acx9BAAAAAAAAAD8AAAAAAAAAAHiaREEAAEBAAAAAAAAAAAAAAIA/AAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAACAPwAAgD8BAAAAAQAAAAwAAAAAAAAARkZlZAAAAAAAAIA/",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Pro-R 2": [
|
||||||
|
"xz/rIu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAASwMAAAEAAAAAAAAA",
|
||||||
|
"OAIAAAEAAABGRkJTAQAAAIgAAAAAAAA/AAAAAAAAAD8AAAA/AAAAAJqZmT4AAAAAMzMzPwAAAADIAbRBAAAAAAAAAAAAAHpDAAAAAAAAAAAAAAAAJCeEPQAAAAAAAAAA",
|
||||||
|
"AACAPwAAgD9/CnpA0H/UvnzDuz4AAIA/AACAPwAAgD8AAIA/EqcxQUgPwb7MiIk+AAAAAAAAgD8AAAAAAAAAANpzH0EAAAAAAAAAPwAAAAAAAIA/AAAAAAAAAADacx9B",
|
||||||
|
"AAAAAAAAAD8AAAAAAACAPwAAAAAAAAAA2nMfQQAAAAAAAAA/AAAAAAAAgD8AAAAAAAAAANpzH0EAAAAAAAAAPwAAAAAAAIA/AAAAAAAAAADacy9BAAAAwCsNGD8AAAAA",
|
||||||
|
"AACAPwAAAEAAAIA/AACAPwAAgD/cz1hBAAAAwAAAAD8AAIBAAACAPwAAAEAAAIA/AACAPwAAgD8+qRNBAACQwKvlzz4AAAAAAACAPwAAAEAAAIA/AAAAAAAAAADacx9B",
|
||||||
|
"AAAAAAAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAADacx9BAAAAAAAAAD8AAAAAAACAPwAAAEAAAIA/AAAAAAAAAADacx9BAAAAAAAAAD8AAAAAAACAPwAAAEAAAIA/",
|
||||||
|
"AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAAM3MTD/NzEw/AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAgD8AAAAARkZwcgEAAAAAAAAA",
|
||||||
|
"AwEAAAAAAABGUjJwAwAAAA8AAABEZWZhdWx0IFNldHRpbmf/////AQAAAAAAAAAAAAAAQ3VTVgEAAAADAAAABgAAAEFVVEhPUgkAAABGYWJGaWx0ZXILAAAAREVTQ1JJ",
|
||||||
|
"UFRJT053AAAAVGhpcyBpcyB0aGUgZGVmYXVsdCBwcmVzZXQgZm9yIFByby1SIDIsIHdoaWNoIGlzIGxvYWRlZCBmb3IgZXZlcnkgbmV3IGluc3RhbmNlLgoKRmVlbCBm",
|
||||||
|
"cmVlIHRvIGN1c3RvbWl6ZSBpdCBhcyB5b3UgbGlrZSEEAAAAVEFHUxMAAABkZWZhdWx0LGhhbGwsbWVkaXVtRkZlZAAAAAAAAIA/",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Pro-L 2": [
|
||||||
|
"GYiKc+5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAMcAAAABAAAAAAAAAA==",
|
||||||
|
"qwAAAAEAAABGYWJGAgAAAA8AAABEZWZhdWx0IFNldHRpbmcAAAAAIAAAAAAAAAAAAKBA7FE4PqR/0D5+jcY+AADAPgAAAD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAEAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAEAAAIA/AAAAAAAAAAAAAGDBAAAAAAAAgD8AAAAAAQAAAAEAAAAMAAAAAAAAAEZGZWQA",
|
||||||
|
"AAAAAACAPw==",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Saturn 2": [
|
||||||
|
"D1eoVe5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAEcPAAABAAAAAAAAAA==",
|
||||||
|
"9A4AAAEAAABGRkJTAQAAALcDAAAAAAAAAAAAAAMAgL8AAAAAAAAAAAAAyEIAAIA/AAAAANPn/kAK16M8AADAQM3MTD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAyEIAAAAA",
|
||||||
|
"AAAAAAAAgD8AAAAAPE2qQAAAAEAAAAAAtef+QAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIQgAAAAAAAAAAAACAPwAAAAA8TapAAAAAQAAAAAC15/5A",
|
||||||
|
"AAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhCAAAAAAAAAAAAAIA/AAAAADxNqkAAAABAAAAAALXn/kAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAyEIAAAAAAAAAAAAAgD8AAAAAPE2qQAAAAEAAAAAAtef+QAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIQgAAAAAAAAAAAACAPwAAAAA8TapA",
|
||||||
|
"AAAAQAAAAAC15/5AAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhCAAAAAAAAAAAAAIA/AAAAADxNqkAAAABAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAA",
|
||||||
|
"AAAAAAAAgD8AAIA/AAAAAAAAAAAAAIA/AACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAIA/AAAAAAAAAAAAAIA/AACAPwAAAAC+9H0+AAAAAAAAAD8AAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvvR9PgAAAAAAAAA/",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL70fT4AAAAA",
|
||||||
|
"AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+9H0+",
|
||||||
|
"AAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/AAAAAAAAAAAAAAA/AAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/",
|
||||||
|
"AAAAAAAAAAAAAAA/AAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/AAAAAAAAAAAAAAA/AAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/",
|
||||||
|
"AAAAAAAAAAAAAAA/AAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/AAAAAAAAAAAAAAA/AAAAAAAAAAAaPY8+AAAAAAAAAAAAAAA/AAAAAAAAAD8AAIA/",
|
||||||
|
"AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAABGRnByAQAAAAAAAABDAAAA",
|
||||||
|
"AAAAAEZTMmEDAAAADwAAAERlZmF1bHQgU2V0dGluZ/////8BAAAAAAAAAAAAAABDdVNWAQAAAAAAAABGRmVkAAAAAAAAgD8=",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"FabFilter Timeless 3": [
|
||||||
|
"y1aTfu5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAJMQAAABAAAAAAAAAA==",
|
||||||
|
"1A8AAAEAAABGRkJTAQAAAO8DAACBMJY+ADJrPBrAFT8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAAAACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/",
|
||||||
|
"AAAAPwAAAAAAAAAAAAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAAAACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/AAAAPwAAAAAAAAAAAAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAA",
|
||||||
|
"AACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/AAAAPwAAAAAAAAAAAAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAAAACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/AAAAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAgD8AAAA/AAAAAAAAAAAAAAAAAACAPwAAAD8AAAAAAAAAAAAAAAAAAIA/AAAAPwAAAAAAAAAAAACAPwAAgD8AAAAAMzOzPgAAAAAAAAAAAAAAAAAAAAB4mvRA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAgQQAAgD8AAIA/AACAPwAAgD94mkRBAAAAAAAAAADAzEw9AAAgQQAAAAAAAABAAACAPwAAgD88TQpBAAAAwQAAAACEPbW+AAAgQQAAgEAAAIA/",
|
||||||
|
"AACAPwAAAAC1poFBAAAAAAAAAAAAAAAAAAAgQQAAAAAAAIA/AACAPwAAAAC1poFBAAAAAAAAAAAAAAAAAAAgQQAAAAAAAIA/AACAPwAAAAC1poFBAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAgQQAAAAAAAIA/AACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAzMzMvgAAAAAAAABA",
|
||||||
|
"AAAAANDMTD4AAAAAAAAAAAAAAADQzEw+AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AAAAAAAAAAAAAIA/AACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAIA/",
|
||||||
|
"AAAAQA6kXj4AAAAAAAAAPwAAAABmZmY/AAAAAAAAAAAAAAAAAACAPwAAAAB9ZXo/AABAQAAAgD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAADpB04+AAAAAAAAAD8AAAAAAPBJPgAAAAAAAAAAAAAAAAAAgEEAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAH5qLD8AAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA4JETPgAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAJgsjPwAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA",
|
||||||
|
"AAAAAAAAgD8AAAAAvvR9PgAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAL70fT4AAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAC+9H0+AAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvvR9PgAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABo9jz4AAAAAAAAAAAAAAD8AAAAAAAAAPwAAgD8AAAAAAAAAAAAAAD8AAAAAAAAAABo9jz4AAAAA",
|
||||||
|
"AAAAAAAAAD8AAAAAAAAAPwAAgD8AAAAAAAAAAAAAAD8AAAAAAAAAABo9jz4AAAAAAAAAAAAAAD8AAAAAAAAAPwAAgD8AAAAAAAAAAAAAAD8AAAAAAAAAABo9jz4AAAAA",
|
||||||
|
"AAAAAAAAAD8AAAAAAAAAPwAAgD8AAAAAAAAAAAAAAD8AAAAAAAAAABo9jz4AAAAAAAAAAAAAAD8AAAAAAAAAPwAAgD8AAAAAAAAAAAAAAD8AAIA/AAAAAAAAAAAh1Ec+dd2pPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAACBBAADIQQAAAAAAAAAAAACAPwAAqkIAAABA",
|
||||||
|
"cT0KPwAAAAAAAAAAAAA+QwAAUEEAAAAAAAAAAAAAAAAAAABAAACAQAAAgD4AAAAAAAAAAAAAQEMAAFBBAAAAAAAAAAAAAAAAAACAPwAAgEAAADA+AAAAAAAAAAAAAEJD",
|
||||||
|
"AABgQQAAAAAAAAAAAAAAAAAAQkMAAGBBAAAAAAAAAAAAAAAAAABAQwAAgEAAACA+AAAAAAAAAAAAAEVDAACAQAAAAD4AAAAAAAAAAAAAREMAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAEZGcHIBAAAAAAAAAK8AAAAAAAAARjNUcwMAAAAPAAAARGVmYXVsdCBTZXR0aW5n/////wEAAAAAAAAAAAAAAEN1U1YBAAAABgAAAAMAAABF",
|
||||||
|
"RjEIAAAARW52ZWxvcGUDAAAARUYyAAAAAAUAAABYTEZPMQYAAABSYW5kb20FAAAAWExGTzIGAAAAV29iYmxlAwAAAFhZMQcAAABEdWNraW5nAwAAAFhZMgsAAABJbnN0",
|
||||||
|
"YWJpbGl0eUZGZWQAAAAAAACAPw==",
|
||||||
|
"AFByb2dyYW0gMQAAAAAA",
|
||||||
|
],
|
||||||
|
"The Glue": [
|
||||||
|
"xaYOFO5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAJMIAAABAAAA//8AAA==",
|
||||||
|
"gwgAAAEAAABWc3RXAAAACAAAAAEAAAAAQ2NuSwAACGtGQkNoAAAAAkN5VGcAAQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH01ZDMiGrBwAA",
|
||||||
|
"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4gPEN5dG9taWMgUHJvZHVjdD0iVGhlIEdsdWUiIFZlcnNpb249IjEuOC4wIj48U29uZ1ByZXNldCBW",
|
||||||
|
"ZXJzaW9uPSIxLjguMCI+PFN0YXRlIFZlcnNpb249IjEuOC4wIj48VHVwbGUgS2V5PSJVaVNjYWxlIiBWYWx1ZT0iMSIvPjxUdXBsZSBLZXk9IkhkUmVuZGVyIiBWYWx1",
|
||||||
|
"ZT0iZmFsc2UiLz48VHVwbGUgS2V5PSJEZXRlY3RTaWxlbmNlIiBWYWx1ZT0idHJ1ZSIvPjxUdXBsZSBLZXk9Ik92ZXJTYW1wbGVSZWFsdGltZSIgVmFsdWU9IngyIi8+",
|
||||||
|
"PFR1cGxlIEtleT0iT3ZlclNhbXBsZVJlbmRlciIgVmFsdWU9Ing4Ii8+PFR1cGxlIEtleT0iT3ZlclNhbXBsZVR5cGVVcCIgVmFsdWU9IkxpblBoYXNlIi8+PFR1cGxl",
|
||||||
|
"IEtleT0iT3ZlclNhbXBsZVR5cGVEbiIgVmFsdWU9IkxpblBoYXNlIi8+PFR1cGxlIEtleT0iQ2hlY2tJbyIgVmFsdWU9ImZhbHNlIi8+PFR1cGxlIEtleT0iQ3VycmVu",
|
||||||
|
"dFByZXNldCIgVmFsdWU9IjAiLz48VHVwbGUgS2V5PSJHdWlTdGF0ZSIgVmFsdWU9IjE3Nzc4MjUxMDYyMDkiLz48VHVwbGUgS2V5PSJTaG93UGVha05lZWRsZSIgVmFs",
|
||||||
|
"dWU9ImZhbHNlIi8+PC9TdGF0ZT48UHJlc2V0IE5hbWU9IkZhY3RvcnkgRGVmYXVsdCIgVmVyc2lvbj0iMS44LjAiPjxQYXJhbWV0ZXJzPjxUdXBsZSBLZXk9IlBvd2Vy",
|
||||||
|
"IiBWYWx1ZT0iWWVzIi8+PFR1cGxlIEtleT0iVGhyZXNob2xkIiBWYWx1ZT0iMC4wMDAwMDAwMGRCIi8+PFR1cGxlIEtleT0iTWFrZXVwIiBWYWx1ZT0iMC4wMDAwMDAw",
|
||||||
|
"MGRCIi8+PFR1cGxlIEtleT0iUmFuZ2UiIFZhbHVlPSJGdWxsIi8+PFR1cGxlIEtleT0iQXR0YWNrIiBWYWx1ZT0iMSBtUyIvPjxUdXBsZSBLZXk9IlJlbGVhc2UiIFZh",
|
||||||
|
"bHVlPSIwLjYgUyIvPjxUdXBsZSBLZXk9IlJhdGlvIiBWYWx1ZT0iNCIvPjxUdXBsZSBLZXk9IkNvbXBJbiIgVmFsdWU9IlllcyIvPjxUdXBsZSBLZXk9IldldE1peCIg",
|
||||||
|
"VmFsdWU9IjEwMC4wMDAwMDAwMCUiLz48VHVwbGUgS2V5PSJQZWFrQ2xpcEluIiBWYWx1ZT0iTm8iLz48VHVwbGUgS2V5PSJTaWRlY2hhaW5IcCIgVmFsdWU9Ik9mZiIv",
|
||||||
|
"PjxUdXBsZSBLZXk9IkV4dFNpZGVjaGFpbkluIiBWYWx1ZT0iTm8iLz48VHVwbGUgS2V5PSJEY0Jsb2NrSW8iIFZhbHVlPSJObyIvPjxUdXBsZSBLZXk9IkJ5cGFzcyIg",
|
||||||
|
"VmFsdWU9Ik5vIi8+PFR1cGxlIEtleT0iVnVNZXRlciIgVmFsdWU9IjAuMDAwMDA0NzdkQiIvPjxUdXBsZSBLZXk9IlN0ZXJlb0xpbmsiIFZhbHVlPSIxMDAgJSIvPjwv",
|
||||||
|
"UGFyYW1ldGVycz48L1ByZXNldD48UHJlc2V0IE5hbWU9IkZhY3RvcnkgRGVmYXVsdCIgVmVyc2lvbj0iMS44LjAiPjxQYXJhbWV0ZXJzPjxUdXBsZSBLZXk9IlBvd2Vy",
|
||||||
|
"IiBWYWx1ZT0iWWVzIi8+PFR1cGxlIEtleT0iVGhyZXNob2xkIiBWYWx1ZT0iMC4wMDAwMDAwMGRCIi8+PFR1cGxlIEtleT0iTWFrZXVwIiBWYWx1ZT0iMC4wMDAwMDAw",
|
||||||
|
"MGRCIi8+PFR1cGxlIEtleT0iUmFuZ2UiIFZhbHVlPSJGdWxsIi8+PFR1cGxlIEtleT0iQXR0YWNrIiBWYWx1ZT0iMSBtUyIvPjxUdXBsZSBLZXk9IlJlbGVhc2UiIFZh",
|
||||||
|
"bHVlPSIwLjYgUyIvPjxUdXBsZSBLZXk9IlJhdGlvIiBWYWx1ZT0iNCIvPjxUdXBsZSBLZXk9IkNvbXBJbiIgVmFsdWU9IlllcyIvPjxUdXBsZSBLZXk9IldldE1peCIg",
|
||||||
|
"VmFsdWU9IjEwMC4wMDAwMDAwMCUiLz48VHVwbGUgS2V5PSJQZWFrQ2xpcEluIiBWYWx1ZT0iTm8iLz48VHVwbGUgS2V5PSJTaWRlY2hhaW5IcCIgVmFsdWU9Ik9mZiIv",
|
||||||
|
"PjxUdXBsZSBLZXk9IkV4dFNpZGVjaGFpbkluIiBWYWx1ZT0iTm8iLz48VHVwbGUgS2V5PSJEY0Jsb2NrSW8iIFZhbHVlPSJObyIvPjxUdXBsZSBLZXk9IkJ5cGFzcyIg",
|
||||||
|
"VmFsdWU9Ik5vIi8+PFR1cGxlIEtleT0iVnVNZXRlciIgVmFsdWU9IjAuMDAwMDAwMDBkQiIvPjxUdXBsZSBLZXk9IlN0ZXJlb0xpbmsiIFZhbHVlPSIxMDAgJSIvPjwv",
|
||||||
|
"UGFyYW1ldGVycz48L1ByZXNldD48L1NvbmdQcmVzZXQ+PC9DeXRvbWljPgAAAAAAAAAAAAAAAAAAAAAASlVDRVByaXZhdGVEYXRhAAAAAAAAAAA=",
|
||||||
|
"AAAAAAAA",
|
||||||
|
],
|
||||||
|
"Valhalla Delay": [
|
||||||
|
"owDRY+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAVQQAAAEAAAD//wAA",
|
||||||
|
"RQQAAAEAAABWc3RXAAAACAAAAAEAAAAAQ2NuSwAABC1GQkNoAAAAAmRMYXkAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADlVZDMiFQAwAA",
|
||||||
|
"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4gPFZhbGhhbGxhRGVsYXkgcGx1Z2luVmVyc2lvbj0iMy4wLjB2MTQiIHByZXNldE5hbWU9IkRlZmF1",
|
||||||
|
"bHQiIE1peD0iMC41IiBEZWxheVN0eWxlPSIwLjAiIERlbGF5TFN5bmM9IjAuMjUiIERlbGF5TE5vdGU9IjAuMjAwMDAwMDAyOTgwMjMyMiIgRGVsYXlMX01zPSIwLjMw",
|
||||||
|
"MDAwMDAxMTkyMDkyOSIgRGVsYXlSU3luYz0iMC4yNSIgRGVsYXlSTm90ZT0iMC4yMDAwMDAwMDI5ODAyMzIyIiBEZWxheVJfTXM9IjAuMzAwMDAwMDExOTIwOTI5IiBE",
|
||||||
|
"ZWxheVNwcmVhZD0iMC41IiBEZWxheVNwYWNpbmc9IjAuNSIgRGVsYXlSYXRpbz0iMC42MTQxNDE0MDQ2Mjg3NTM3IiBSZXBlYXRzU3dlbGQ9IjEuMCIgVGFwQj0iMS4w",
|
||||||
|
"IiBUYXBDPSIxLjAiIFRhcEQ9IjEuMCIgRmVlZGJhY2s9IjAuMzQ5OTk5OTk0MDM5NTM1NSIgV2lkdGg9IjEuMCIgRHJpdmVJbj0iMC4wIiBBZ2U9IjAuNSIgRGlmZnVz",
|
||||||
|
"aW9uPSIwLjAiIERpZmZTaXplPSIxLjAiIExvd0N1dD0iMC4wIiBIaWdoQ3V0PSIxLjAiIE1vZFJhdGU9IjAuMjczODM0MTA5MzA2MzM1NCIgTW9kRGVwdGg9IjAuNSIg",
|
||||||
|
"V293cz0iMC41IiBGbHV0dGVyPSIwLjUiIEZyZXFTaGlmdD0iMC41IiBGcmVxRGV0dW5lPSIwLjU3OTk5OTk4MzMxMDY5OTUiIFBpdGNoU2hpZnQ9IjAuNSIgUGl0Y2gk",
|
||||||
|
"RGV0dW5lPSIwLjUiIE1vZGU9IjAuMDQxNjY2Njc3OTA4NDMwMSIgRXJhPSIwLjMzMzMzMzMzNDMyNjc0NDA4IiBEdWNraW5nPSIwLjAiIFJlc2VydmVkMj0iMC4wIiBS",
|
||||||
|
"ZXNlcnZlZDM9IjAuMCIgUmVzZXJ2ZWQ0PSIwLjAiIG1peExvY2s9IjAiIHVpV2lkdGg9Ijk0NSIgdWlIZWlnaHQ9IjQzNSIvPgAAAAAAAAAAABKVUNFUHJpdmF0ZURh",
|
||||||
|
"dGEAAQFCeXBhc3MAAQEDAB0AAAAAAAAASlVDRVByaXZhdGVEYXRhAAAAAAAAAAA=",
|
||||||
|
"AAAAAAAA",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
def _build_plugin(self, plugin: PluginDef) -> Element:
|
def _build_plugin(self, plugin: PluginDef) -> Element:
|
||||||
"""Build a VST Element inside FXCHAIN."""
|
"""Build a VST Element inside FXCHAIN.
|
||||||
params_str = " ".join(str(v) for v in plugin.params.values()) if plugin.params else ""
|
|
||||||
vst = Element("VST", [plugin.name, plugin.path, str(plugin.index), "", *params_str.split(), "0", "0"])
|
VST3: <VST "VST3: PluginName (Vendor)" filename.vst3 0 "" uid{GUID} "">
|
||||||
return vst
|
[preset_data_lines...]
|
||||||
|
VST2: <VST "VST: PluginName (Cockos)" filename.dll 0 "" uid{GUID} "">
|
||||||
|
"""
|
||||||
|
# VST3 plugins — identified by .vst3 extension
|
||||||
|
if plugin.path.endswith(".vst3"):
|
||||||
|
entry = self.VST3_REGISTRY.get(plugin.name)
|
||||||
|
if entry:
|
||||||
|
display_name, filename, uid_guid = entry
|
||||||
|
preset_data = self.VST3_PRESETS.get(plugin.name)
|
||||||
|
return vst3_element(display_name, filename, uid_guid, preset_data)
|
||||||
|
# Fallback: match by filename against registry entries
|
||||||
|
for registry_entry in self.VST3_REGISTRY.values():
|
||||||
|
_, reg_filename, uid_guid = registry_entry
|
||||||
|
if reg_filename == plugin.path:
|
||||||
|
display_name = plugin.name if plugin.name.startswith("VST3:") else f"VST3: {plugin.name}"
|
||||||
|
preset_data = self.VST3_PRESETS.get(plugin.name)
|
||||||
|
return vst3_element(display_name, plugin.path, uid_guid, preset_data)
|
||||||
|
# Final fallback: use plugin.name as-is
|
||||||
|
display_name = plugin.name if plugin.name.startswith("VST3:") else f"VST3: {plugin.name}"
|
||||||
|
return vst3_element(display_name, plugin.path)
|
||||||
|
|
||||||
|
# Built-in VST2 plugins (ReaEQ, ReaComp, etc.) — .dll format
|
||||||
|
dll_map = {
|
||||||
|
"ReaEQ": "reaeq.dll",
|
||||||
|
"ReaComp": "reacomp.dll",
|
||||||
|
"ReaVerbate": "reaverbate.dll",
|
||||||
|
"ReaDelay": "readelay.dll",
|
||||||
|
"ReaCast": "reacast.dll",
|
||||||
|
"ReaFIR": "reafir.dll",
|
||||||
|
"ReaGate": "reagate.dll",
|
||||||
|
"ReaLimit": "realimit.dll",
|
||||||
|
"ReaPitch": "reapitch.dll",
|
||||||
|
"ReaVerb": "reaverb.dll",
|
||||||
|
"ReaXComp": "reaxcomp.dll",
|
||||||
|
}
|
||||||
|
dll_name = dll_map.get(plugin.name, plugin.path)
|
||||||
|
param_slots = ["0"] * 19
|
||||||
|
return Element("VST", [plugin.name, dll_name, "0", "", *param_slots])
|
||||||
|
|
||||||
def _build_clip(self, clip: ClipDef) -> Element:
|
def _build_clip(self, clip: ClipDef) -> Element:
|
||||||
"""Build an ITEM Element."""
|
"""Build an ITEM Element."""
|
||||||
|
|||||||
@@ -25,50 +25,33 @@ def compose_via_builder(
|
|||||||
|
|
||||||
This lets us test the compose logic without hitting the filesystem for samples.
|
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.rhythm import get_notes
|
||||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||||
|
|
||||||
genre_bar_map = {"reggaeton": 64, "trap": 32, "house": 64, "drill": 32}
|
genre_path = _ROOT / "knowledge" / "genres" / f"{genre.lower()}_2009.json"
|
||||||
bar_count = genre_bar_map.get(genre.lower(), 48)
|
with open(genre_path, "r", encoding="utf-8") as f:
|
||||||
|
genre_config = json.load(f)
|
||||||
|
|
||||||
# Drum tracks
|
from scripts.compose import (
|
||||||
drum_tracks = []
|
build_section_tracks, create_return_tracks, EFFECT_ALIASES,
|
||||||
for role, generator_name in [
|
build_fx_chain, build_sampler_plugin,
|
||||||
("kick", "kick_main_notes"),
|
)
|
||||||
("snare", "snare_verse_notes"),
|
from src.selector import SampleSelector
|
||||||
("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]))
|
|
||||||
|
|
||||||
# Melodic tracks (no selector — audio_path stays None)
|
index_path = _ROOT / "data" / "sample_index.json"
|
||||||
for role, generator_fn in [
|
selector = SampleSelector(str(index_path))
|
||||||
("bass", bass_tresillo),
|
|
||||||
("lead", lead_hook),
|
tracks, sections = build_section_tracks(genre_config, selector, key, bpm)
|
||||||
("chords", chords_block),
|
return_tracks = create_return_tracks()
|
||||||
("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]))
|
|
||||||
|
|
||||||
meta = SongMeta(bpm=bpm, key=key, title=f"{genre.capitalize()} Track")
|
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"
|
assert output.exists(), f"Expected {output} to exist"
|
||||||
|
|
||||||
def test_compose_rpp_has_min_4_tracks(self, tmp_path):
|
def test_compose_rpp_has_min_6_tracks(self, tmp_path):
|
||||||
"""The .rpp output contains at least 4 <TRACK blocks."""
|
"""The .rpp output contains at least 6 <TRACK blocks (roles + 2 returns)."""
|
||||||
output = tmp_path / "track.rpp"
|
output = tmp_path / "track.rpp"
|
||||||
|
|
||||||
with patch("scripts.compose.SampleSelector") as mock_selector_cls:
|
with patch("scripts.compose.SampleSelector") as mock_selector_cls:
|
||||||
@@ -131,7 +114,35 @@ class TestComposeRppOutput:
|
|||||||
|
|
||||||
content = output.read_text(encoding="utf-8")
|
content = output.read_text(encoding="utf-8")
|
||||||
track_count = content.count("<TRACK")
|
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):
|
def test_compose_invalid_bpm_raises(self):
|
||||||
"""main() with bpm=0 raises ValueError."""
|
"""main() with bpm=0 raises ValueError."""
|
||||||
@@ -168,3 +179,45 @@ class TestComposeRppOutput:
|
|||||||
main()
|
main()
|
||||||
finally:
|
finally:
|
||||||
sys.argv = original_argv
|
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"
|
||||||
@@ -7,7 +7,7 @@ sys.path.insert(0, str(Path(__file__).parents[1]))
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tempfile
|
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
|
from src.reaper_builder import RPPBuilder
|
||||||
|
|
||||||
|
|
||||||
@@ -174,3 +174,272 @@ class TestRPPBuilderMasterTrack:
|
|||||||
assert "NAME master" in content
|
assert "NAME master" in content
|
||||||
finally:
|
finally:
|
||||||
Path(tmp_path).unlink(missing_ok=True)
|
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)
|
||||||
|
|||||||
209
tests/test_section_builder.py
Normal file
209
tests/test_section_builder.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
"""Tests for section builder — SectionDef, build_fx_chain, effect alias mapping."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from src.core.schema import SectionDef, PluginDef
|
||||||
|
|
||||||
|
|
||||||
|
class TestSectionDef:
|
||||||
|
"""Test SectionDef dataclass."""
|
||||||
|
|
||||||
|
def test_section_def_instantiation(self):
|
||||||
|
"""SectionDef creates with name, bars, energy."""
|
||||||
|
section = SectionDef(name="chorus", bars=8, energy=0.9)
|
||||||
|
assert section.name == "chorus"
|
||||||
|
assert section.bars == 8
|
||||||
|
assert section.energy == 0.9
|
||||||
|
# velocity_mult and vol_mult default to 1.0 (not derived from energy)
|
||||||
|
assert section.velocity_mult == 1.0
|
||||||
|
assert section.vol_mult == 1.0
|
||||||
|
|
||||||
|
def test_section_def_default_energy(self):
|
||||||
|
"""SectionDef defaults energy to 0.5, velocity_mult/vol_mult to 1.0."""
|
||||||
|
section = SectionDef(name="verse", bars=8)
|
||||||
|
assert section.energy == 0.5
|
||||||
|
assert section.velocity_mult == 1.0
|
||||||
|
assert section.vol_mult == 1.0
|
||||||
|
|
||||||
|
def test_section_def_custom_mults(self):
|
||||||
|
"""SectionDef accepts custom velocity_mult and vol_mult via __init__ args."""
|
||||||
|
section = SectionDef(
|
||||||
|
name="intro", bars=4, energy=0.3,
|
||||||
|
velocity_mult=0.4, vol_mult=0.6
|
||||||
|
)
|
||||||
|
assert section.velocity_mult == 0.4
|
||||||
|
assert section.vol_mult == 0.6
|
||||||
|
|
||||||
|
|
||||||
|
class TestVST3Effects:
|
||||||
|
"""Test VST3 premium plugin mappings."""
|
||||||
|
|
||||||
|
def test_vst3_effects_defined(self):
|
||||||
|
"""_VST3_EFFECTS maps effect names to VST3 plugins."""
|
||||||
|
from scripts.compose import _VST3_EFFECTS
|
||||||
|
assert "Pro-Q 3" in _VST3_EFFECTS
|
||||||
|
assert "Pro-C 2" in _VST3_EFFECTS
|
||||||
|
assert "Pro-R 2" in _VST3_EFFECTS
|
||||||
|
assert "Timeless 3" in _VST3_EFFECTS
|
||||||
|
|
||||||
|
def test_fruity_eq_maps_to_proq3(self):
|
||||||
|
"""Fruity Parametric EQ 2 → FabFilter Pro-Q 3 via normalization."""
|
||||||
|
from scripts.compose import _VST3_EFFECTS
|
||||||
|
# Fruity Parametric EQ 2 normalizes to Pro-Q 3
|
||||||
|
registry_key, filename = _VST3_EFFECTS["Pro-Q 3"]
|
||||||
|
assert registry_key == "FabFilter Pro-Q 3"
|
||||||
|
assert filename == "FabFilter Pro-Q 3.vst3"
|
||||||
|
|
||||||
|
def test_fruity_compressor_maps_to_proc2(self):
|
||||||
|
"""Fruity Compressor → FabFilter Pro-C 2 via normalization."""
|
||||||
|
from scripts.compose import _VST3_EFFECTS
|
||||||
|
registry_key, filename = _VST3_EFFECTS["Pro-C 2"]
|
||||||
|
assert registry_key == "FabFilter Pro-C 2"
|
||||||
|
assert filename == "FabFilter Pro-C 2.vst3"
|
||||||
|
|
||||||
|
def test_pro_r_maps_to_pror2(self):
|
||||||
|
"""Pro-R 2 → FabFilter Pro-R 2."""
|
||||||
|
from scripts.compose import _VST3_EFFECTS
|
||||||
|
registry_key, filename = _VST3_EFFECTS["Pro-R 2"]
|
||||||
|
assert registry_key == "FabFilter Pro-R 2"
|
||||||
|
assert filename == "FabFilter Pro-R 2.vst3"
|
||||||
|
|
||||||
|
def test_unknown_effect_returns_none(self):
|
||||||
|
"""Unknown effect names return no VST3 info."""
|
||||||
|
from scripts.compose import _VST3_EFFECTS
|
||||||
|
assert _VST3_EFFECTS.get("Some Unknown Plugin") is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildFxChain:
|
||||||
|
"""Test build_fx_chain function."""
|
||||||
|
|
||||||
|
def test_build_fx_chain_drums(self):
|
||||||
|
"""build_fx_chain returns PluginDef list for drums role."""
|
||||||
|
from scripts.compose import build_fx_chain
|
||||||
|
|
||||||
|
genre_config = {
|
||||||
|
"mix": {
|
||||||
|
"per_role": {
|
||||||
|
"drums": {
|
||||||
|
"effects": ["Fruity Parametric EQ 2", "Fruity Compressor"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins = build_fx_chain("drums", genre_config, [])
|
||||||
|
assert len(plugins) == 2
|
||||||
|
# Fruity Parametric EQ 2 → Pro-Q 3
|
||||||
|
assert "FabFilter" in plugins[0].name
|
||||||
|
assert ".vst3" in plugins[0].path
|
||||||
|
# Fruity Compressor → Pro-C 2
|
||||||
|
assert "FabFilter" in plugins[1].name
|
||||||
|
|
||||||
|
def test_build_fx_chain_bass(self):
|
||||||
|
"""build_fx_chain returns PluginDef list for bass role."""
|
||||||
|
from scripts.compose import build_fx_chain
|
||||||
|
|
||||||
|
genre_config = {
|
||||||
|
"mix": {
|
||||||
|
"per_role": {
|
||||||
|
"bass": {
|
||||||
|
"effects": ["Fruity Parametric EQ 2", "Saturn 2"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins = build_fx_chain("bass", genre_config, [])
|
||||||
|
assert len(plugins) == 2
|
||||||
|
# Saturn 2 → FabFilter Saturn 2
|
||||||
|
assert "Saturn" in plugins[1].name
|
||||||
|
|
||||||
|
def test_build_fx_chain_empty_effects(self):
|
||||||
|
"""build_fx_chain returns empty list when no effects configured."""
|
||||||
|
from scripts.compose import build_fx_chain
|
||||||
|
|
||||||
|
genre_config = {"mix": {"per_role": {}}}
|
||||||
|
plugins = build_fx_chain("drums", genre_config, [])
|
||||||
|
assert plugins == []
|
||||||
|
|
||||||
|
def test_build_fx_chain_unknown_effect_uses_name(self):
|
||||||
|
"""Unknown effect names are used as-is."""
|
||||||
|
from scripts.compose import build_fx_chain
|
||||||
|
|
||||||
|
genre_config = {
|
||||||
|
"mix": {
|
||||||
|
"per_role": {
|
||||||
|
"lead": {
|
||||||
|
"effects": ["Some Unknown FX"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins = build_fx_chain("lead", genre_config, [])
|
||||||
|
# Unknown effects are skipped (not added to plugins)
|
||||||
|
assert len(plugins) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstrumentPlugins:
|
||||||
|
"""Test instrument plugin helpers (Serum 2, Omnisphere)."""
|
||||||
|
|
||||||
|
def test_serum2_plugin_def(self):
|
||||||
|
"""serum2() returns PluginDef with registry key name."""
|
||||||
|
from scripts.compose import serum2
|
||||||
|
|
||||||
|
plugin = serum2()
|
||||||
|
assert plugin.name == "Serum2"
|
||||||
|
assert plugin.path == "Serum2.vst3"
|
||||||
|
assert plugin.index == 0
|
||||||
|
|
||||||
|
def test_omnisphere_plugin_def(self):
|
||||||
|
"""omnisphere() returns PluginDef with registry key name."""
|
||||||
|
from scripts.compose import omnisphere
|
||||||
|
|
||||||
|
plugin = omnisphere()
|
||||||
|
assert plugin.name == "Omnisphere"
|
||||||
|
assert plugin.path == "Omnisphere.vst3"
|
||||||
|
assert plugin.index == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateReturnTracks:
|
||||||
|
"""Test create_return_tracks function."""
|
||||||
|
|
||||||
|
def test_create_return_tracks_returns_two(self):
|
||||||
|
"""create_return_tracks returns [Reverb, Delay] tracks."""
|
||||||
|
from scripts.compose import create_return_tracks
|
||||||
|
|
||||||
|
tracks = create_return_tracks()
|
||||||
|
assert len(tracks) == 2
|
||||||
|
assert tracks[0].name == "Reverb"
|
||||||
|
assert tracks[1].name == "Delay"
|
||||||
|
|
||||||
|
def test_reverb_track_has_pro_r2(self):
|
||||||
|
"""Reverb return track has FabFilter Pro-R 2 plugin."""
|
||||||
|
from scripts.compose import create_return_tracks
|
||||||
|
|
||||||
|
tracks = create_return_tracks()
|
||||||
|
reverb = tracks[0]
|
||||||
|
assert len(reverb.plugins) == 1
|
||||||
|
assert "FabFilter" in reverb.plugins[0].name
|
||||||
|
assert ".vst3" in reverb.plugins[0].path
|
||||||
|
|
||||||
|
def test_delay_track_has_timeless3(self):
|
||||||
|
"""Delay return track has FabFilter Timeless 3 plugin."""
|
||||||
|
from scripts.compose import create_return_tracks
|
||||||
|
|
||||||
|
tracks = create_return_tracks()
|
||||||
|
delay = tracks[1]
|
||||||
|
assert len(delay.plugins) == 1
|
||||||
|
assert "Timeless" in delay.plugins[0].name
|
||||||
|
assert ".vst3" in delay.plugins[0].path
|
||||||
|
|
||||||
|
def test_return_tracks_have_volume_0_7(self):
|
||||||
|
"""Return tracks have volume 0.7."""
|
||||||
|
from scripts.compose import create_return_tracks
|
||||||
|
|
||||||
|
tracks = create_return_tracks()
|
||||||
|
for t in tracks:
|
||||||
|
assert t.volume == 0.7
|
||||||
Reference in New Issue
Block a user