#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Carla plugin list code # Copyright (C) 2011-2022 Filipe Coelho # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a full copy of the GNU General Public License see the doc/GPL.txt file. # --------------------------------------------------------------------------------------------------------------------- # Imports (Global) import os from copy import deepcopy from subprocess import Popen, PIPE from PyQt5.QtCore import qWarning # --------------------------------------------------------------------------------------------------------------------- # Imports (Carla) from carla_backend import ( BINARY_NATIVE, BINARY_NONE, PLUGIN_AU, PLUGIN_DSSI, PLUGIN_LADSPA, PLUGIN_LV2, PLUGIN_NONE, PLUGIN_SF2, PLUGIN_SFZ, PLUGIN_VST2, PLUGIN_VST3, PLUGIN_CLAP, ) from carla_shared import ( LINUX, MACOS, WINDOWS, ) from carla_utils import getPluginCategoryAsString # --------------------------------------------------------------------------------------------------------------------- # Plugin Query (helper functions) def findBinaries(binPath, pluginType, OS): binaries = [] if OS == "HAIKU": extensions = ("") if pluginType == PLUGIN_VST2 else (".so",) elif OS == "MACOS": extensions = (".dylib", ".so") elif OS == "WINDOWS": extensions = (".dll",) else: extensions = (".so",) for root, _, files in os.walk(binPath): for name in tuple(name for name in files if name.lower().endswith(extensions)): binaries.append(os.path.join(root, name)) return binaries def findVST3Binaries(binPath): binaries = [] for root, dirs, files in os.walk(binPath): for name in tuple(name for name in (files+dirs) if name.lower().endswith(".vst3")): binaries.append(os.path.join(root, name)) return binaries def findCLAPBinaries(binPath): binaries = [] for root, _, files in os.walk(binPath, followlinks=True): for name in tuple(name for name in files if name.lower().endswith(".clap")): binaries.append(os.path.join(root, name)) return binaries def findLV2Bundles(bundlePath): bundles = [] for root, _, _2 in os.walk(bundlePath, followlinks=True): if root == bundlePath: continue if os.path.exists(os.path.join(root, "manifest.ttl")): bundles.append(root) return bundles def findMacBundles(bundlePath, pluginType): bundles = [] if pluginType == PLUGIN_VST2: extension = ".vst" elif pluginType == PLUGIN_VST3: extension = ".vst3" elif pluginType == PLUGIN_CLAP: extension = ".clap" else: return bundles for root, dirs, _ in os.walk(bundlePath, followlinks=True): for name in tuple(name for name in dirs if name.lower().endswith(extension)): bundles.append(os.path.join(root, name)) return bundles def findFilenames(filePath, stype): filenames = [] if stype == "sf2": extensions = (".sf2",".sf3",) else: return [] for root, _, files in os.walk(filePath): for name in tuple(name for name in files if name.lower().endswith(extensions)): filenames.append(os.path.join(root, name)) return filenames # --------------------------------------------------------------------------------------------------------------------- # Plugin Query # NOTE: this code is ugly, it is meant to be replaced, so let it be as-is for now PLUGIN_QUERY_API_VERSION = 12 PyPluginInfo = { 'API': PLUGIN_QUERY_API_VERSION, 'valid': False, 'build': BINARY_NONE, 'type': PLUGIN_NONE, 'hints': 0x0, 'category': "", 'filename': "", 'name': "", 'label': "", 'maker': "", 'uniqueId': 0, 'audio.ins': 0, 'audio.outs': 0, 'cv.ins': 0, 'cv.outs': 0, 'midi.ins': 0, 'midi.outs': 0, 'parameters.ins': 0, 'parameters.outs': 0 } gDiscoveryProcess = None def findWinePrefix(filename, recursionLimit = 10): if recursionLimit == 0 or len(filename) < 5 or "/" not in filename: return "" path = filename[:filename.rfind("/")] if os.path.isdir(path + "/dosdevices"): return path return findWinePrefix(path, recursionLimit-1) def runCarlaDiscovery(itype, stype, filename, tool, wineSettings=None): if not os.path.exists(tool): qWarning(f"runCarlaDiscovery() - tool '{tool}' does not exist") return [] command = [] if LINUX or MACOS: command.append("env") command.append("LANG=C") command.append("LD_PRELOAD=") if wineSettings is not None: command.append("WINEDEBUG=-all") if wineSettings['autoPrefix']: winePrefix = findWinePrefix(filename) else: winePrefix = "" if not winePrefix: envWinePrefix = os.getenv("WINEPREFIX") if envWinePrefix: winePrefix = envWinePrefix elif wineSettings['fallbackPrefix']: winePrefix = os.path.expanduser(wineSettings['fallbackPrefix']) else: winePrefix = os.path.expanduser("~/.wine") wineCMD = wineSettings['executable'] if wineSettings['executable'] else "wine" if tool.endswith("64.exe") and os.path.exists(wineCMD + "64"): wineCMD += "64" command.append("WINEPREFIX=" + winePrefix) command.append(wineCMD) command.append(tool) command.append(stype) command.append(filename) # pylint: disable=global-statement global gDiscoveryProcess # pylint: enable=global-statement # pylint: disable=consider-using-with gDiscoveryProcess = Popen(command, stdout=PIPE) # pylint: enable=consider-using-with pinfo = None plugins = [] fakeLabel = os.path.basename(filename).rsplit(".", 1)[0] while True: try: line = gDiscoveryProcess.stdout.readline().decode("utf-8", errors="ignore") except: print("ERROR: discovery readline failed") break # line is valid, strip it if line: line = line.strip() # line is invalid, try poll() again elif gDiscoveryProcess.poll() is None: continue # line is invalid and poll() failed, stop here else: break if line == "carla-discovery::init::-----------": pinfo = deepcopy(PyPluginInfo) pinfo['type'] = itype pinfo['filename'] = filename if filename != ":all" else "" elif line == "carla-discovery::end::------------": if pinfo is not None: plugins.append(pinfo) del pinfo pinfo = None elif line == "Segmentation fault": print(f"carla-discovery::crash::{filename} crashed during discovery") elif line.startswith("err:module:import_dll Library"): print(line) elif line.startswith("carla-discovery::info::"): print(f"{line} - {filename}") elif line.startswith("carla-discovery::warning::"): print(f"{line} - {filename}") elif line.startswith("carla-discovery::error::"): print(f"{line} - {filename}") elif line.startswith("carla-discovery::"): if pinfo is None: continue try: prop, value = line.replace("carla-discovery::", "").split("::", 1) except: continue # pylint: disable=unsupported-assignment-operation if prop == "build": if value.isdigit(): pinfo['build'] = int(value) elif prop == "name": pinfo['name'] = value if value else fakeLabel elif prop == "label": pinfo['label'] = value if value else fakeLabel elif prop == "filename": pinfo['filename'] = value elif prop == "maker": pinfo['maker'] = value elif prop == "category": pinfo['category'] = value elif prop == "uniqueId": if value.isdigit(): pinfo['uniqueId'] = int(value) elif prop == "hints": if value.isdigit(): pinfo['hints'] = int(value) elif prop == "audio.ins": if value.isdigit(): pinfo['audio.ins'] = int(value) elif prop == "audio.outs": if value.isdigit(): pinfo['audio.outs'] = int(value) elif prop == "cv.ins": if value.isdigit(): pinfo['cv.ins'] = int(value) elif prop == "cv.outs": if value.isdigit(): pinfo['cv.outs'] = int(value) elif prop == "midi.ins": if value.isdigit(): pinfo['midi.ins'] = int(value) elif prop == "midi.outs": if value.isdigit(): pinfo['midi.outs'] = int(value) elif prop == "parameters.ins": if value.isdigit(): pinfo['parameters.ins'] = int(value) elif prop == "parameters.outs": if value.isdigit(): pinfo['parameters.outs'] = int(value) elif prop == "uri": if value: pinfo['label'] = value else: # cannot use empty URIs del pinfo pinfo = None continue else: print(f"{line} - {filename} (unknown property)") # pylint: enable=unsupported-assignment-operation tmp = gDiscoveryProcess gDiscoveryProcess = None del tmp return plugins def killDiscovery(): # pylint: disable=global-variable-not-assigned global gDiscoveryProcess # pylint: enable=global-variable-not-assigned if gDiscoveryProcess is not None: gDiscoveryProcess.kill() def checkPluginCached(desc, ptype): pinfo = deepcopy(PyPluginInfo) pinfo['build'] = BINARY_NATIVE pinfo['type'] = ptype pinfo['hints'] = desc['hints'] pinfo['name'] = desc['name'] pinfo['label'] = desc['label'] pinfo['maker'] = desc['maker'] pinfo['category'] = getPluginCategoryAsString(desc['category']) pinfo['audio.ins'] = desc['audioIns'] pinfo['audio.outs'] = desc['audioOuts'] pinfo['cv.ins'] = desc['cvIns'] pinfo['cv.outs'] = desc['cvOuts'] pinfo['midi.ins'] = desc['midiIns'] pinfo['midi.outs'] = desc['midiOuts'] pinfo['parameters.ins'] = desc['parameterIns'] pinfo['parameters.outs'] = desc['parameterOuts'] if ptype == PLUGIN_LV2: pinfo['filename'], pinfo['label'] = pinfo['label'].split('\\' if WINDOWS else '/',1) elif ptype == PLUGIN_SFZ: pinfo['filename'] = pinfo['label'] pinfo['label'] = pinfo['name'] return pinfo def checkPluginLADSPA(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_LADSPA, "LADSPA", filename, tool, wineSettings) def checkPluginDSSI(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_DSSI, "DSSI", filename, tool, wineSettings) def checkPluginLV2(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_LV2, "LV2", filename, tool, wineSettings) def checkPluginVST2(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_VST2, "VST2", filename, tool, wineSettings) def checkPluginVST3(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_VST3, "VST3", filename, tool, wineSettings) def checkPluginCLAP(filename, tool, wineSettings=None): return runCarlaDiscovery(PLUGIN_CLAP, "CLAP", filename, tool, wineSettings) def checkFileSF2(filename, tool): return runCarlaDiscovery(PLUGIN_SF2, "SF2", filename, tool) def checkFileSFZ(filename, tool): return runCarlaDiscovery(PLUGIN_SFZ, "SFZ", filename, tool) def checkAllPluginsAU(tool): return runCarlaDiscovery(PLUGIN_AU, "AU", ":all", tool)