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.

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