615 lines
23 KiB
Python
615 lines
23 KiB
Python
"""
|
|
VST/AU Plugin Manager for AbletonMCP_AI
|
|
|
|
Manages VST and AU plugin detection, loading, and parameter configuration.
|
|
Supports popular plugins like Serum, Massive, Sylenth1, FabFilter, and ValhallaDSP.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
from dataclasses import dataclass, asdict
|
|
from enum import Enum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PluginType(Enum):
|
|
"""Types of plugins supported."""
|
|
VST2 = "VST2"
|
|
VST3 = "VST3"
|
|
AU = "AU" # Audio Units (macOS)
|
|
|
|
|
|
class PluginCategory(Enum):
|
|
"""Categories of plugins."""
|
|
SYNTH = "synth"
|
|
EFFECT = "effect"
|
|
EQ = "eq"
|
|
COMPRESSOR = "compressor"
|
|
REVERB = "reverb"
|
|
DELAY = "delay"
|
|
UTILITY = "utility"
|
|
|
|
|
|
@dataclass
|
|
class PluginInfo:
|
|
"""Information about a detected plugin."""
|
|
name: str
|
|
display_name: str
|
|
plugin_type: PluginType
|
|
category: PluginCategory
|
|
manufacturer: str
|
|
path: Optional[str] = None
|
|
is_installed: bool = False
|
|
version: str = ""
|
|
presets: List[str] = None
|
|
|
|
def __post_init__(self):
|
|
if self.presets is None:
|
|
self.presets = []
|
|
|
|
|
|
@dataclass
|
|
class ParameterInfo:
|
|
"""Information about a plugin parameter."""
|
|
name: str
|
|
display_name: str
|
|
min_value: float
|
|
max_value: float
|
|
default_value: float
|
|
value_type: str = "float" # float, int, bool
|
|
|
|
|
|
# Popular plugin database with known parameters
|
|
POPULAR_PLUGINS = {
|
|
# Synths
|
|
"serum": PluginInfo(
|
|
name="Serum",
|
|
display_name="Xfer Serum",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.SYNTH,
|
|
manufacturer="Xfer Records",
|
|
is_installed=False,
|
|
presets=["Init", "Bass - Basic", "Lead - Saw", "Pad - Warm"]
|
|
),
|
|
"massive": PluginInfo(
|
|
name="Massive",
|
|
display_name="Native Instruments Massive",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.SYNTH,
|
|
manufacturer="Native Instruments",
|
|
is_installed=False,
|
|
presets=["Init", "Bass - Deep", "Lead - Scream", "Pad - Atmosphere"]
|
|
),
|
|
"sylenth1": PluginInfo(
|
|
name="Sylenth1",
|
|
display_name="LennarDigital Sylenth1",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.SYNTH,
|
|
manufacturer="LennarDigital",
|
|
is_installed=False,
|
|
presets=["Init", "Bass - Sub", "Lead - SuperSaw", "Pad - Cloud"]
|
|
),
|
|
|
|
# FabFilter Effects
|
|
"pro-q": PluginInfo(
|
|
name="Pro-Q",
|
|
display_name="FabFilter Pro-Q 3",
|
|
plugin_type=PluginType.VST3,
|
|
category=PluginCategory.EQ,
|
|
manufacturer="FabFilter",
|
|
is_installed=False,
|
|
presets=["Default", "Low Cut", "High Cut", "Vocal", "Drums", "Mastering"]
|
|
),
|
|
"pro-c": PluginInfo(
|
|
name="Pro-C",
|
|
display_name="FabFilter Pro-C 2",
|
|
plugin_type=PluginType.VST3,
|
|
category=PluginCategory.COMPRESSOR,
|
|
manufacturer="FabFilter",
|
|
is_installed=False,
|
|
presets=["Default", "Vocal", "Drums", "Bus", "Mastering"]
|
|
),
|
|
"pro-r": PluginInfo(
|
|
name="Pro-R",
|
|
display_name="FabFilter Pro-R",
|
|
plugin_type=PluginType.VST3,
|
|
category=PluginCategory.REVERB,
|
|
manufacturer="FabFilter",
|
|
is_installed=False,
|
|
presets=["Default", "Hall", "Room", "Plate", "Vocal"]
|
|
),
|
|
|
|
# ValhallaDSP Effects
|
|
"valhalla_room": PluginInfo(
|
|
name="ValhallaRoom",
|
|
display_name="ValhallaRoom",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.REVERB,
|
|
manufacturer="ValhallaDSP",
|
|
is_installed=False,
|
|
presets=["Default", "Small Room", "Medium Room", "Large Hall", "Cathedral"]
|
|
),
|
|
"valhalla_vintage_verb": PluginInfo(
|
|
name="ValhallaVintageVerb",
|
|
display_name="ValhallaVintageVerb",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.REVERB,
|
|
manufacturer="ValhallaDSP",
|
|
is_installed=False,
|
|
presets=["Default", "1970s", "1980s", "1990s", "Modern"]
|
|
),
|
|
"valhalla_delay": PluginInfo(
|
|
name="ValhallaDelay",
|
|
display_name="ValhallaDelay",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.DELAY,
|
|
manufacturer="ValhallaDSP",
|
|
is_installed=False,
|
|
presets=["Default", "Tape", "Ping Pong", "Reverse"]
|
|
),
|
|
"valhalla_supermassive": PluginInfo(
|
|
name="ValhallaSupermassive",
|
|
display_name="ValhallaSupermassive",
|
|
plugin_type=PluginType.VST2,
|
|
category=PluginCategory.DELAY,
|
|
manufacturer="ValhallaDSP",
|
|
is_installed=False,
|
|
presets=["Default", "Sagittarius", "Great Wall", "Circinus"]
|
|
),
|
|
}
|
|
|
|
# Known parameter mappings for popular plugins
|
|
PLUGIN_PARAMETERS = {
|
|
"serum": {
|
|
"osc_a_wave": ParameterInfo("osc_a_wave", "Osc A Waveform", 0, 100, 0),
|
|
"osc_a_level": ParameterInfo("osc_a_level", "Osc A Level", 0, 1, 0.8),
|
|
"osc_b_wave": ParameterInfo("osc_b_wave", "Osc B Waveform", 0, 100, 0),
|
|
"osc_b_level": ParameterInfo("osc_b_level", "Osc B Level", 0, 1, 0),
|
|
"filter_cutoff": ParameterInfo("filter_cutoff", "Filter Cutoff", 0, 22000, 22000),
|
|
"filter_resonance": ParameterInfo("filter_resonance", "Filter Resonance", 0, 1, 0),
|
|
"attack": ParameterInfo("attack", "Amp Attack", 0, 10, 0.01),
|
|
"decay": ParameterInfo("decay", "Amp Decay", 0, 10, 0),
|
|
"sustain": ParameterInfo("sustain", "Amp Sustain", 0, 1, 1),
|
|
"release": ParameterInfo("release", "Amp Release", 0, 10, 0.5),
|
|
},
|
|
"massive": {
|
|
"osc1_pitch": ParameterInfo("osc1_pitch", "Osc 1 Pitch", -24, 24, 0),
|
|
"osc1_wtpos": ParameterInfo("osc1_wtpos", "Osc 1 Wavetable Pos", 0, 100, 0),
|
|
"osc2_pitch": ParameterInfo("osc2_pitch", "Osc 2 Pitch", -24, 24, 0),
|
|
"filter_cutoff": ParameterInfo("filter_cutoff", "Filter Cutoff", 0, 127, 127),
|
|
"filter_resonance": ParameterInfo("filter_resonance", "Filter Resonance", 0, 127, 0),
|
|
"attack": ParameterInfo("attack", "Amp Attack", 0, 127, 0),
|
|
"decay": ParameterInfo("decay", "Amp Decay", 0, 127, 0),
|
|
"sustain": ParameterInfo("sustain", "Amp Sustain", 0, 127, 127),
|
|
"release": ParameterInfo("release", "Amp Release", 0, 127, 20),
|
|
},
|
|
"sylenth1": {
|
|
"osc_a1_wave": ParameterInfo("osc_a1_wave", "Osc A1 Wave", 0, 4, 0),
|
|
"osc_a1_pitch": ParameterInfo("osc_a1_pitch", "Osc A1 Pitch", -10, 10, 0),
|
|
"osc_a2_wave": ParameterInfo("osc_a2_wave", "Osc A2 Wave", 0, 4, 0),
|
|
"cutoff_a": ParameterInfo("cutoff_a", "Filter A Cutoff", 0, 10, 10),
|
|
"resonance_a": ParameterInfo("resonance_a", "Filter A Resonance", 0, 10, 0),
|
|
"attack": ParameterInfo("attack", "Amp Attack", 0, 10, 0),
|
|
"decay": ParameterInfo("decay", "Amp Decay", 0, 10, 0),
|
|
"sustain": ParameterInfo("sustain", "Amp Sustain", 0, 10, 10),
|
|
"release": ParameterInfo("release", "Amp Release", 0, 10, 0),
|
|
},
|
|
"pro-q": {
|
|
"gain": ParameterInfo("gain", "Output Gain", -36, 36, 0),
|
|
"mix": ParameterInfo("mix", "Mix", 0, 100, 100),
|
|
"band1_gain": ParameterInfo("band1_gain", "Band 1 Gain", -30, 30, 0),
|
|
"band1_freq": ParameterInfo("band1_freq", "Band 1 Freq", 10, 30000, 200),
|
|
"band1_q": ParameterInfo("band1_q", "Band 1 Q", 0.025, 40, 1),
|
|
"band2_gain": ParameterInfo("band2_gain", "Band 2 Gain", -30, 30, 0),
|
|
"band2_freq": ParameterInfo("band2_freq", "Band 2 Freq", 10, 30000, 1000),
|
|
},
|
|
"pro-c": {
|
|
"threshold": ParameterInfo("threshold", "Threshold", -60, 0, 0),
|
|
"ratio": ParameterInfo("ratio", "Ratio", 1, 20, 2),
|
|
"attack": ParameterInfo("attack", "Attack", 0.005, 250, 10),
|
|
"release": ParameterInfo("release", "Release", 1, 2500, 100),
|
|
"makeup": ParameterInfo("makeup", "Makeup Gain", -36, 36, 0),
|
|
},
|
|
"valhalla_room": {
|
|
"mix": ParameterInfo("mix", "Mix", 0, 100, 50),
|
|
"decay": ParameterInfo("decay", "Decay", 0.1, 10, 2),
|
|
"size": ParameterInfo("size", "Size", 0, 1, 0.5),
|
|
"predelay": ParameterInfo("predelay", "PreDelay", 0, 200, 20),
|
|
},
|
|
"valhalla_vintage_verb": {
|
|
"mix": ParameterInfo("mix", "Mix", 0, 100, 50),
|
|
"decay": ParameterInfo("decay", "Decay", 0.1, 10, 2),
|
|
"damping": ParameterInfo("damping", "Damping", 0, 100, 50),
|
|
},
|
|
}
|
|
|
|
|
|
class VSTManager:
|
|
"""Manager for VST/AU plugins in Ableton Live."""
|
|
|
|
def __init__(self, song=None, connection=None):
|
|
"""
|
|
Initialize VST Manager.
|
|
|
|
Args:
|
|
song: Ableton Live song object (optional)
|
|
connection: TCP connection to Ableton (optional)
|
|
"""
|
|
self.song = song
|
|
self.connection = connection
|
|
self._scanned_plugins: Dict[str, PluginInfo] = {}
|
|
self._plugin_cache_file = self._get_cache_path()
|
|
|
|
# Initialize with known plugins
|
|
self._initialize_plugin_database()
|
|
|
|
# Try to load cached scan results
|
|
self._load_cached_plugins()
|
|
|
|
def _get_cache_path(self) -> str:
|
|
"""Get path for plugin cache file."""
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
return os.path.join(script_dir, "..", "..", "vst_plugin_cache.json")
|
|
|
|
def _initialize_plugin_database(self):
|
|
"""Initialize the plugin database with known popular plugins."""
|
|
for key, info in POPULAR_PLUGINS.items():
|
|
self._scanned_plugins[key] = PluginInfo(
|
|
name=info.name,
|
|
display_name=info.display_name,
|
|
plugin_type=info.plugin_type,
|
|
category=info.category,
|
|
manufacturer=info.manufacturer,
|
|
is_installed=False,
|
|
version=info.version,
|
|
presets=list(info.presets) if info.presets else []
|
|
)
|
|
|
|
def _load_cached_plugins(self):
|
|
"""Load cached plugin scan results."""
|
|
try:
|
|
if os.path.exists(self._plugin_cache_file):
|
|
with open(self._plugin_cache_file, 'r') as f:
|
|
cached = json.load(f)
|
|
for key, data in cached.items():
|
|
if key in self._scanned_plugins:
|
|
self._scanned_plugins[key].is_installed = data.get('is_installed', False)
|
|
self._scanned_plugins[key].path = data.get('path')
|
|
if 'presets' in data and data['presets']:
|
|
self._scanned_plugins[key].presets = data['presets']
|
|
logger.info(f"Loaded {len(cached)} plugins from cache")
|
|
except Exception as e:
|
|
logger.warning(f"Could not load plugin cache: {e}")
|
|
|
|
def _save_cached_plugins(self):
|
|
"""Save plugin scan results to cache."""
|
|
try:
|
|
cache_data = {}
|
|
for key, info in self._scanned_plugins.items():
|
|
cache_data[key] = {
|
|
'name': info.name,
|
|
'is_installed': info.is_installed,
|
|
'path': info.path,
|
|
'presets': info.presets
|
|
}
|
|
with open(self._plugin_cache_file, 'w') as f:
|
|
json.dump(cache_data, f, indent=2)
|
|
except Exception as e:
|
|
logger.warning(f"Could not save plugin cache: {e}")
|
|
|
|
def scan_vst_plugins(self, force_rescan: bool = False) -> Dict[str, Any]:
|
|
"""
|
|
Scan for installed VST/AU plugins.
|
|
|
|
This attempts to detect which popular plugins are installed
|
|
in the system. In a real implementation, this would query
|
|
the operating system's plugin registry or Ableton's plugin list.
|
|
|
|
Args:
|
|
force_rescan: Force a fresh scan even if cache exists
|
|
|
|
Returns:
|
|
Dictionary with scan results and installed plugin list
|
|
"""
|
|
installed_count = 0
|
|
|
|
if force_rescan or not any(p.is_installed for p in self._scanned_plugins.values()):
|
|
# In a real implementation, this would:
|
|
# 1. Query Windows Registry for VST2 paths
|
|
# 2. Scan VST3 standard paths
|
|
# 3. Query macOS AudioUnit registry
|
|
# 4. Or query Ableton Live's plugin database
|
|
|
|
# For now, we simulate detection based on common installation paths
|
|
# and let the user confirm installation when loading
|
|
|
|
common_paths = self._get_common_plugin_paths()
|
|
|
|
for key, info in self._scanned_plugins.items():
|
|
# Check if plugin might be installed
|
|
# This is a heuristic - actual detection requires OS-specific calls
|
|
found_path = self._find_plugin_file(info.name, common_paths, info.plugin_type)
|
|
if found_path:
|
|
info.is_installed = True
|
|
info.path = found_path
|
|
installed_count += 1
|
|
logger.info(f"Detected plugin: {info.name} at {found_path}")
|
|
|
|
self._save_cached_plugins()
|
|
|
|
# Build results
|
|
installed = [p.name for p in self._scanned_plugins.values() if p.is_installed]
|
|
not_installed = [p.name for p in self._scanned_plugins.values() if not p.is_installed]
|
|
|
|
return {
|
|
"total_known": len(self._scanned_plugins),
|
|
"installed_count": len(installed),
|
|
"installed_plugins": installed,
|
|
"not_installed": not_installed,
|
|
"categories": self._get_plugins_by_category(),
|
|
"scan_paths_checked": self._get_common_plugin_paths() if force_rescan else [],
|
|
"cache_file": self._plugin_cache_file,
|
|
"note": "Plugin detection is heuristic. Actual availability verified when loading."
|
|
}
|
|
|
|
def _get_common_plugin_paths(self) -> List[str]:
|
|
"""Get common plugin installation paths."""
|
|
paths = []
|
|
|
|
# Windows VST2 paths
|
|
if os.name == 'nt':
|
|
paths.extend([
|
|
os.path.expandvars(r"%PROGRAMFILES%\VstPlugins"),
|
|
os.path.expandvars(r"%PROGRAMFILES(x86)%\VstPlugins"),
|
|
os.path.expandvars(r"%COMMONPROGRAMFILES%\VST2"),
|
|
os.path.expandvars(r"%COMMONPROGRAMFILES(x86)%\VST2"),
|
|
os.path.expandvars(r"%PROGRAMFILES%\Common Files\VST2"),
|
|
os.path.expandvars(r"%PROGRAMFILES%\Common Files\VST3"),
|
|
os.path.expandvars(r"%LOCALAPPDATA%\Programs\Common\VST3"),
|
|
])
|
|
|
|
# macOS paths (for completeness)
|
|
else:
|
|
paths.extend([
|
|
"/Library/Audio/Plug-Ins/VST",
|
|
"/Library/Audio/Plug-Ins/VST3",
|
|
"/Library/Audio/Plug-Ins/Components",
|
|
os.path.expanduser("~/Library/Audio/Plug-Ins/VST"),
|
|
os.path.expanduser("~/Library/Audio/Plug-Ins/VST3"),
|
|
os.path.expanduser("~/Library/Audio/Plug-Ins/Components"),
|
|
])
|
|
|
|
return paths
|
|
|
|
def _find_plugin_file(self, plugin_name: str, paths: List[str], plugin_type: PluginType) -> Optional[str]:
|
|
"""Search for plugin file in given paths."""
|
|
# Normalize plugin name for file search
|
|
name_variants = [
|
|
plugin_name,
|
|
plugin_name.replace(" ", ""),
|
|
plugin_name.replace(" ", "-"),
|
|
]
|
|
|
|
# Extension based on plugin type
|
|
extensions = []
|
|
if plugin_type == PluginType.VST2:
|
|
extensions = ['.dll'] if os.name == 'nt' else ['.vst', '.so']
|
|
elif plugin_type == PluginType.VST3:
|
|
extensions = ['.vst3']
|
|
elif plugin_type == PluginType.AU:
|
|
extensions = ['.component']
|
|
|
|
for path in paths:
|
|
if not os.path.exists(path):
|
|
continue
|
|
|
|
try:
|
|
for root, dirs, files in os.walk(path):
|
|
for ext in extensions:
|
|
for name_variant in name_variants:
|
|
filename = name_variant + ext
|
|
if filename.lower() in [f.lower() for f in files]:
|
|
return os.path.join(root, filename)
|
|
|
|
# Check directories (for bundles)
|
|
if ext == '.component' or ext == '.vst3':
|
|
bundle_name = name_variant + ext
|
|
if bundle_name.lower() in [d.lower() for d in dirs]:
|
|
return os.path.join(root, bundle_name)
|
|
except Exception as e:
|
|
logger.debug(f"Error scanning {path}: {e}")
|
|
|
|
return None
|
|
|
|
def get_vst_presets(self, plugin_name: str) -> Dict[str, Any]:
|
|
"""
|
|
Get available presets for a plugin.
|
|
|
|
Args:
|
|
plugin_name: Name of the plugin
|
|
|
|
Returns:
|
|
Dictionary with preset list and plugin info
|
|
"""
|
|
key = plugin_name.lower().replace(" ", "_")
|
|
|
|
# Handle aliases
|
|
aliases = {
|
|
"serum": "serum",
|
|
"xfer_serum": "serum",
|
|
"massive": "massive",
|
|
"ni_massive": "massive",
|
|
"sylenth1": "sylenth1",
|
|
"sylenth": "sylenth1",
|
|
"pro-q": "pro-q",
|
|
"pro_q": "pro-q",
|
|
"fabfilter_pro_q": "pro-q",
|
|
"pro-c": "pro-c",
|
|
"pro_c": "pro-c",
|
|
"fabfilter_pro_c": "pro-c",
|
|
"valhallaroom": "valhalla_room",
|
|
"valhalla_room": "valhalla_room",
|
|
"valhallavintageverb": "valhalla_vintage_verb",
|
|
"valhalla_vintage_verb": "valhalla_vintage_verb",
|
|
}
|
|
|
|
key = aliases.get(key, key)
|
|
|
|
if key not in self._scanned_plugins:
|
|
return {
|
|
"status": "error",
|
|
"message": f"Unknown plugin: {plugin_name}",
|
|
"available_plugins": list(self._scanned_plugins.keys())
|
|
}
|
|
|
|
info = self._scanned_plugins[key]
|
|
|
|
# Get parameters for this plugin
|
|
params = PLUGIN_PARAMETERS.get(key, {})
|
|
param_list = [asdict(p) for p in params.values()]
|
|
|
|
return {
|
|
"status": "success",
|
|
"plugin_name": info.name,
|
|
"display_name": info.display_name,
|
|
"manufacturer": info.manufacturer,
|
|
"category": info.category.value,
|
|
"is_installed": info.is_installed,
|
|
"presets": info.presets,
|
|
"parameters": param_list,
|
|
"plugin_type": info.plugin_type.value,
|
|
}
|
|
|
|
def get_all_plugins(self) -> Dict[str, Any]:
|
|
"""Get list of all known plugins with their status."""
|
|
plugins = []
|
|
for key, info in self._scanned_plugins.items():
|
|
plugins.append({
|
|
"key": key,
|
|
"name": info.name,
|
|
"display_name": info.display_name,
|
|
"manufacturer": info.manufacturer,
|
|
"category": info.category.value,
|
|
"is_installed": info.is_installed,
|
|
"plugin_type": info.plugin_type.value,
|
|
})
|
|
|
|
return {
|
|
"total": len(plugins),
|
|
"installed": sum(1 for p in plugins if p["is_installed"]),
|
|
"plugins": plugins
|
|
}
|
|
|
|
def _get_plugins_by_category(self) -> Dict[str, List[str]]:
|
|
"""Group plugins by category."""
|
|
by_category = {}
|
|
for info in self._scanned_plugins.values():
|
|
cat = info.category.value
|
|
if cat not in by_category:
|
|
by_category[cat] = []
|
|
by_category[cat].append(info.name)
|
|
return by_category
|
|
|
|
def validate_plugin_installation(self, plugin_name: str) -> Tuple[bool, str]:
|
|
"""
|
|
Validate if a plugin is actually installed and usable.
|
|
|
|
Args:
|
|
plugin_name: Name of the plugin to validate
|
|
|
|
Returns:
|
|
Tuple of (is_installed, message)
|
|
"""
|
|
key = plugin_name.lower().replace(" ", "_")
|
|
|
|
# Handle common aliases
|
|
aliases = {
|
|
"serum": "serum",
|
|
"xfer_serum": "serum",
|
|
"massive": "massive",
|
|
"ni_massive": "massive",
|
|
"sylenth1": "sylenth1",
|
|
"sylenth": "sylenth1",
|
|
"pro-q": "pro-q",
|
|
"pro-q_3": "pro-q",
|
|
"pro-c": "pro-c",
|
|
"pro-c_2": "pro-c",
|
|
"valhallaroom": "valhalla_room",
|
|
"valhalla_vintage_verb": "valhalla_vintage_verb",
|
|
}
|
|
key = aliases.get(key, key)
|
|
|
|
if key not in self._scanned_plugins:
|
|
return False, f"Plugin '{plugin_name}' not in database"
|
|
|
|
info = self._scanned_plugins[key]
|
|
|
|
if info.is_installed and info.path:
|
|
# Verify file still exists
|
|
if os.path.exists(info.path):
|
|
return True, f"Plugin found at {info.path}"
|
|
else:
|
|
# File moved or deleted, update status
|
|
info.is_installed = False
|
|
info.path = None
|
|
self._save_cached_plugins()
|
|
return False, "Plugin was moved or deleted"
|
|
|
|
# Not marked as installed, try to find it
|
|
paths = self._get_common_plugin_paths()
|
|
found = self._find_plugin_file(info.name, paths, info.plugin_type)
|
|
|
|
if found:
|
|
info.is_installed = True
|
|
info.path = found
|
|
self._save_cached_plugins()
|
|
return True, f"Plugin found at {found}"
|
|
|
|
return False, f"Plugin '{plugin_name}' not found in standard plugin directories"
|
|
|
|
|
|
# Global instance
|
|
_vst_manager: Optional[VSTManager] = None
|
|
|
|
|
|
def get_vst_manager(song=None, connection=None) -> VSTManager:
|
|
"""Get or create global VST manager instance."""
|
|
global _vst_manager
|
|
if _vst_manager is None:
|
|
_vst_manager = VSTManager(song=song, connection=connection)
|
|
return _vst_manager
|
|
|
|
|
|
def scan_vst_plugins(force_rescan: bool = False) -> Dict[str, Any]:
|
|
"""Scan for installed VST/AU plugins."""
|
|
manager = get_vst_manager()
|
|
return manager.scan_vst_plugins(force_rescan=force_rescan)
|
|
|
|
|
|
def get_vst_presets(plugin_name: str) -> Dict[str, Any]:
|
|
"""Get presets for a plugin."""
|
|
manager = get_vst_manager()
|
|
return manager.get_vst_presets(plugin_name)
|
|
|
|
|
|
def get_all_plugins() -> Dict[str, Any]:
|
|
"""Get all known plugins."""
|
|
manager = get_vst_manager()
|
|
return manager.get_all_plugins()
|
|
|
|
|
|
def validate_plugin(plugin_name: str) -> Tuple[bool, str]:
|
|
"""Validate plugin installation."""
|
|
manager = get_vst_manager()
|
|
return manager.validate_plugin_installation(plugin_name)
|
|
|
|
|
|
def get_plugin_parameters(plugin_name: str) -> Dict[str, ParameterInfo]:
|
|
"""Get parameters for a plugin."""
|
|
key = plugin_name.lower().replace(" ", "_")
|
|
return PLUGIN_PARAMETERS.get(key, {})
|