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.

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