|  | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Carla plugin list code
# Copyright (C) 2011-2022 Filipe Coelho <falktx@falktx.com>
#
# 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)
 |