Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
12KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla plugin list code
  4. # Copyright (C) 2011-2022 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  17. # ---------------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. import os
  20. from copy import deepcopy
  21. from subprocess import Popen, PIPE
  22. from PyQt5.QtCore import qWarning
  23. # ---------------------------------------------------------------------------------------------------------------------
  24. # Imports (Carla)
  25. from carla_backend import (
  26. BINARY_NATIVE,
  27. BINARY_NONE,
  28. PLUGIN_AU,
  29. PLUGIN_DSSI,
  30. PLUGIN_LADSPA,
  31. PLUGIN_LV2,
  32. PLUGIN_NONE,
  33. PLUGIN_SF2,
  34. PLUGIN_SFZ,
  35. PLUGIN_VST2,
  36. PLUGIN_VST3,
  37. PLUGIN_CLAP,
  38. )
  39. from carla_shared import (
  40. LINUX,
  41. MACOS,
  42. WINDOWS,
  43. )
  44. from carla_utils import getPluginCategoryAsString
  45. # ---------------------------------------------------------------------------------------------------------------------
  46. # Plugin Query (helper functions)
  47. def findBinaries(binPath, pluginType, OS):
  48. binaries = []
  49. if OS == "HAIKU":
  50. extensions = ("") if pluginType == PLUGIN_VST2 else (".so",)
  51. elif OS == "MACOS":
  52. extensions = (".dylib", ".so")
  53. elif OS == "WINDOWS":
  54. extensions = (".dll",)
  55. else:
  56. extensions = (".so",)
  57. for root, _, files in os.walk(binPath):
  58. for name in tuple(name for name in files if name.lower().endswith(extensions)):
  59. binaries.append(os.path.join(root, name))
  60. return binaries
  61. def findVST3Binaries(binPath):
  62. binaries = []
  63. for root, dirs, files in os.walk(binPath):
  64. for name in tuple(name for name in (files+dirs) if name.lower().endswith(".vst3")):
  65. binaries.append(os.path.join(root, name))
  66. return binaries
  67. def findCLAPBinaries(binPath):
  68. binaries = []
  69. for root, dirs, files in os.walk(binPath):
  70. for name in tuple(name for name in (files+dirs) if name.lower().endswith(".clap")):
  71. binaries.append(os.path.join(root, name))
  72. return binaries
  73. def findLV2Bundles(bundlePath):
  74. bundles = []
  75. for root, _, _2 in os.walk(bundlePath, followlinks=True):
  76. if root == bundlePath:
  77. continue
  78. if os.path.exists(os.path.join(root, "manifest.ttl")):
  79. bundles.append(root)
  80. return bundles
  81. def findMacVSTBundles(bundlePath, isVST3):
  82. bundles = []
  83. extension = ".vst3" if isVST3 else ".vst"
  84. for root, dirs, _ in os.walk(bundlePath, followlinks=True):
  85. for name in tuple(name for name in dirs if name.lower().endswith(extension)):
  86. bundles.append(os.path.join(root, name))
  87. return bundles
  88. def findFilenames(filePath, stype):
  89. filenames = []
  90. if stype == "sf2":
  91. extensions = (".sf2",".sf3",)
  92. else:
  93. return []
  94. for root, _, files in os.walk(filePath):
  95. for name in tuple(name for name in files if name.lower().endswith(extensions)):
  96. filenames.append(os.path.join(root, name))
  97. return filenames
  98. # ---------------------------------------------------------------------------------------------------------------------
  99. # Plugin Query
  100. # NOTE: this code is ugly, it is meant to be replaced, so let it be as-is for now
  101. PLUGIN_QUERY_API_VERSION = 12
  102. PyPluginInfo = {
  103. 'API': PLUGIN_QUERY_API_VERSION,
  104. 'valid': False,
  105. 'build': BINARY_NONE,
  106. 'type': PLUGIN_NONE,
  107. 'hints': 0x0,
  108. 'category': "",
  109. 'filename': "",
  110. 'name': "",
  111. 'label': "",
  112. 'maker': "",
  113. 'uniqueId': 0,
  114. 'audio.ins': 0,
  115. 'audio.outs': 0,
  116. 'cv.ins': 0,
  117. 'cv.outs': 0,
  118. 'midi.ins': 0,
  119. 'midi.outs': 0,
  120. 'parameters.ins': 0,
  121. 'parameters.outs': 0
  122. }
  123. gDiscoveryProcess = None
  124. def findWinePrefix(filename, recursionLimit = 10):
  125. if recursionLimit == 0 or len(filename) < 5 or "/" not in filename:
  126. return ""
  127. path = filename[:filename.rfind("/")]
  128. if os.path.isdir(path + "/dosdevices"):
  129. return path
  130. return findWinePrefix(path, recursionLimit-1)
  131. def runCarlaDiscovery(itype, stype, filename, tool, wineSettings=None):
  132. if not os.path.exists(tool):
  133. qWarning(f"runCarlaDiscovery() - tool '{tool}' does not exist")
  134. return []
  135. command = []
  136. if LINUX or MACOS:
  137. command.append("env")
  138. command.append("LANG=C")
  139. command.append("LD_PRELOAD=")
  140. if wineSettings is not None:
  141. command.append("WINEDEBUG=-all")
  142. if wineSettings['autoPrefix']:
  143. winePrefix = findWinePrefix(filename)
  144. else:
  145. winePrefix = ""
  146. if not winePrefix:
  147. envWinePrefix = os.getenv("WINEPREFIX")
  148. if envWinePrefix:
  149. winePrefix = envWinePrefix
  150. elif wineSettings['fallbackPrefix']:
  151. winePrefix = os.path.expanduser(wineSettings['fallbackPrefix'])
  152. else:
  153. winePrefix = os.path.expanduser("~/.wine")
  154. wineCMD = wineSettings['executable'] if wineSettings['executable'] else "wine"
  155. if tool.endswith("64.exe") and os.path.exists(wineCMD + "64"):
  156. wineCMD += "64"
  157. command.append("WINEPREFIX=" + winePrefix)
  158. command.append(wineCMD)
  159. command.append(tool)
  160. command.append(stype)
  161. command.append(filename)
  162. # pylint: disable=global-statement
  163. global gDiscoveryProcess
  164. # pylint: enable=global-statement
  165. # pylint: disable=consider-using-with
  166. gDiscoveryProcess = Popen(command, stdout=PIPE)
  167. # pylint: enable=consider-using-with
  168. pinfo = None
  169. plugins = []
  170. fakeLabel = os.path.basename(filename).rsplit(".", 1)[0]
  171. while True:
  172. try:
  173. line = gDiscoveryProcess.stdout.readline().decode("utf-8", errors="ignore")
  174. except:
  175. print("ERROR: discovery readline failed")
  176. break
  177. # line is valid, strip it
  178. if line:
  179. line = line.strip()
  180. # line is invalid, try poll() again
  181. elif gDiscoveryProcess.poll() is None:
  182. continue
  183. # line is invalid and poll() failed, stop here
  184. else:
  185. break
  186. if line == "carla-discovery::init::-----------":
  187. pinfo = deepcopy(PyPluginInfo)
  188. pinfo['type'] = itype
  189. pinfo['filename'] = filename if filename != ":all" else ""
  190. elif line == "carla-discovery::end::------------":
  191. if pinfo is not None:
  192. plugins.append(pinfo)
  193. del pinfo
  194. pinfo = None
  195. elif line == "Segmentation fault":
  196. print(f"carla-discovery::crash::{filename} crashed during discovery")
  197. elif line.startswith("err:module:import_dll Library"):
  198. print(line)
  199. elif line.startswith("carla-discovery::info::"):
  200. print(f"{line} - {filename}")
  201. elif line.startswith("carla-discovery::warning::"):
  202. print(f"{line} - {filename}")
  203. elif line.startswith("carla-discovery::error::"):
  204. print(f"{line} - {filename}")
  205. elif line.startswith("carla-discovery::"):
  206. if pinfo is None:
  207. continue
  208. try:
  209. prop, value = line.replace("carla-discovery::", "").split("::", 1)
  210. except:
  211. continue
  212. # pylint: disable=unsupported-assignment-operation
  213. if prop == "build":
  214. if value.isdigit():
  215. pinfo['build'] = int(value)
  216. elif prop == "name":
  217. pinfo['name'] = value if value else fakeLabel
  218. elif prop == "label":
  219. pinfo['label'] = value if value else fakeLabel
  220. elif prop == "filename":
  221. pinfo['filename'] = value
  222. elif prop == "maker":
  223. pinfo['maker'] = value
  224. elif prop == "category":
  225. pinfo['category'] = value
  226. elif prop == "uniqueId":
  227. if value.isdigit():
  228. pinfo['uniqueId'] = int(value)
  229. elif prop == "hints":
  230. if value.isdigit():
  231. pinfo['hints'] = int(value)
  232. elif prop == "audio.ins":
  233. if value.isdigit():
  234. pinfo['audio.ins'] = int(value)
  235. elif prop == "audio.outs":
  236. if value.isdigit():
  237. pinfo['audio.outs'] = int(value)
  238. elif prop == "cv.ins":
  239. if value.isdigit():
  240. pinfo['cv.ins'] = int(value)
  241. elif prop == "cv.outs":
  242. if value.isdigit():
  243. pinfo['cv.outs'] = int(value)
  244. elif prop == "midi.ins":
  245. if value.isdigit():
  246. pinfo['midi.ins'] = int(value)
  247. elif prop == "midi.outs":
  248. if value.isdigit():
  249. pinfo['midi.outs'] = int(value)
  250. elif prop == "parameters.ins":
  251. if value.isdigit():
  252. pinfo['parameters.ins'] = int(value)
  253. elif prop == "parameters.outs":
  254. if value.isdigit():
  255. pinfo['parameters.outs'] = int(value)
  256. elif prop == "uri":
  257. if value:
  258. pinfo['label'] = value
  259. else:
  260. # cannot use empty URIs
  261. del pinfo
  262. pinfo = None
  263. continue
  264. else:
  265. print(f"{line} - {filename} (unknown property)")
  266. # pylint: enable=unsupported-assignment-operation
  267. tmp = gDiscoveryProcess
  268. gDiscoveryProcess = None
  269. del tmp
  270. return plugins
  271. def killDiscovery():
  272. # pylint: disable=global-variable-not-assigned
  273. global gDiscoveryProcess
  274. # pylint: enable=global-variable-not-assigned
  275. if gDiscoveryProcess is not None:
  276. gDiscoveryProcess.kill()
  277. def checkPluginCached(desc, ptype):
  278. pinfo = deepcopy(PyPluginInfo)
  279. pinfo['build'] = BINARY_NATIVE
  280. pinfo['type'] = ptype
  281. pinfo['hints'] = desc['hints']
  282. pinfo['name'] = desc['name']
  283. pinfo['label'] = desc['label']
  284. pinfo['maker'] = desc['maker']
  285. pinfo['category'] = getPluginCategoryAsString(desc['category'])
  286. pinfo['audio.ins'] = desc['audioIns']
  287. pinfo['audio.outs'] = desc['audioOuts']
  288. pinfo['cv.ins'] = desc['cvIns']
  289. pinfo['cv.outs'] = desc['cvOuts']
  290. pinfo['midi.ins'] = desc['midiIns']
  291. pinfo['midi.outs'] = desc['midiOuts']
  292. pinfo['parameters.ins'] = desc['parameterIns']
  293. pinfo['parameters.outs'] = desc['parameterOuts']
  294. if ptype == PLUGIN_LV2:
  295. pinfo['filename'], pinfo['label'] = pinfo['label'].split('\\' if WINDOWS else '/',1)
  296. elif ptype == PLUGIN_SFZ:
  297. pinfo['filename'] = pinfo['label']
  298. pinfo['label'] = pinfo['name']
  299. return pinfo
  300. def checkPluginLADSPA(filename, tool, wineSettings=None):
  301. return runCarlaDiscovery(PLUGIN_LADSPA, "LADSPA", filename, tool, wineSettings)
  302. def checkPluginDSSI(filename, tool, wineSettings=None):
  303. return runCarlaDiscovery(PLUGIN_DSSI, "DSSI", filename, tool, wineSettings)
  304. def checkPluginLV2(filename, tool, wineSettings=None):
  305. return runCarlaDiscovery(PLUGIN_LV2, "LV2", filename, tool, wineSettings)
  306. def checkPluginVST2(filename, tool, wineSettings=None):
  307. return runCarlaDiscovery(PLUGIN_VST2, "VST2", filename, tool, wineSettings)
  308. def checkPluginVST3(filename, tool, wineSettings=None):
  309. return runCarlaDiscovery(PLUGIN_VST3, "VST3", filename, tool, wineSettings)
  310. def checkPluginCLAP(filename, tool, wineSettings=None):
  311. return runCarlaDiscovery(PLUGIN_CLAP, "CLAP", filename, tool, wineSettings)
  312. def checkFileSF2(filename, tool):
  313. return runCarlaDiscovery(PLUGIN_SF2, "SF2", filename, tool)
  314. def checkFileSFZ(filename, tool):
  315. return runCarlaDiscovery(PLUGIN_SFZ, "SFZ", filename, tool)
  316. def checkAllPluginsAU(tool):
  317. return runCarlaDiscovery(PLUGIN_AU, "AU", ":all", tool)