External, Non-PPA KXStudio Repository
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.

6950 lines
192KB

  1. diff --git a/cmake/Modules/FindCarlaUtils.cmake b/cmake/Modules/FindCarlaUtils.cmake
  2. new file mode 100644
  3. index 000000000..1925a542e
  4. --- /dev/null
  5. +++ b/cmake/Modules/FindCarlaUtils.cmake
  6. @@ -0,0 +1,118 @@
  7. +# Once done these will be defined:
  8. +#
  9. +# CARLAUTILS_FOUND CARLAUTILS_INCLUDE_DIRS CARLAUTILS_LIBRARIES
  10. +
  11. +# QUIET
  12. +find_package(PkgConfig)
  13. +if(PKG_CONFIG_FOUND)
  14. + pkg_check_modules(_CARLAUTILS carla-utils)
  15. +endif()
  16. +
  17. +if(CMAKE_SIZEOF_VOID_P EQUAL 8)
  18. + set(_lib_suffix 64)
  19. +else()
  20. + set(_lib_suffix 32)
  21. +endif()
  22. +
  23. +find_path(
  24. + CARLAUTILS_INCLUDE_DIR
  25. + NAMES utils/CarlaBridgeUtils.hpp
  26. + HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_INCLUDE_DIRS}
  27. + PATHS /usr/include/carla /usr/local/include/carla /opt/local/include/carla /sw/include/carla
  28. + PATH_SUFFIXES carla)
  29. +
  30. +find_library(
  31. + CARLAUTILS_LIBRARY
  32. + NAMES carla_utils libcarla_utils
  33. + HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS}
  34. + PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla
  35. + PATH_SUFFIXES
  36. + lib${_lib_suffix}/carla
  37. + lib/carla
  38. + libs${_lib_suffix}/carla
  39. + libs/carla
  40. + bin${_lib_suffix}
  41. + bin
  42. + ../lib${_lib_suffix}/carla
  43. + ../lib/carla
  44. + ../libs${_lib_suffix}/carla
  45. + ../libs/carla
  46. + ../bin${_lib_suffix}
  47. + ../bin)
  48. +
  49. +# $<$<PLATFORM_ID:Windows>:.exe>
  50. +
  51. +find_program(
  52. + CARLAUTILS_BRIDGE_NATIVE
  53. + NAMES carla-bridge-native
  54. + HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS}
  55. + PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla
  56. + PATH_SUFFIXES
  57. + lib${_lib_suffix}/carla
  58. + lib/carla
  59. + libs${_lib_suffix}/carla
  60. + libs/carla
  61. + bin${_lib_suffix}
  62. + bin
  63. + ../lib${_lib_suffix}/carla
  64. + ../lib/carla
  65. + ../libs${_lib_suffix}/carla
  66. + ../libs/carla
  67. + ../bin${_lib_suffix}
  68. + ../bin)
  69. +
  70. +find_program(
  71. + CARLAUTILS_DISCOVERY_NATIVE
  72. + NAMES carla-discovery-native
  73. + HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS}
  74. + PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla
  75. + PATH_SUFFIXES
  76. + lib${_lib_suffix}/carla
  77. + lib/carla
  78. + libs${_lib_suffix}/carla
  79. + libs/carla
  80. + bin${_lib_suffix}
  81. + bin
  82. + ../lib${_lib_suffix}/carla
  83. + ../lib/carla
  84. + ../libs${_lib_suffix}/carla
  85. + ../libs/carla
  86. + ../bin${_lib_suffix}
  87. + ../bin)
  88. +
  89. +include(FindPackageHandleStandardArgs)
  90. +find_package_handle_standard_args(
  91. + CarlaUtils
  92. + FOUND_VAR CARLAUTILS_FOUND
  93. + REQUIRED_VARS CARLAUTILS_INCLUDE_DIR CARLAUTILS_LIBRARY CARLAUTILS_BRIDGE_NATIVE CARLAUTILS_DISCOVERY_NATIVE)
  94. +mark_as_advanced(CARLAUTILS_INCLUDE_DIR CARLAUTILS_LIBRARY CARLAUTILS_BRIDGE_NATIVE CARLAUTILS_DISCOVERY_NATIVE)
  95. +
  96. +if(CARLAUTILS_FOUND)
  97. + set(CARLAUTILS_INCLUDE_DIRS ${CARLAUTILS_INCLUDE_DIR} ${CARLAUTILS_INCLUDE_DIR}/includes
  98. + ${CARLAUTILS_INCLUDE_DIR}/utils)
  99. + set(CARLAUTILS_LIBRARIES ${CARLAUTILS_LIBRARY})
  100. +
  101. + if(NOT TARGET carla::utils)
  102. + if(IS_ABSOLUTE "${CARLAUTILS_LIBRARIES}")
  103. + add_library(carla::utils UNKNOWN IMPORTED GLOBAL)
  104. + set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CARLAUTILS_LIBRARIES}")
  105. + else()
  106. + add_library(carla::utils INTERFACE IMPORTED GLOBAL)
  107. + set_target_properties(carla::utils PROPERTIES IMPORTED_LIBNAME "${CARLAUTILS_LIBRARIES}")
  108. + endif()
  109. +
  110. + set_target_properties(carla::utils PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CARLAUTILS_INCLUDE_DIRS}")
  111. + endif()
  112. +
  113. + if(NOT TARGET carla::bridge-native)
  114. + add_executable(carla::bridge-native IMPORTED GLOBAL)
  115. + set_target_properties(carla::bridge-native PROPERTIES IMPORTED_LOCATION "${CARLAUTILS_BRIDGE_NATIVE}")
  116. + add_dependencies(carla::utils carla::bridge-native)
  117. + endif()
  118. +
  119. + if(NOT TARGET carla::discovery-native)
  120. + add_executable(carla::discovery-native IMPORTED GLOBAL)
  121. + set_target_properties(carla::discovery-native PROPERTIES IMPORTED_LOCATION "${CARLAUTILS_DISCOVERY_NATIVE}")
  122. + add_dependencies(carla::utils carla::discovery-native)
  123. + endif()
  124. +endif()
  125. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
  126. index f928f772c..3cf662a6c 100644
  127. --- a/plugins/CMakeLists.txt
  128. +++ b/plugins/CMakeLists.txt
  129. @@ -35,6 +35,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
  130. # Add plugins in alphabetical order to retain order in IDE projects
  131. add_subdirectory(aja)
  132. + add_subdirectory(carla)
  133. if(OS_WINDOWS OR OS_MACOS)
  134. add_subdirectory(coreaudio-encoder)
  135. endif()
  136. @@ -191,3 +192,4 @@ add_subdirectory(obs-transitions)
  137. add_subdirectory(rtmp-services)
  138. add_subdirectory(text-freetype2)
  139. add_subdirectory(aja)
  140. +add_subdirectory(carla)
  141. diff --git a/plugins/carla/CMakeLists.txt b/plugins/carla/CMakeLists.txt
  142. new file mode 100644
  143. index 000000000..fe819b74f
  144. --- /dev/null
  145. +++ b/plugins/carla/CMakeLists.txt
  146. @@ -0,0 +1,100 @@
  147. +cmake_minimum_required(VERSION 3.16...3.25)
  148. +
  149. +option(ENABLE_CARLA "Enable building OBS with carla plugin host" ON)
  150. +
  151. +if(NOT ENABLE_CARLA)
  152. + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
  153. + target_disable(carla)
  154. + else()
  155. + message(STATUS "OBS: DISABLED carla")
  156. + endif()
  157. + return()
  158. +endif()
  159. +
  160. +# Find carla utils
  161. +if(APPLE)
  162. + # even though cmake finds the framework, it refuses to work with it. let's force it
  163. + find_library(CARLAUTILS_FRAMEWORK NAMES carla-utils)
  164. + get_filename_component(CARLAUTILS_LIBRARY_DIR "${CARLAUTILS_FRAMEWORK}/.." ABSOLUTE)
  165. + message("Found carla-utils: ${CARLAUTILS_LIBRARY_DIR} ${CARLAUTILS_FRAMEWORK}")
  166. + add_library(carla::utils INTERFACE IMPORTED)
  167. + set_target_properties(
  168. + carla::utils
  169. + PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CARLAUTILS_FRAMEWORK}/Headers"
  170. + INTERFACE_LINK_OPTIONS "-F${CARLAUTILS_LIBRARY_DIR}"
  171. + INTERFACE_LINK_LIBRARIES $<LINK_LIBRARY:FRAMEWORK,carla-utils.framework>)
  172. +else()
  173. + find_package(CarlaUtils)
  174. + if(NOT CARLAUTILS_FOUND)
  175. + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
  176. + target_disable(carla)
  177. + else()
  178. + message(STATUS "OBS: DISABLED carla (carla-utils library not found)")
  179. + endif()
  180. + return()
  181. + endif()
  182. +endif()
  183. +
  184. +# Find Qt
  185. +find_qt(COMPONENTS Core Widgets)
  186. +
  187. +# Setup carla-bridge target
  188. +add_library(carla-bridge MODULE)
  189. +add_library(OBS::carla-bridge ALIAS carla-bridge)
  190. +
  191. +target_compile_definitions(carla-bridge PRIVATE CARLA_MODULE_ID="carla-bridge" CARLA_MODULE_NAME="Audio Plugin"
  192. + CARLA_UTILS_USE_QT)
  193. +
  194. +target_link_libraries(carla-bridge PRIVATE carla::utils OBS::libobs Qt::Core Qt::Widgets $<$<C_COMPILER_ID:GNU>:dl>)
  195. +
  196. +target_sources(
  197. + carla-bridge
  198. + PRIVATE carla.c
  199. + carla-bridge.cpp
  200. + carla-bridge-wrapper.cpp
  201. + common.c
  202. + pluginlistdialog.cpp
  203. + pluginrefreshdialog.hpp
  204. + qtutils.cpp)
  205. +
  206. +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
  207. + set_target_properties_obs(
  208. + carla-bridge
  209. + PROPERTIES AUTOMOC ON
  210. + AUTOUIC ON
  211. + AUTORCC ON
  212. + FOLDER plugins
  213. + PREFIX "")
  214. +else()
  215. + set_target_properties(
  216. + carla-bridge
  217. + PROPERTIES AUTOMOC ON
  218. + AUTOUIC ON
  219. + AUTORCC ON
  220. + FOLDER plugins
  221. + PREFIX "")
  222. + setup_plugin_target(carla-bridge)
  223. +endif()
  224. +
  225. +# Setup carla-patchbay target (only available for certain systems)
  226. +if(PKGCONFIG_FOUND AND NOT (OS_MACOS OR OS_WINDOWS))
  227. + pkg_check_modules(carla-host-plugin IMPORTED_TARGET QUIET carla-host-plugin)
  228. + if(carla-host-plugin_FOUND)
  229. + add_library(carla-patchbay MODULE)
  230. + add_library(OBS::carla-patchbay ALIAS carla-patchbay)
  231. +
  232. + target_compile_definitions(carla-patchbay PRIVATE CARLA_MODULE_ID="carla-patchbay"
  233. + CARLA_MODULE_NAME="Carla Patchbay")
  234. +
  235. + target_link_libraries(carla-patchbay PRIVATE OBS::libobs Qt::Core Qt::Widgets PkgConfig::carla-host-plugin)
  236. +
  237. + target_sources(carla-patchbay PRIVATE carla.c carla-patchbay-wrapper.c common.c qtutils.cpp)
  238. +
  239. + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
  240. + set_target_properties_obs(carla-patchbay PROPERTIES FOLDER plugins PREFIX "")
  241. + else()
  242. + set_target_properties(carla-patchbay PROPERTIES FOLDER plugins PREFIX "")
  243. + setup_plugin_target(carla-patchbay)
  244. + endif()
  245. + endif()
  246. +endif()
  247. diff --git a/plugins/carla/carla-bridge-wrapper.cpp b/plugins/carla/carla-bridge-wrapper.cpp
  248. new file mode 100644
  249. index 000000000..0a7d91dec
  250. --- /dev/null
  251. +++ b/plugins/carla/carla-bridge-wrapper.cpp
  252. @@ -0,0 +1,577 @@
  253. +/*
  254. + * Carla plugin for OBS
  255. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  256. + * SPDX-License-Identifier: GPL-2.0-or-later
  257. + */
  258. +
  259. +// needed for strcasestr
  260. +#if !defined(_GNU_SOURCE) && !defined(_WIN32)
  261. +#define _GNU_SOURCE
  262. +#endif
  263. +
  264. +#include <CarlaBackendUtils.hpp>
  265. +#include <CarlaBinaryUtils.hpp>
  266. +
  267. +#include <QtCore/QFileInfo>
  268. +#include <QtCore/QString>
  269. +
  270. +#include <util/platform.h>
  271. +
  272. +#include "carla-bridge.hpp"
  273. +#include "carla-wrapper.h"
  274. +#include "common.h"
  275. +#include "qtutils.h"
  276. +
  277. +#if CARLA_VERSION_HEX >= 0x020591
  278. +#define CARLA_2_6_FEATURES
  279. +#endif
  280. +
  281. +// ----------------------------------------------------------------------------
  282. +// private data methods
  283. +
  284. +struct carla_priv : carla_bridge_callback {
  285. + obs_source_t *source = nullptr;
  286. + uint32_t bufferSize = 0;
  287. + double sampleRate = 0;
  288. +
  289. + // update properties when timeout is reached, 0 means do nothing
  290. + uint64_t update_request = 0;
  291. +
  292. + carla_bridge bridge;
  293. +
  294. + void bridge_parameter_changed(uint index, float value) override
  295. + {
  296. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  297. + param_index_to_name(index, pname);
  298. +
  299. + obs_data_t *settings = obs_source_get_settings(source);
  300. +
  301. + /**/ if (bridge.paramDetails[index].hints &
  302. + PARAMETER_IS_BOOLEAN)
  303. + obs_data_set_bool(settings, pname, value > 0.5f);
  304. + else if (bridge.paramDetails[index].hints &
  305. + PARAMETER_IS_INTEGER)
  306. + obs_data_set_int(settings, pname, value);
  307. + else
  308. + obs_data_set_double(settings, pname, value);
  309. +
  310. + obs_data_release(settings);
  311. +
  312. + postpone_update_request(&update_request);
  313. + }
  314. +};
  315. +
  316. +// ----------------------------------------------------------------------------
  317. +// carla + obs integration methods
  318. +
  319. +struct carla_priv *carla_priv_create(obs_source_t *source,
  320. + enum buffer_size_mode bufsize,
  321. + uint32_t srate)
  322. +{
  323. + struct carla_priv *priv = new struct carla_priv;
  324. + if (priv == NULL)
  325. + return NULL;
  326. +
  327. + priv->bridge.callback = priv;
  328. + priv->source = source;
  329. + priv->bufferSize = bufsize_mode_to_frames(bufsize);
  330. + priv->sampleRate = srate;
  331. +
  332. + return priv;
  333. +}
  334. +
  335. +void carla_priv_destroy(struct carla_priv *priv)
  336. +{
  337. + priv->bridge.cleanup();
  338. + delete priv;
  339. +}
  340. +
  341. +// ----------------------------------------------------------------------------
  342. +
  343. +void carla_priv_activate(struct carla_priv *priv)
  344. +{
  345. + priv->bridge.activate();
  346. +}
  347. +
  348. +void carla_priv_deactivate(struct carla_priv *priv)
  349. +{
  350. + priv->bridge.deactivate();
  351. +}
  352. +
  353. +void carla_priv_process_audio(struct carla_priv *priv,
  354. + float *buffers[MAX_AV_PLANES], uint32_t frames)
  355. +{
  356. + priv->bridge.process(buffers, frames);
  357. +}
  358. +
  359. +void carla_priv_idle(struct carla_priv *priv)
  360. +{
  361. + priv->bridge.idle();
  362. + handle_update_request(priv->source, &priv->update_request);
  363. +}
  364. +
  365. +// ----------------------------------------------------------------------------
  366. +
  367. +void carla_priv_save(struct carla_priv *priv, obs_data_t *settings)
  368. +{
  369. + priv->bridge.save_and_wait();
  370. +
  371. + obs_data_set_string(settings, "btype",
  372. + getBinaryTypeAsString(priv->bridge.info.btype));
  373. + obs_data_set_string(settings, "ptype",
  374. + getPluginTypeAsString(priv->bridge.info.ptype));
  375. + obs_data_set_string(settings, "filename", priv->bridge.info.filename);
  376. + obs_data_set_string(settings, "label", priv->bridge.info.label);
  377. + obs_data_set_int(settings, "uniqueId",
  378. + static_cast<long long>(priv->bridge.info.uniqueId));
  379. +
  380. + if (!priv->bridge.customData.empty()) {
  381. + obs_data_array_t *array = obs_data_array_create();
  382. +
  383. + for (CustomData &cdata : priv->bridge.customData) {
  384. + obs_data_t *data = obs_data_create();
  385. + obs_data_set_string(data, "type", cdata.type);
  386. + obs_data_set_string(data, "key", cdata.key);
  387. + obs_data_set_string(data, "value", cdata.value);
  388. + obs_data_array_push_back(array, data);
  389. + obs_data_release(data);
  390. + }
  391. +
  392. + obs_data_set_array(settings, PROP_CUSTOM_DATA, array);
  393. + obs_data_array_release(array);
  394. + } else {
  395. + obs_data_erase(settings, PROP_CUSTOM_DATA);
  396. + }
  397. +
  398. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  399. +
  400. + if ((priv->bridge.info.options & PLUGIN_OPTION_USE_CHUNKS) &&
  401. + !priv->bridge.chunk.isEmpty()) {
  402. + char *b64ptr = CarlaString::asBase64(priv->bridge.chunk.data(),
  403. + priv->bridge.chunk.size())
  404. + .releaseBufferPointer();
  405. + const CarlaString b64chunk(b64ptr, false);
  406. + obs_data_set_string(settings, PROP_CHUNK, b64chunk.buffer());
  407. +
  408. + for (uint32_t i = 0;
  409. + i < priv->bridge.paramCount && i < MAX_PARAMS; ++i) {
  410. + const carla_param_data &param(
  411. + priv->bridge.paramDetails[i]);
  412. +
  413. + if ((param.hints & PARAMETER_IS_ENABLED) == 0)
  414. + continue;
  415. +
  416. + param_index_to_name(i, pname);
  417. + obs_data_erase(settings, pname);
  418. + }
  419. + } else {
  420. + obs_data_erase(settings, PROP_CHUNK);
  421. +
  422. + for (uint32_t i = 0;
  423. + i < priv->bridge.paramCount && i < MAX_PARAMS; ++i) {
  424. + const carla_param_data &param(
  425. + priv->bridge.paramDetails[i]);
  426. +
  427. + if ((param.hints & PARAMETER_IS_ENABLED) == 0)
  428. + continue;
  429. +
  430. + param_index_to_name(i, pname);
  431. +
  432. + if (param.hints & PARAMETER_IS_BOOLEAN) {
  433. + obs_data_set_bool(settings, pname,
  434. + carla_isEqual(param.value,
  435. + param.max));
  436. + } else if (param.hints & PARAMETER_IS_INTEGER) {
  437. + obs_data_set_int(settings, pname, param.value);
  438. + } else {
  439. + obs_data_set_double(settings, pname,
  440. + param.value);
  441. + }
  442. + }
  443. + }
  444. +}
  445. +
  446. +void carla_priv_load(struct carla_priv *priv, obs_data_t *settings)
  447. +{
  448. + const BinaryType btype =
  449. + getBinaryTypeFromString(obs_data_get_string(settings, "btype"));
  450. + const PluginType ptype =
  451. + getPluginTypeFromString(obs_data_get_string(settings, "ptype"));
  452. +
  453. + // abort early if both of these are null, likely from an empty config
  454. + if (btype == BINARY_NONE && ptype == PLUGIN_NONE)
  455. + return;
  456. +
  457. + const char *const filename = obs_data_get_string(settings, "filename");
  458. + const char *const label = obs_data_get_string(settings, "label");
  459. + const int64_t uniqueId =
  460. + static_cast<int64_t>(obs_data_get_int(settings, "uniqueId"));
  461. +
  462. + priv->bridge.cleanup();
  463. + priv->bridge.init(priv->bufferSize, priv->sampleRate);
  464. +
  465. + if (!priv->bridge.start(btype, ptype, label, filename, uniqueId)) {
  466. + carla_show_error_dialog("Failed to load plugin",
  467. + priv->bridge.get_last_error());
  468. + return;
  469. + }
  470. +
  471. + obs_data_array_t *array =
  472. + obs_data_get_array(settings, PROP_CUSTOM_DATA);
  473. + if (array) {
  474. + const size_t count = obs_data_array_count(array);
  475. + for (size_t i = 0; i < count; ++i) {
  476. + obs_data_t *data = obs_data_array_item(array, i);
  477. + const char *type = obs_data_get_string(data, "type");
  478. + const char *key = obs_data_get_string(data, "key");
  479. + const char *value = obs_data_get_string(data, "value");
  480. + priv->bridge.add_custom_data(type, key, value);
  481. + }
  482. + priv->bridge.custom_data_loaded();
  483. + }
  484. +
  485. + if (priv->bridge.info.options & PLUGIN_OPTION_USE_CHUNKS) {
  486. + const char *b64chunk =
  487. + obs_data_get_string(settings, PROP_CHUNK);
  488. + priv->bridge.load_chunk(b64chunk);
  489. + } else {
  490. + for (uint32_t i = 0; i < priv->bridge.paramCount; ++i) {
  491. + const carla_param_data &param(
  492. + priv->bridge.paramDetails[i]);
  493. +
  494. + priv->bridge.set_value(i, param.value);
  495. + }
  496. + }
  497. +
  498. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  499. +
  500. + for (uint32_t i = 0; i < priv->bridge.paramCount && i < MAX_PARAMS;
  501. + ++i) {
  502. + const carla_param_data &param(priv->bridge.paramDetails[i]);
  503. +
  504. + if ((param.hints & PARAMETER_IS_ENABLED) == 0)
  505. + continue;
  506. +
  507. + param_index_to_name(i, pname);
  508. +
  509. + if (param.hints & PARAMETER_IS_BOOLEAN) {
  510. + obs_data_set_bool(settings, pname,
  511. + carla_isEqual(param.value,
  512. + param.max));
  513. + } else if (param.hints & PARAMETER_IS_INTEGER) {
  514. + obs_data_set_int(settings, pname, param.value);
  515. + } else {
  516. + obs_data_set_double(settings, pname, param.value);
  517. + }
  518. + }
  519. +
  520. + priv->bridge.activate();
  521. +}
  522. +
  523. +// ----------------------------------------------------------------------------
  524. +
  525. +uint32_t carla_priv_get_num_channels(struct carla_priv *priv)
  526. +{
  527. + return std::max(priv->bridge.info.numAudioIns,
  528. + priv->bridge.info.numAudioOuts);
  529. +}
  530. +
  531. +void carla_priv_set_buffer_size(struct carla_priv *priv,
  532. + enum buffer_size_mode bufsize)
  533. +{
  534. + priv->bridge.set_buffer_size(bufsize_mode_to_frames(bufsize));
  535. +}
  536. +
  537. +// ----------------------------------------------------------------------------
  538. +
  539. +static bool carla_post_load_callback(struct carla_priv *priv,
  540. + obs_properties_t *props)
  541. +{
  542. + obs_source_t *source = priv->source;
  543. + obs_data_t *settings = obs_source_get_settings(source);
  544. + remove_all_props(props, settings);
  545. + carla_priv_readd_properties(priv, props, true);
  546. + obs_data_release(settings);
  547. + return true;
  548. +}
  549. +
  550. +static bool carla_priv_load_file_callback(obs_properties_t *props,
  551. + obs_property_t *property, void *data)
  552. +{
  553. + UNUSED_PARAMETER(property);
  554. +
  555. + struct carla_priv *priv = static_cast<struct carla_priv *>(data);
  556. +
  557. + char *filename = carla_qt_file_dialog(
  558. + false, false, obs_module_text("Load File"), NULL);
  559. +
  560. + if (filename == NULL)
  561. + return false;
  562. +
  563. +#ifndef _WIN32
  564. + // truncate plug.vst3/Contents/<plat>/plug.so -> plug.vst3
  565. + if (char *const vst3str = strcasestr(filename, ".vst3/Contents/"))
  566. + vst3str[5] = '\0';
  567. +#endif
  568. +
  569. + BinaryType btype;
  570. + PluginType ptype;
  571. +
  572. + {
  573. + const QFileInfo fileInfo(QString::fromUtf8(filename));
  574. + const QString extension(fileInfo.suffix());
  575. +
  576. +#if defined(CARLA_OS_MAC)
  577. + if (extension == "vst")
  578. + ptype = PLUGIN_VST2;
  579. +#else
  580. + if (extension == "dll" || extension == "so")
  581. + ptype = PLUGIN_VST2;
  582. +#endif
  583. + else if (extension == "vst3")
  584. + ptype = PLUGIN_VST3;
  585. +#ifdef CARLA_2_6_FEATURES
  586. + else if (extension == "clap")
  587. + ptype = PLUGIN_CLAP;
  588. +#endif
  589. + else {
  590. + carla_show_error_dialog("Failed to load file",
  591. + "Unknown file type");
  592. + return false;
  593. + }
  594. +
  595. + btype = getBinaryTypeFromFile(filename);
  596. + }
  597. +
  598. + priv->bridge.cleanup();
  599. + priv->bridge.init(priv->bufferSize, priv->sampleRate);
  600. +
  601. + if (priv->bridge.start(btype, ptype, "(none)", filename, 0))
  602. + priv->bridge.activate();
  603. + else
  604. + carla_show_error_dialog("Failed to load file",
  605. + priv->bridge.get_last_error());
  606. +
  607. + return carla_post_load_callback(priv, props);
  608. +}
  609. +
  610. +static bool carla_priv_select_plugin_callback(obs_properties_t *props,
  611. + obs_property_t *property,
  612. + void *data)
  613. +{
  614. + UNUSED_PARAMETER(property);
  615. +
  616. + struct carla_priv *priv = static_cast<struct carla_priv *>(data);
  617. +
  618. + const PluginListDialogResults *plugin = carla_exec_plugin_list_dialog();
  619. +
  620. + if (plugin == NULL)
  621. + return false;
  622. +
  623. + priv->bridge.cleanup();
  624. + priv->bridge.init(priv->bufferSize, priv->sampleRate);
  625. +
  626. + if (priv->bridge.start(static_cast<BinaryType>(plugin->build),
  627. + static_cast<PluginType>(plugin->type),
  628. + plugin->label, plugin->filename,
  629. + plugin->uniqueId))
  630. + priv->bridge.activate();
  631. + else
  632. + carla_show_error_dialog("Failed to load plugin",
  633. + priv->bridge.get_last_error());
  634. +
  635. + return carla_post_load_callback(priv, props);
  636. +}
  637. +
  638. +static bool carla_priv_reload_callback(obs_properties_t *props,
  639. + obs_property_t *property, void *data)
  640. +{
  641. + UNUSED_PARAMETER(property);
  642. +
  643. + struct carla_priv *priv = static_cast<struct carla_priv *>(data);
  644. +
  645. + if (priv->bridge.is_running()) {
  646. + priv->bridge.reload();
  647. + return true;
  648. + }
  649. +
  650. + if (priv->bridge.info.btype == BINARY_NONE)
  651. + return false;
  652. +
  653. + // cache relevant information for later
  654. + const BinaryType btype = priv->bridge.info.btype;
  655. + const PluginType ptype = priv->bridge.info.ptype;
  656. + const int64_t uniqueId = priv->bridge.info.uniqueId;
  657. + char *const label = priv->bridge.info.label.releaseBufferPointer();
  658. + char *const filename =
  659. + priv->bridge.info.filename.releaseBufferPointer();
  660. +
  661. + priv->bridge.cleanup(false);
  662. + priv->bridge.init(priv->bufferSize, priv->sampleRate);
  663. +
  664. + if (priv->bridge.start(btype, ptype, label, filename, uniqueId)) {
  665. + priv->bridge.restore_state();
  666. + priv->bridge.activate();
  667. + } else {
  668. + carla_show_error_dialog("Failed to reload plugin",
  669. + priv->bridge.get_last_error());
  670. + }
  671. +
  672. + std::free(label);
  673. + std::free(filename);
  674. +
  675. + return carla_post_load_callback(priv, props);
  676. +}
  677. +
  678. +static bool carla_priv_show_gui_callback(obs_properties_t *props,
  679. + obs_property_t *property, void *data)
  680. +{
  681. + UNUSED_PARAMETER(props);
  682. + UNUSED_PARAMETER(property);
  683. +
  684. + struct carla_priv *priv = static_cast<struct carla_priv *>(data);
  685. +
  686. + priv->bridge.show_ui();
  687. +
  688. + return false;
  689. +}
  690. +
  691. +static bool carla_priv_param_changed(void *data, obs_properties_t *props,
  692. + obs_property_t *property,
  693. + obs_data_t *settings)
  694. +{
  695. + UNUSED_PARAMETER(props);
  696. +
  697. + struct carla_priv *priv = static_cast<struct carla_priv *>(data);
  698. +
  699. + const char *const pname = obs_property_name(property);
  700. + if (pname == NULL)
  701. + return false;
  702. +
  703. + const char *pname2 = pname + 1;
  704. + while (*pname2 == '0')
  705. + ++pname2;
  706. +
  707. + const int pindex = atoi(pname2);
  708. +
  709. + if (pindex < 0 || pindex >= (int)priv->bridge.paramCount)
  710. + return false;
  711. +
  712. + const uint index = static_cast<uint>(pindex);
  713. +
  714. + const float min = priv->bridge.paramDetails[index].min;
  715. + const float max = priv->bridge.paramDetails[index].max;
  716. +
  717. + float value;
  718. + switch (obs_property_get_type(property)) {
  719. + case OBS_PROPERTY_BOOL:
  720. + value = obs_data_get_bool(settings, pname) ? max : min;
  721. + break;
  722. + case OBS_PROPERTY_INT:
  723. + value = obs_data_get_int(settings, pname);
  724. + if (value < min)
  725. + value = min;
  726. + else if (value > max)
  727. + value = max;
  728. + break;
  729. + case OBS_PROPERTY_FLOAT:
  730. + value = obs_data_get_double(settings, pname);
  731. + if (value < min)
  732. + value = min;
  733. + else if (value > max)
  734. + value = max;
  735. + break;
  736. + default:
  737. + return false;
  738. + }
  739. +
  740. + priv->bridge.set_value(index, value);
  741. +
  742. + return false;
  743. +}
  744. +
  745. +void carla_priv_readd_properties(struct carla_priv *priv,
  746. + obs_properties_t *props, bool reset)
  747. +{
  748. + if (!reset) {
  749. + obs_properties_add_button2(props, PROP_SELECT_PLUGIN,
  750. + obs_module_text("Select plugin..."),
  751. + carla_priv_select_plugin_callback,
  752. + priv);
  753. +
  754. + obs_properties_add_button2(props, PROP_LOAD_FILE,
  755. + obs_module_text("Load file..."),
  756. + carla_priv_load_file_callback, priv);
  757. + }
  758. +
  759. + if (priv->bridge.info.ptype != PLUGIN_NONE)
  760. + obs_properties_add_button2(props, PROP_RELOAD_PLUGIN,
  761. + obs_module_text("Reload"),
  762. + carla_priv_reload_callback, priv);
  763. +
  764. + if (priv->bridge.info.hints & PLUGIN_HAS_CUSTOM_UI)
  765. + obs_properties_add_button2(props, PROP_SHOW_GUI,
  766. + obs_module_text("Show custom GUI"),
  767. + carla_priv_show_gui_callback, priv);
  768. +
  769. + obs_data_t *settings = obs_source_get_settings(priv->source);
  770. +
  771. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  772. +
  773. + for (uint32_t i = 0; i < priv->bridge.paramCount && i < MAX_PARAMS;
  774. + ++i) {
  775. + const carla_param_data &param(priv->bridge.paramDetails[i]);
  776. +
  777. + if ((param.hints & PARAMETER_IS_ENABLED) == 0)
  778. + continue;
  779. +
  780. + obs_property_t *prop;
  781. + param_index_to_name(i, pname);
  782. +
  783. + if (param.hints & PARAMETER_IS_BOOLEAN) {
  784. + prop = obs_properties_add_bool(props, pname,
  785. + param.name);
  786. +
  787. + obs_data_set_default_bool(settings, pname,
  788. + carla_isEqual(param.def,
  789. + param.max));
  790. +
  791. + if (reset)
  792. + obs_data_set_bool(settings, pname,
  793. + carla_isEqual(param.value,
  794. + param.max));
  795. + } else if (param.hints & PARAMETER_IS_INTEGER) {
  796. + prop = obs_properties_add_int_slider(
  797. + props, pname, param.name, param.min, param.max,
  798. + param.step);
  799. +
  800. + obs_data_set_default_int(settings, pname, param.def);
  801. +
  802. + if (param.unit.isNotEmpty())
  803. + obs_property_int_set_suffix(prop, param.unit);
  804. +
  805. + if (reset)
  806. + obs_data_set_int(settings, pname, param.value);
  807. + } else {
  808. + prop = obs_properties_add_float_slider(
  809. + props, pname, param.name, param.min, param.max,
  810. + param.step);
  811. +
  812. + obs_data_set_default_double(settings, pname, param.def);
  813. +
  814. + if (param.unit.isNotEmpty())
  815. + obs_property_float_set_suffix(prop, param.unit);
  816. +
  817. + if (reset)
  818. + obs_data_set_double(settings, pname,
  819. + param.value);
  820. + }
  821. +
  822. + obs_property_set_modified_callback2(
  823. + prop, carla_priv_param_changed, priv);
  824. + }
  825. +
  826. + obs_data_release(settings);
  827. +}
  828. +
  829. +// ----------------------------------------------------------------------------
  830. diff --git a/plugins/carla/carla-bridge.cpp b/plugins/carla/carla-bridge.cpp
  831. new file mode 100644
  832. index 000000000..5f20b7c01
  833. --- /dev/null
  834. +++ b/plugins/carla/carla-bridge.cpp
  835. @@ -0,0 +1,1405 @@
  836. +/*
  837. + * Carla plugin for OBS
  838. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  839. + * SPDX-License-Identifier: GPL-2.0-or-later
  840. + */
  841. +
  842. +#include <CarlaBackendUtils.hpp>
  843. +#include <CarlaBase64Utils.hpp>
  844. +#include <CarlaBinaryUtils.hpp>
  845. +
  846. +#ifdef CARLA_OS_MAC
  847. +#include <CarlaMacUtils.hpp>
  848. +#endif
  849. +
  850. +#include <QtCore/QCoreApplication>
  851. +#include <QtCore/QDir>
  852. +#include <QtCore/QFile>
  853. +#include <QtCore/QFileInfo>
  854. +#include <QtWidgets/QMessageBox>
  855. +
  856. +#include <util/platform.h>
  857. +
  858. +#include "carla-bridge.hpp"
  859. +
  860. +#include "common.h"
  861. +#include "qtutils.h"
  862. +
  863. +#if defined(CARLA_OS_MAC) && defined(__aarch64__)
  864. +// ----------------------------------------------------------------------------
  865. +// check the header of a plugin binary to see if it matches mach 64bit + intel
  866. +
  867. +static bool isIntel64BitPlugin(const char *const pluginBundle)
  868. +{
  869. + const char *const pluginBinary = findBinaryInBundle(pluginBundle);
  870. + CARLA_SAFE_ASSERT_RETURN(pluginBinary != nullptr, false);
  871. +
  872. + FILE *const f = fopen(pluginBinary, "r");
  873. + CARLA_SAFE_ASSERT_RETURN(f != nullptr, false);
  874. +
  875. + bool match = false;
  876. + uint8_t buf[8];
  877. + if (fread(buf, sizeof(buf), 1, f) == 1) {
  878. + const uint32_t magic = *(uint32_t *)buf;
  879. + if (magic == 0xfeedfacf && buf[4] == 0x07)
  880. + match = true;
  881. + }
  882. +
  883. + fclose(f);
  884. + return match;
  885. +}
  886. +#endif
  887. +
  888. +// ----------------------------------------------------------------------------
  889. +// utility class for reading and deleting incoming bridge text in RAII fashion
  890. +
  891. +struct BridgeTextReader {
  892. + char *text = nullptr;
  893. +
  894. + BridgeTextReader(BridgeNonRtServerControl &nonRtServerCtrl)
  895. + {
  896. + const uint32_t size = nonRtServerCtrl.readUInt();
  897. + CARLA_SAFE_ASSERT_RETURN(size != 0, );
  898. +
  899. + text = new char[size + 1];
  900. + nonRtServerCtrl.readCustomData(text, size);
  901. + text[size] = '\0';
  902. + }
  903. +
  904. + BridgeTextReader(BridgeNonRtServerControl &nonRtServerCtrl,
  905. + const uint32_t size)
  906. + {
  907. + text = new char[size + 1];
  908. +
  909. + if (size != 0)
  910. + nonRtServerCtrl.readCustomData(text, size);
  911. +
  912. + text[size] = '\0';
  913. + }
  914. +
  915. + ~BridgeTextReader() noexcept { delete[] text; }
  916. +
  917. + CARLA_DECLARE_NON_COPYABLE(BridgeTextReader)
  918. +};
  919. +
  920. +// ----------------------------------------------------------------------------
  921. +// custom bridge process implementation
  922. +
  923. +BridgeProcess::BridgeProcess(const char *const shmIds)
  924. +{
  925. + // move object to the correct/expected thread
  926. + moveToThread(qApp->thread());
  927. +
  928. + // setup environment for client side
  929. + QProcessEnvironment env(QProcessEnvironment::systemEnvironment());
  930. + env.insert("ENGINE_BRIDGE_SHM_IDS", shmIds);
  931. + setProcessEnvironment(env);
  932. +}
  933. +
  934. +void BridgeProcess::start()
  935. +{
  936. + // pass-through all bridge output
  937. + setInputChannelMode(QProcess::ForwardedInputChannel);
  938. + setProcessChannelMode(QProcess::ForwardedChannels);
  939. + QProcess::start(QIODevice::Unbuffered | QIODevice::ReadOnly);
  940. +}
  941. +
  942. +// NOTE: process instance cannot be used after this!
  943. +void BridgeProcess::stop()
  944. +{
  945. + if (state() != QProcess::NotRunning) {
  946. + terminate();
  947. + waitForFinished(2000);
  948. +
  949. + if (state() != QProcess::NotRunning) {
  950. + blog(LOG_INFO,
  951. + "[carla] bridge refused to close, force kill now");
  952. + kill();
  953. + }
  954. + }
  955. +
  956. + deleteLater();
  957. +}
  958. +
  959. +// ----------------------------------------------------------------------------
  960. +
  961. +bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate)
  962. +{
  963. + // add entropy to rand calls, used for finding unused paths
  964. + std::srand(static_cast<uint>(os_gettime_ns() / 1000000));
  965. +
  966. + // initialize the several communication channels
  967. + if (!audiopool.initializeServer()) {
  968. + blog(LOG_WARNING,
  969. + "[carla] Failed to initialize shared memory audio pool");
  970. + goto fail1;
  971. + }
  972. +
  973. + if (!rtClientCtrl.initializeServer()) {
  974. + blog(LOG_WARNING,
  975. + "[carla] Failed to initialize RT client control");
  976. + goto fail2;
  977. + }
  978. +
  979. + if (!nonRtClientCtrl.initializeServer()) {
  980. + blog(LOG_WARNING,
  981. + "[carla] Failed to initialize Non-RT client control");
  982. + goto fail3;
  983. + }
  984. +
  985. + if (!nonRtServerCtrl.initializeServer()) {
  986. + blog(LOG_WARNING,
  987. + "[carla] Failed to initialize Non-RT server control");
  988. + goto fail4;
  989. + }
  990. +
  991. + // resize audiopool data to be as large as needed
  992. + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES);
  993. +
  994. + // clear realtime data
  995. + rtClientCtrl.data->procFlags = 0;
  996. + carla_zeroStruct(rtClientCtrl.data->timeInfo);
  997. + carla_zeroBytes(rtClientCtrl.data->midiOut,
  998. + kBridgeRtClientDataMidiOutSize);
  999. +
  1000. + // clear ringbuffers
  1001. + rtClientCtrl.clearData();
  1002. + nonRtClientCtrl.clearData();
  1003. + nonRtServerCtrl.clearData();
  1004. +
  1005. + // first ever message is bridge API version
  1006. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientVersion);
  1007. + nonRtClientCtrl.writeUInt(CARLA_PLUGIN_BRIDGE_API_VERSION_CURRENT);
  1008. +
  1009. + // then expected size for each data channel
  1010. + nonRtClientCtrl.writeUInt(
  1011. + static_cast<uint32_t>(sizeof(BridgeRtClientData)));
  1012. + nonRtClientCtrl.writeUInt(
  1013. + static_cast<uint32_t>(sizeof(BridgeNonRtClientData)));
  1014. + nonRtClientCtrl.writeUInt(
  1015. + static_cast<uint32_t>(sizeof(BridgeNonRtServerData)));
  1016. +
  1017. + // and finally the initial buffer size and sample rate
  1018. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientInitialSetup);
  1019. + nonRtClientCtrl.writeUInt(maxBufferSize);
  1020. + nonRtClientCtrl.writeDouble(sampleRate);
  1021. +
  1022. + nonRtClientCtrl.commitWrite();
  1023. +
  1024. + // report audiopool size to client side
  1025. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool);
  1026. + rtClientCtrl.writeULong(static_cast<uint64_t>(audiopool.dataSize));
  1027. + rtClientCtrl.commitWrite();
  1028. +
  1029. + // FIXME
  1030. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize);
  1031. + rtClientCtrl.writeUInt(maxBufferSize);
  1032. + rtClientCtrl.commitWrite();
  1033. +
  1034. + bufferSize = maxBufferSize;
  1035. + return true;
  1036. +
  1037. +fail4:
  1038. + nonRtClientCtrl.clear();
  1039. +
  1040. +fail3:
  1041. + rtClientCtrl.clear();
  1042. +
  1043. +fail2:
  1044. + audiopool.clear();
  1045. +
  1046. +fail1:
  1047. + setLastError("Failed to initialize shared memory");
  1048. + return false;
  1049. +}
  1050. +
  1051. +void carla_bridge::cleanup(const bool clearPluginData)
  1052. +{
  1053. + // signal to stop processing audio
  1054. + const bool wasActivated = activated;
  1055. + ready = activated = false;
  1056. +
  1057. + // stop bridge process
  1058. + if (childprocess != nullptr) {
  1059. + // make `childprocess` null first
  1060. + BridgeProcess *proc = childprocess;
  1061. + childprocess = nullptr;
  1062. +
  1063. + // if process is running, ask nicely for it to close
  1064. + if (proc->state() != QProcess::NotRunning) {
  1065. + {
  1066. + const CarlaMutexLocker cml(
  1067. + nonRtClientCtrl.mutex);
  1068. +
  1069. + if (wasActivated) {
  1070. + nonRtClientCtrl.writeOpcode(
  1071. + kPluginBridgeNonRtClientDeactivate);
  1072. + nonRtClientCtrl.commitWrite();
  1073. + }
  1074. +
  1075. + nonRtClientCtrl.writeOpcode(
  1076. + kPluginBridgeNonRtClientQuit);
  1077. + nonRtClientCtrl.commitWrite();
  1078. + }
  1079. +
  1080. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientQuit);
  1081. + rtClientCtrl.commitWrite();
  1082. +
  1083. + if (!timedErr && !timedOut)
  1084. + wait("stopping", 3000);
  1085. + } else {
  1086. + // log warning in case plugin process crashed
  1087. + if (proc->exitStatus() == QProcess::CrashExit) {
  1088. + blog(LOG_WARNING, "[carla] bridge crashed");
  1089. +
  1090. + if (!clearPluginData) {
  1091. + carla_show_error_dialog(
  1092. + "A plugin bridge has crashed",
  1093. + info.name);
  1094. + }
  1095. + }
  1096. + }
  1097. +
  1098. + // let Qt do the final cleanup on the main thread
  1099. + QMetaObject::invokeMethod(proc, "stop");
  1100. + }
  1101. +
  1102. + // cleanup shared memory bits
  1103. + nonRtServerCtrl.clear();
  1104. + nonRtClientCtrl.clear();
  1105. + rtClientCtrl.clear();
  1106. + audiopool.clear();
  1107. +
  1108. + // clear cached plugin data if requested
  1109. + if (clearPluginData) {
  1110. + info.clear();
  1111. + chunk.clear();
  1112. + clear_custom_data();
  1113. + }
  1114. +}
  1115. +
  1116. +bool carla_bridge::start(const BinaryType btype, const PluginType ptype,
  1117. + const char *label, const char *filename,
  1118. + const int64_t uniqueId)
  1119. +{
  1120. + // make sure we are trying to load something valid
  1121. + if (btype == BINARY_NONE || ptype == PLUGIN_NONE) {
  1122. + setLastError("Invalid plugin state");
  1123. + return false;
  1124. + }
  1125. +
  1126. + // find path to bridge binary
  1127. + QString bridgeBinary(QString::fromUtf8(get_carla_bin_path()));
  1128. +
  1129. + if (btype == BINARY_NATIVE) {
  1130. + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-native";
  1131. + } else {
  1132. + switch (btype) {
  1133. + case BINARY_POSIX32:
  1134. + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-posix32";
  1135. + break;
  1136. + case BINARY_POSIX64:
  1137. + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-posix64";
  1138. + break;
  1139. + case BINARY_WIN32:
  1140. + bridgeBinary += CARLA_OS_SEP_STR
  1141. + "carla-bridge-win32.exe";
  1142. + break;
  1143. + case BINARY_WIN64:
  1144. + bridgeBinary += CARLA_OS_SEP_STR
  1145. + "carla-bridge-win64.exe";
  1146. + break;
  1147. + default:
  1148. + bridgeBinary.clear();
  1149. + break;
  1150. + }
  1151. + }
  1152. +
  1153. + if (bridgeBinary.isEmpty() || !QFileInfo(bridgeBinary).isExecutable()) {
  1154. + setLastError("Required plugin bridge is not available");
  1155. + return false;
  1156. + }
  1157. +
  1158. + // create string of shared memory ids to pass into the bridge process
  1159. + char shmIdsStr[6 * 4 + 1] = {};
  1160. +
  1161. + size_t len = audiopool.filename.length();
  1162. + CARLA_SAFE_ASSERT_RETURN(len > 6, false);
  1163. + std::strncpy(shmIdsStr, &audiopool.filename[len - 6], 6);
  1164. +
  1165. + len = rtClientCtrl.filename.length();
  1166. + CARLA_SAFE_ASSERT_RETURN(len > 6, false);
  1167. + std::strncpy(shmIdsStr + 6, &rtClientCtrl.filename[len - 6], 6);
  1168. +
  1169. + len = nonRtClientCtrl.filename.length();
  1170. + CARLA_SAFE_ASSERT_RETURN(len > 6, false);
  1171. + std::strncpy(shmIdsStr + 12, &nonRtClientCtrl.filename[len - 6], 6);
  1172. +
  1173. + len = nonRtServerCtrl.filename.length();
  1174. + CARLA_SAFE_ASSERT_RETURN(len > 6, false);
  1175. + std::strncpy(shmIdsStr + 18, &nonRtServerCtrl.filename[len - 6], 6);
  1176. +
  1177. + // create bridge process and setup arguments
  1178. + BridgeProcess *proc = new BridgeProcess(shmIdsStr);
  1179. +
  1180. + QStringList arguments;
  1181. +
  1182. +#if defined(CARLA_OS_MAC) && defined(__aarch64__)
  1183. + // see if this binary needs special help (x86_64 plugins under arm64 systems)
  1184. + switch (ptype) {
  1185. + case PLUGIN_VST2:
  1186. + case PLUGIN_VST3:
  1187. + case PLUGIN_CLAP:
  1188. + if (isIntel64BitPlugin(filename)) {
  1189. + // TODO we need to hook into qprocess for:
  1190. + // posix_spawnattr_setbinpref_np + CPU_TYPE_X86_64
  1191. + arguments.append("-arch");
  1192. + arguments.append("x86_64");
  1193. + arguments.append(bridgeBinary);
  1194. + bridgeBinary = "arch";
  1195. + }
  1196. + default:
  1197. + break;
  1198. + }
  1199. +#endif
  1200. +
  1201. + // do not use null strings for label and filename
  1202. + if (label == nullptr || label[0] == '\0')
  1203. + label = "(none)";
  1204. + if (filename == nullptr || filename[0] == '\0')
  1205. + filename = "(none)";
  1206. +
  1207. + // arg 1: plugin type
  1208. + arguments.append(QString::fromUtf8(getPluginTypeAsString(ptype)));
  1209. +
  1210. + // arg 2: filename
  1211. + arguments.append(QString::fromUtf8(filename));
  1212. +
  1213. + // arg 3: label
  1214. + arguments.append(QString::fromUtf8(label));
  1215. +
  1216. + // arg 4: uniqueId
  1217. + arguments.append(QString::number(uniqueId));
  1218. +
  1219. + proc->setProgram(bridgeBinary);
  1220. + proc->setArguments(arguments);
  1221. +
  1222. + // start process on main thread
  1223. + QMetaObject::invokeMethod(proc, "start");
  1224. +
  1225. + // check if it started correctly
  1226. + const bool started = proc->waitForStarted(5000);
  1227. +
  1228. + if (!started) {
  1229. + QMetaObject::invokeMethod(proc, "stop");
  1230. + setLastError("Plugin bridge failed to start");
  1231. + return false;
  1232. + }
  1233. +
  1234. + // wait for plugin process to start talking to us
  1235. + ready = false;
  1236. + timedErr = false;
  1237. + timedOut = false;
  1238. +
  1239. + const uint64_t start_time = os_gettime_ns();
  1240. +
  1241. + // NOTE: we cannot rely on `proc->state() == QProcess::Running` here
  1242. + // as Qt only updates QProcess state on main thread
  1243. + while (proc != nullptr && !ready && !timedErr) {
  1244. + os_sleep_ms(5);
  1245. +
  1246. + // timeout after 5s
  1247. + if (os_gettime_ns() - start_time > 5 * 1000000000ULL)
  1248. + break;
  1249. +
  1250. + readMessages();
  1251. + }
  1252. +
  1253. + if (!ready) {
  1254. + QMetaObject::invokeMethod(proc, "stop");
  1255. + if (!timedErr)
  1256. + setLastError(
  1257. + "Timeout while waiting for plugin bridge to start");
  1258. + return false;
  1259. + }
  1260. +
  1261. + // refuse to load plugin with incompatible IO
  1262. + if (info.hasCV || info.numAudioIns > MAX_AV_PLANES ||
  1263. + info.numAudioOuts > MAX_AV_PLANES) {
  1264. + // tell bridge process to quit
  1265. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientQuit);
  1266. + nonRtClientCtrl.commitWrite();
  1267. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientQuit);
  1268. + rtClientCtrl.commitWrite();
  1269. + wait("stopping", 3000);
  1270. + QMetaObject::invokeMethod(proc, "stop");
  1271. +
  1272. + // cleanup shared memory bits
  1273. + nonRtServerCtrl.clear();
  1274. + nonRtClientCtrl.clear();
  1275. + rtClientCtrl.clear();
  1276. + audiopool.clear();
  1277. +
  1278. + // also clear cached info
  1279. + info.clear();
  1280. + chunk.clear();
  1281. + clear_custom_data();
  1282. + delete[] paramDetails;
  1283. + paramDetails = nullptr;
  1284. + paramCount = 0;
  1285. +
  1286. + setLastError("Selected plugin has IO incompatible with OBS");
  1287. + return false;
  1288. + }
  1289. +
  1290. + // cache relevant information for later
  1291. + info.btype = btype;
  1292. + info.ptype = ptype;
  1293. + info.filename = filename;
  1294. + info.label = label;
  1295. + info.uniqueId = uniqueId;
  1296. +
  1297. + // finally assign childprocess and set active
  1298. + childprocess = proc;
  1299. +
  1300. + return true;
  1301. +}
  1302. +
  1303. +bool carla_bridge::is_running() const
  1304. +{
  1305. + return childprocess != nullptr &&
  1306. + childprocess->state() == QProcess::Running;
  1307. +}
  1308. +
  1309. +bool carla_bridge::idle()
  1310. +{
  1311. + if (childprocess == nullptr)
  1312. + return false;
  1313. +
  1314. + switch (childprocess->state()) {
  1315. + case QProcess::Running:
  1316. + if (!pendingPing) {
  1317. + pendingPing = true;
  1318. +
  1319. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1320. +
  1321. + nonRtClientCtrl.writeOpcode(
  1322. + kPluginBridgeNonRtClientPing);
  1323. + nonRtClientCtrl.commitWrite();
  1324. + }
  1325. + break;
  1326. + case QProcess::NotRunning:
  1327. + activated = false;
  1328. + timedErr = true;
  1329. + cleanup(false);
  1330. + return false;
  1331. + default:
  1332. + return false;
  1333. + }
  1334. +
  1335. + if (timedOut && activated) {
  1336. + deactivate();
  1337. + return idle();
  1338. + }
  1339. +
  1340. + try {
  1341. + readMessages();
  1342. + }
  1343. + CARLA_SAFE_EXCEPTION("readMessages");
  1344. +
  1345. + return true;
  1346. +}
  1347. +
  1348. +bool carla_bridge::wait(const char *const action, const uint msecs)
  1349. +{
  1350. + CARLA_SAFE_ASSERT_RETURN(!timedErr, false);
  1351. + CARLA_SAFE_ASSERT_RETURN(!timedOut, false);
  1352. +
  1353. + if (rtClientCtrl.waitForClient(msecs))
  1354. + return true;
  1355. +
  1356. + timedOut = true;
  1357. + blog(LOG_WARNING, "[carla] wait(%s) timed out", action);
  1358. + return false;
  1359. +}
  1360. +
  1361. +// ----------------------------------------------------------------------------
  1362. +
  1363. +void carla_bridge::set_value(uint index, float value)
  1364. +{
  1365. + CARLA_SAFE_ASSERT_UINT2_RETURN(index < paramCount, index, paramCount, );
  1366. +
  1367. + paramDetails[index].value = value;
  1368. +
  1369. + if (is_running()) {
  1370. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1371. +
  1372. + nonRtClientCtrl.writeOpcode(
  1373. + kPluginBridgeNonRtClientSetParameterValue);
  1374. + nonRtClientCtrl.writeUInt(index);
  1375. + nonRtClientCtrl.writeFloat(value);
  1376. + nonRtClientCtrl.commitWrite();
  1377. +
  1378. + if (info.hints & PLUGIN_HAS_CUSTOM_UI) {
  1379. + nonRtClientCtrl.writeOpcode(
  1380. + kPluginBridgeNonRtClientUiParameterChange);
  1381. + nonRtClientCtrl.writeUInt(index);
  1382. + nonRtClientCtrl.writeFloat(value);
  1383. + nonRtClientCtrl.commitWrite();
  1384. + }
  1385. +
  1386. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1387. + }
  1388. +}
  1389. +
  1390. +void carla_bridge::show_ui()
  1391. +{
  1392. + if (is_running()) {
  1393. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1394. +
  1395. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientShowUI);
  1396. + nonRtClientCtrl.commitWrite();
  1397. + }
  1398. +}
  1399. +
  1400. +bool carla_bridge::is_active() const noexcept
  1401. +{
  1402. + return activated;
  1403. +}
  1404. +
  1405. +void carla_bridge::activate()
  1406. +{
  1407. + if (activated)
  1408. + return;
  1409. +
  1410. + if (is_running()) {
  1411. + {
  1412. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1413. +
  1414. + nonRtClientCtrl.writeOpcode(
  1415. + kPluginBridgeNonRtClientActivate);
  1416. + nonRtClientCtrl.commitWrite();
  1417. + }
  1418. +
  1419. + try {
  1420. + wait("activate", 2000);
  1421. + }
  1422. + CARLA_SAFE_EXCEPTION("activate - waitForClient");
  1423. +
  1424. + activated = true;
  1425. + }
  1426. +}
  1427. +
  1428. +void carla_bridge::deactivate()
  1429. +{
  1430. + CARLA_SAFE_ASSERT_RETURN(activated, );
  1431. +
  1432. + activated = false;
  1433. + timedErr = false;
  1434. + timedOut = false;
  1435. +
  1436. + if (is_running()) {
  1437. + {
  1438. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1439. +
  1440. + nonRtClientCtrl.writeOpcode(
  1441. + kPluginBridgeNonRtClientDeactivate);
  1442. + nonRtClientCtrl.commitWrite();
  1443. + }
  1444. +
  1445. + try {
  1446. + wait("deactivate", 2000);
  1447. + }
  1448. + CARLA_SAFE_EXCEPTION("deactivate - waitForClient");
  1449. + }
  1450. +}
  1451. +
  1452. +void carla_bridge::reload()
  1453. +{
  1454. + ready = false;
  1455. + timedErr = false;
  1456. + timedOut = false;
  1457. +
  1458. + if (activated)
  1459. + deactivate();
  1460. +
  1461. + if (is_running()) {
  1462. + {
  1463. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1464. +
  1465. + nonRtClientCtrl.writeOpcode(
  1466. + kPluginBridgeNonRtClientReload);
  1467. + nonRtClientCtrl.commitWrite();
  1468. + }
  1469. + }
  1470. +
  1471. + activate();
  1472. +
  1473. + if (is_running()) {
  1474. + try {
  1475. + wait("reload", 2000);
  1476. + }
  1477. + CARLA_SAFE_EXCEPTION("reload - waitForClient");
  1478. + }
  1479. +
  1480. + // wait for plugin process to start talking back to us
  1481. + const uint64_t start_time = os_gettime_ns();
  1482. +
  1483. + while (childprocess != nullptr && !ready) {
  1484. + os_sleep_ms(5);
  1485. +
  1486. + // timeout after 1s
  1487. + if (os_gettime_ns() - start_time > 1000000000ULL)
  1488. + break;
  1489. +
  1490. + readMessages();
  1491. + }
  1492. +}
  1493. +
  1494. +void carla_bridge::restore_state()
  1495. +{
  1496. + const uint32_t maxLocalValueLen = clientBridgeVersion >= 10 ? 4096
  1497. + : 16384;
  1498. +
  1499. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1500. +
  1501. + for (CustomData &cdata : customData) {
  1502. + const uint32_t typeLen =
  1503. + static_cast<uint32_t>(std::strlen(cdata.type));
  1504. + const uint32_t keyLen =
  1505. + static_cast<uint32_t>(std::strlen(cdata.key));
  1506. + const uint32_t valueLen =
  1507. + static_cast<uint32_t>(std::strlen(cdata.value));
  1508. +
  1509. + nonRtClientCtrl.writeOpcode(
  1510. + kPluginBridgeNonRtClientSetCustomData);
  1511. +
  1512. + nonRtClientCtrl.writeUInt(typeLen);
  1513. + nonRtClientCtrl.writeCustomData(cdata.type, typeLen);
  1514. +
  1515. + nonRtClientCtrl.writeUInt(keyLen);
  1516. + nonRtClientCtrl.writeCustomData(cdata.key, keyLen);
  1517. +
  1518. + nonRtClientCtrl.writeUInt(valueLen);
  1519. +
  1520. + if (valueLen > 0) {
  1521. + if (valueLen > maxLocalValueLen) {
  1522. + QString filePath(QDir::tempPath());
  1523. +
  1524. + filePath += CARLA_OS_SEP_STR
  1525. + ".CarlaCustomData_";
  1526. + filePath += audiopool.getFilenameSuffix();
  1527. +
  1528. + QFile file(filePath);
  1529. + if (file.open(QIODevice::WriteOnly) &&
  1530. + file.write(cdata.value) !=
  1531. + static_cast<qint64>(valueLen)) {
  1532. + const uint32_t ulength =
  1533. + static_cast<uint32_t>(
  1534. + filePath.length());
  1535. +
  1536. + nonRtClientCtrl.writeUInt(ulength);
  1537. + nonRtClientCtrl.writeCustomData(
  1538. + filePath.toUtf8().constData(),
  1539. + ulength);
  1540. + } else {
  1541. + nonRtClientCtrl.writeUInt(0);
  1542. + }
  1543. + } else {
  1544. + nonRtClientCtrl.writeCustomData(cdata.value,
  1545. + valueLen);
  1546. + }
  1547. + }
  1548. +
  1549. + nonRtClientCtrl.commitWrite();
  1550. +
  1551. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1552. + }
  1553. +
  1554. + if (info.ptype == PLUGIN_LV2) {
  1555. + nonRtClientCtrl.writeOpcode(
  1556. + kPluginBridgeNonRtClientRestoreLV2State);
  1557. + nonRtClientCtrl.commitWrite();
  1558. + }
  1559. +
  1560. + if (info.options & PLUGIN_OPTION_USE_CHUNKS) {
  1561. + QString filePath(QDir::tempPath());
  1562. +
  1563. + filePath += CARLA_OS_SEP_STR ".CarlaChunk_";
  1564. + filePath += audiopool.getFilenameSuffix();
  1565. +
  1566. + QFile file(filePath);
  1567. + if (file.open(QIODevice::WriteOnly) &&
  1568. + file.write(CarlaString::asBase64(chunk.data(), chunk.size())
  1569. + .buffer()) != 0) {
  1570. + file.close();
  1571. +
  1572. + const uint32_t ulength =
  1573. + static_cast<uint32_t>(filePath.length());
  1574. +
  1575. + nonRtClientCtrl.writeOpcode(
  1576. + kPluginBridgeNonRtClientSetChunkDataFile);
  1577. + nonRtClientCtrl.writeUInt(ulength);
  1578. + nonRtClientCtrl.writeCustomData(
  1579. + filePath.toUtf8().constData(), ulength);
  1580. + nonRtClientCtrl.commitWrite();
  1581. +
  1582. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1583. + }
  1584. + } else {
  1585. + for (uint32_t i = 0; i < paramCount; ++i) {
  1586. + const carla_param_data &param(paramDetails[i]);
  1587. +
  1588. + nonRtClientCtrl.writeOpcode(
  1589. + kPluginBridgeNonRtClientSetParameterValue);
  1590. + nonRtClientCtrl.writeUInt(i);
  1591. + nonRtClientCtrl.writeFloat(param.value);
  1592. + nonRtClientCtrl.commitWrite();
  1593. +
  1594. + nonRtClientCtrl.writeOpcode(
  1595. + kPluginBridgeNonRtClientUiParameterChange);
  1596. + nonRtClientCtrl.writeUInt(i);
  1597. + nonRtClientCtrl.writeFloat(param.value);
  1598. + nonRtClientCtrl.commitWrite();
  1599. +
  1600. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1601. + }
  1602. + }
  1603. +}
  1604. +
  1605. +void carla_bridge::process(float *buffers[MAX_AV_PLANES], const uint32_t frames)
  1606. +{
  1607. + if (!ready || !activated)
  1608. + return;
  1609. +
  1610. + rtClientCtrl.data->timeInfo.usecs = os_gettime_ns() / 1000;
  1611. +
  1612. + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c)
  1613. + carla_copyFloats(audiopool.data + (c * bufferSize), buffers[c],
  1614. + frames);
  1615. +
  1616. + {
  1617. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientProcess);
  1618. + rtClientCtrl.writeUInt(frames);
  1619. + rtClientCtrl.commitWrite();
  1620. + }
  1621. +
  1622. + if (wait("process", 1000)) {
  1623. + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c)
  1624. + carla_copyFloats(
  1625. + buffers[c],
  1626. + audiopool.data +
  1627. + ((c + info.numAudioIns) * bufferSize),
  1628. + frames);
  1629. + }
  1630. +}
  1631. +
  1632. +void carla_bridge::add_custom_data(const char *const type,
  1633. + const char *const key,
  1634. + const char *const value,
  1635. + const bool sendToPlugin)
  1636. +{
  1637. + CARLA_SAFE_ASSERT_RETURN(type != nullptr && type[0] != '\0', );
  1638. + CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0', );
  1639. + CARLA_SAFE_ASSERT_RETURN(value != nullptr, );
  1640. +
  1641. + // Check if we already have this key
  1642. + bool found = false;
  1643. + for (CustomData &cdata : customData) {
  1644. + if (std::strcmp(cdata.key, key) == 0) {
  1645. + bfree(const_cast<char *>(cdata.value));
  1646. + cdata.value = bstrdup(value);
  1647. + found = true;
  1648. + break;
  1649. + }
  1650. + }
  1651. +
  1652. + // Otherwise store it
  1653. + if (!found) {
  1654. + CustomData cdata = {};
  1655. + cdata.type = bstrdup(type);
  1656. + cdata.key = bstrdup(key);
  1657. + cdata.value = bstrdup(value);
  1658. + customData.push_back(cdata);
  1659. + }
  1660. +
  1661. + if (sendToPlugin) {
  1662. + const uint32_t maxLocalValueLen =
  1663. + clientBridgeVersion >= 10 ? 4096 : 16384;
  1664. +
  1665. + const uint32_t typeLen =
  1666. + static_cast<uint32_t>(std::strlen(type));
  1667. + const uint32_t keyLen = static_cast<uint32_t>(std::strlen(key));
  1668. + const uint32_t valueLen =
  1669. + static_cast<uint32_t>(std::strlen(value));
  1670. +
  1671. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1672. +
  1673. + if (valueLen > maxLocalValueLen)
  1674. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1675. +
  1676. + nonRtClientCtrl.writeOpcode(
  1677. + kPluginBridgeNonRtClientSetCustomData);
  1678. +
  1679. + nonRtClientCtrl.writeUInt(typeLen);
  1680. + nonRtClientCtrl.writeCustomData(type, typeLen);
  1681. +
  1682. + nonRtClientCtrl.writeUInt(keyLen);
  1683. + nonRtClientCtrl.writeCustomData(key, keyLen);
  1684. +
  1685. + nonRtClientCtrl.writeUInt(valueLen);
  1686. +
  1687. + if (valueLen > 0) {
  1688. + if (valueLen > maxLocalValueLen) {
  1689. + QString filePath(QDir::tempPath());
  1690. +
  1691. + filePath += CARLA_OS_SEP_STR
  1692. + ".CarlaCustomData_";
  1693. + filePath += audiopool.getFilenameSuffix();
  1694. +
  1695. + QFile file(filePath);
  1696. + if (file.open(QIODevice::WriteOnly) &&
  1697. + file.write(value) !=
  1698. + static_cast<qint64>(valueLen)) {
  1699. + const uint32_t ulength =
  1700. + static_cast<uint32_t>(
  1701. + filePath.length());
  1702. +
  1703. + nonRtClientCtrl.writeUInt(ulength);
  1704. + nonRtClientCtrl.writeCustomData(
  1705. + filePath.toUtf8().constData(),
  1706. + ulength);
  1707. + } else {
  1708. + nonRtClientCtrl.writeUInt(0);
  1709. + }
  1710. + } else {
  1711. + nonRtClientCtrl.writeCustomData(value,
  1712. + valueLen);
  1713. + }
  1714. + }
  1715. +
  1716. + nonRtClientCtrl.commitWrite();
  1717. +
  1718. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1719. + }
  1720. +}
  1721. +
  1722. +void carla_bridge::custom_data_loaded()
  1723. +{
  1724. + if (info.ptype != PLUGIN_LV2)
  1725. + return;
  1726. +
  1727. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1728. +
  1729. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientRestoreLV2State);
  1730. + nonRtClientCtrl.commitWrite();
  1731. +}
  1732. +
  1733. +void carla_bridge::clear_custom_data()
  1734. +{
  1735. + for (CustomData &cdata : customData) {
  1736. + bfree(const_cast<char *>(cdata.type));
  1737. + bfree(const_cast<char *>(cdata.key));
  1738. + bfree(const_cast<char *>(cdata.value));
  1739. + }
  1740. + customData.clear();
  1741. +}
  1742. +
  1743. +void carla_bridge::load_chunk(const char *b64chunk)
  1744. +{
  1745. + chunk = QByteArray::fromBase64(b64chunk);
  1746. +
  1747. + QString filePath(QDir::tempPath());
  1748. +
  1749. + filePath += CARLA_OS_SEP_STR ".CarlaChunk_";
  1750. + filePath += audiopool.getFilenameSuffix();
  1751. +
  1752. + QFile file(filePath);
  1753. + if (file.open(QIODevice::WriteOnly) && file.write(b64chunk) != 0) {
  1754. + file.close();
  1755. +
  1756. + const uint32_t ulength =
  1757. + static_cast<uint32_t>(filePath.length());
  1758. +
  1759. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1760. +
  1761. + nonRtClientCtrl.writeOpcode(
  1762. + kPluginBridgeNonRtClientSetChunkDataFile);
  1763. + nonRtClientCtrl.writeUInt(ulength);
  1764. + nonRtClientCtrl.writeCustomData(filePath.toUtf8().constData(),
  1765. + ulength);
  1766. + nonRtClientCtrl.commitWrite();
  1767. +
  1768. + nonRtClientCtrl.waitIfDataIsReachingLimit();
  1769. + }
  1770. +}
  1771. +
  1772. +void carla_bridge::save_and_wait()
  1773. +{
  1774. + if (!is_running())
  1775. + return;
  1776. +
  1777. + saved = false;
  1778. + pendingPing = false;
  1779. +
  1780. + {
  1781. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1782. +
  1783. + // deactivate bridge client-side ping check
  1784. + // some plugins block during save, preventing regular ping timings
  1785. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff);
  1786. + nonRtClientCtrl.writeBool(false);
  1787. + nonRtClientCtrl.commitWrite();
  1788. +
  1789. + // tell plugin bridge to save and report any pending data
  1790. + nonRtClientCtrl.writeOpcode(
  1791. + kPluginBridgeNonRtClientPrepareForSave);
  1792. + nonRtClientCtrl.commitWrite();
  1793. + }
  1794. +
  1795. + // wait for "saved" reply
  1796. + const uint64_t start_time = os_gettime_ns();
  1797. +
  1798. + while (is_running() && !saved) {
  1799. + os_sleep_ms(5);
  1800. +
  1801. + // timeout after 10s
  1802. + if (os_gettime_ns() - start_time > 10 * 1000000000ULL)
  1803. + break;
  1804. +
  1805. + readMessages();
  1806. +
  1807. + // deactivate plugin if we timeout during save
  1808. + if (timedOut && activated) {
  1809. + activated = false;
  1810. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1811. +
  1812. + nonRtClientCtrl.writeOpcode(
  1813. + kPluginBridgeNonRtClientDeactivate);
  1814. + nonRtClientCtrl.commitWrite();
  1815. + }
  1816. + }
  1817. +
  1818. + if (is_running()) {
  1819. + const CarlaMutexLocker cml(nonRtClientCtrl.mutex);
  1820. +
  1821. + // reactivate ping check
  1822. + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff);
  1823. + nonRtClientCtrl.writeBool(true);
  1824. + nonRtClientCtrl.commitWrite();
  1825. + }
  1826. +}
  1827. +
  1828. +void carla_bridge::set_buffer_size(const uint32_t maxBufferSize)
  1829. +{
  1830. + if (bufferSize == maxBufferSize)
  1831. + return;
  1832. +
  1833. + bufferSize = maxBufferSize;
  1834. +
  1835. + if (is_running()) {
  1836. + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES);
  1837. +
  1838. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool);
  1839. + rtClientCtrl.writeULong(
  1840. + static_cast<uint64_t>(audiopool.dataSize));
  1841. + rtClientCtrl.commitWrite();
  1842. +
  1843. + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize);
  1844. + rtClientCtrl.writeUInt(maxBufferSize);
  1845. + rtClientCtrl.commitWrite();
  1846. + }
  1847. +}
  1848. +
  1849. +const char *carla_bridge::get_last_error() const noexcept
  1850. +{
  1851. + return lastError;
  1852. +}
  1853. +
  1854. +// ----------------------------------------------------------------------------
  1855. +
  1856. +void carla_bridge::readMessages()
  1857. +{
  1858. + while (nonRtServerCtrl.isDataAvailableForReading()) {
  1859. + const PluginBridgeNonRtServerOpcode opcode =
  1860. + nonRtServerCtrl.readOpcode();
  1861. +
  1862. + // #ifdef DEBUG
  1863. + if (opcode != kPluginBridgeNonRtServerPong &&
  1864. + opcode != kPluginBridgeNonRtServerParameterValue2) {
  1865. + blog(LOG_DEBUG, "[carla] got opcode: %s",
  1866. + PluginBridgeNonRtServerOpcode2str(opcode));
  1867. + }
  1868. + // #endif
  1869. +
  1870. + switch (opcode) {
  1871. + case kPluginBridgeNonRtServerNull:
  1872. + break;
  1873. +
  1874. + case kPluginBridgeNonRtServerPong:
  1875. + pendingPing = false;
  1876. + break;
  1877. +
  1878. + // uint/version
  1879. + case kPluginBridgeNonRtServerVersion:
  1880. + clientBridgeVersion = nonRtServerCtrl.readUInt();
  1881. + break;
  1882. +
  1883. + // uint/category, uint/hints, uint/optionsAvailable, uint/optionsEnabled, long/uniqueId
  1884. + case kPluginBridgeNonRtServerPluginInfo1: {
  1885. + // const uint32_t category =
  1886. + nonRtServerCtrl.readUInt();
  1887. + info.hints = nonRtServerCtrl.readUInt() |
  1888. + PLUGIN_IS_BRIDGE;
  1889. + // const uint32_t optionAv =
  1890. + nonRtServerCtrl.readUInt();
  1891. + info.options = nonRtServerCtrl.readUInt();
  1892. + const int64_t uniqueId = nonRtServerCtrl.readLong();
  1893. +
  1894. + if (info.uniqueId != 0) {
  1895. + CARLA_SAFE_ASSERT_INT2(info.uniqueId ==
  1896. + uniqueId,
  1897. + info.uniqueId, uniqueId);
  1898. + }
  1899. + } break;
  1900. +
  1901. + // uint/size, str[] (realName), uint/size, str[] (label), uint/size, str[] (maker), uint/size, str[] (copyright)
  1902. + case kPluginBridgeNonRtServerPluginInfo2: {
  1903. + // realName
  1904. + const BridgeTextReader name(nonRtServerCtrl);
  1905. + info.name = name.text;
  1906. +
  1907. + // label
  1908. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  1909. + nonRtServerCtrl.skipRead(size);
  1910. +
  1911. + // maker
  1912. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  1913. + nonRtServerCtrl.skipRead(size);
  1914. +
  1915. + // copyright
  1916. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  1917. + nonRtServerCtrl.skipRead(size);
  1918. + } break;
  1919. +
  1920. + // uint/ins, uint/outs
  1921. + case kPluginBridgeNonRtServerAudioCount:
  1922. + info.numAudioIns = nonRtServerCtrl.readUInt();
  1923. + info.numAudioOuts = nonRtServerCtrl.readUInt();
  1924. + break;
  1925. +
  1926. + // uint/ins, uint/outs
  1927. + case kPluginBridgeNonRtServerMidiCount:
  1928. + nonRtServerCtrl.readUInt();
  1929. + nonRtServerCtrl.readUInt();
  1930. + break;
  1931. +
  1932. + // uint/ins, uint/outs
  1933. + case kPluginBridgeNonRtServerCvCount: {
  1934. + const uint32_t cvIns = nonRtServerCtrl.readUInt();
  1935. + const uint32_t cvOuts = nonRtServerCtrl.readUInt();
  1936. + info.hasCV = cvIns + cvOuts != 0;
  1937. + } break;
  1938. +
  1939. + // uint/count
  1940. + case kPluginBridgeNonRtServerParameterCount: {
  1941. + paramCount = nonRtServerCtrl.readUInt();
  1942. +
  1943. + delete[] paramDetails;
  1944. +
  1945. + if (paramCount != 0)
  1946. + paramDetails = new carla_param_data[paramCount];
  1947. + else
  1948. + paramDetails = nullptr;
  1949. + } break;
  1950. +
  1951. + // uint/count
  1952. + case kPluginBridgeNonRtServerProgramCount:
  1953. + nonRtServerCtrl.readUInt();
  1954. + break;
  1955. +
  1956. + // uint/count
  1957. + case kPluginBridgeNonRtServerMidiProgramCount:
  1958. + nonRtServerCtrl.readUInt();
  1959. + break;
  1960. +
  1961. + // byte/type, uint/index, uint/size, str[] (name)
  1962. + case kPluginBridgeNonRtServerPortName: {
  1963. + nonRtServerCtrl.readByte();
  1964. + nonRtServerCtrl.readUInt();
  1965. +
  1966. + // name
  1967. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  1968. + nonRtServerCtrl.skipRead(size);
  1969. +
  1970. + } break;
  1971. +
  1972. + // uint/index, int/rindex, uint/type, uint/hints, short/cc
  1973. + case kPluginBridgeNonRtServerParameterData1: {
  1974. + const uint32_t index = nonRtServerCtrl.readUInt();
  1975. + nonRtServerCtrl.readInt();
  1976. + const uint32_t type = nonRtServerCtrl.readUInt();
  1977. + const uint32_t hints = nonRtServerCtrl.readUInt();
  1978. + nonRtServerCtrl.readShort();
  1979. +
  1980. + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index,
  1981. + paramCount);
  1982. +
  1983. + if (type != PARAMETER_INPUT)
  1984. + break;
  1985. + if ((hints & PARAMETER_IS_ENABLED) == 0)
  1986. + break;
  1987. + if (hints &
  1988. + (PARAMETER_IS_READ_ONLY | PARAMETER_IS_NOT_SAVED))
  1989. + break;
  1990. +
  1991. + paramDetails[index].hints = hints;
  1992. + } break;
  1993. +
  1994. + // uint/index, uint/size, str[] (name), uint/size, str[] (unit)
  1995. + case kPluginBridgeNonRtServerParameterData2: {
  1996. + const uint32_t index = nonRtServerCtrl.readUInt();
  1997. +
  1998. + // name
  1999. + const BridgeTextReader name(nonRtServerCtrl);
  2000. +
  2001. + // symbol
  2002. + const BridgeTextReader symbol(nonRtServerCtrl);
  2003. +
  2004. + // unit
  2005. + const BridgeTextReader unit(nonRtServerCtrl);
  2006. +
  2007. + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index,
  2008. + paramCount);
  2009. +
  2010. + if (paramDetails[index].hints & PARAMETER_IS_ENABLED) {
  2011. + paramDetails[index].name = name.text;
  2012. + paramDetails[index].symbol = symbol.text;
  2013. + paramDetails[index].unit = unit.text;
  2014. + }
  2015. + } break;
  2016. +
  2017. + // uint/index, float/def, float/min, float/max, float/step, float/stepSmall, float/stepLarge
  2018. + case kPluginBridgeNonRtServerParameterRanges: {
  2019. + const uint32_t index = nonRtServerCtrl.readUInt();
  2020. + const float def = nonRtServerCtrl.readFloat();
  2021. + const float min = nonRtServerCtrl.readFloat();
  2022. + const float max = nonRtServerCtrl.readFloat();
  2023. + const float step = nonRtServerCtrl.readFloat();
  2024. + nonRtServerCtrl.readFloat();
  2025. + nonRtServerCtrl.readFloat();
  2026. +
  2027. + CARLA_SAFE_ASSERT_BREAK(min < max);
  2028. + CARLA_SAFE_ASSERT_BREAK(def >= min);
  2029. + CARLA_SAFE_ASSERT_BREAK(def <= max);
  2030. + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index,
  2031. + paramCount);
  2032. +
  2033. + if (paramDetails[index].hints & PARAMETER_IS_ENABLED) {
  2034. + paramDetails[index].def =
  2035. + paramDetails[index].value = def;
  2036. + paramDetails[index].min = min;
  2037. + paramDetails[index].max = max;
  2038. + paramDetails[index].step = step;
  2039. + }
  2040. + } break;
  2041. +
  2042. + // uint/index, float/value
  2043. + case kPluginBridgeNonRtServerParameterValue: {
  2044. + const uint32_t index = nonRtServerCtrl.readUInt();
  2045. + const float value = nonRtServerCtrl.readFloat();
  2046. +
  2047. + if (index < paramCount) {
  2048. + const float fixedValue = carla_fixedValue(
  2049. + paramDetails[index].min,
  2050. + paramDetails[index].max, value);
  2051. +
  2052. + if (carla_isNotEqual(paramDetails[index].value,
  2053. + fixedValue)) {
  2054. + paramDetails[index].value = fixedValue;
  2055. +
  2056. + if (callback != nullptr) {
  2057. + // skip parameters that we do not show
  2058. + if ((paramDetails[index].hints &
  2059. + PARAMETER_IS_ENABLED) == 0)
  2060. + break;
  2061. +
  2062. + callback->bridge_parameter_changed(
  2063. + index, fixedValue);
  2064. + }
  2065. + }
  2066. + }
  2067. + } break;
  2068. +
  2069. + // uint/index, float/value
  2070. + case kPluginBridgeNonRtServerParameterValue2: {
  2071. + const uint32_t index = nonRtServerCtrl.readUInt();
  2072. + const float value = nonRtServerCtrl.readFloat();
  2073. +
  2074. + if (index < paramCount) {
  2075. + const float fixedValue = carla_fixedValue(
  2076. + paramDetails[index].min,
  2077. + paramDetails[index].max, value);
  2078. + paramDetails[index].value = fixedValue;
  2079. + }
  2080. + } break;
  2081. +
  2082. + // uint/index, bool/touch
  2083. + case kPluginBridgeNonRtServerParameterTouch:
  2084. + nonRtServerCtrl.readUInt();
  2085. + nonRtServerCtrl.readBool();
  2086. + break;
  2087. +
  2088. + // uint/index, float/value
  2089. + case kPluginBridgeNonRtServerDefaultValue: {
  2090. + const uint32_t index = nonRtServerCtrl.readUInt();
  2091. + const float value = nonRtServerCtrl.readFloat();
  2092. +
  2093. + if (index < paramCount)
  2094. + paramDetails[index].def = value;
  2095. + } break;
  2096. +
  2097. + // int/index
  2098. + case kPluginBridgeNonRtServerCurrentProgram:
  2099. + nonRtServerCtrl.readInt();
  2100. + break;
  2101. +
  2102. + // int/index
  2103. + case kPluginBridgeNonRtServerCurrentMidiProgram:
  2104. + nonRtServerCtrl.readInt();
  2105. + break;
  2106. +
  2107. + // uint/index, uint/size, str[] (name)
  2108. + case kPluginBridgeNonRtServerProgramName: {
  2109. + nonRtServerCtrl.readUInt();
  2110. +
  2111. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  2112. + nonRtServerCtrl.skipRead(size);
  2113. + } break;
  2114. +
  2115. + // uint/index, uint/bank, uint/program, uint/size, str[] (name)
  2116. + case kPluginBridgeNonRtServerMidiProgramData: {
  2117. + nonRtServerCtrl.readUInt();
  2118. + nonRtServerCtrl.readUInt();
  2119. + nonRtServerCtrl.readUInt();
  2120. +
  2121. + // name
  2122. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  2123. + nonRtServerCtrl.skipRead(size);
  2124. + } break;
  2125. +
  2126. + // uint/size, str[], uint/size, str[], uint/size, str[]
  2127. + case kPluginBridgeNonRtServerSetCustomData: {
  2128. + const uint32_t maxLocalValueLen =
  2129. + clientBridgeVersion >= 10 ? 4096 : 16384;
  2130. +
  2131. + // type
  2132. + const BridgeTextReader type(nonRtServerCtrl);
  2133. +
  2134. + // key
  2135. + const BridgeTextReader key(nonRtServerCtrl);
  2136. +
  2137. + // value
  2138. + const uint32_t valueSize = nonRtServerCtrl.readUInt();
  2139. +
  2140. + // special case for big values
  2141. + if (valueSize > maxLocalValueLen) {
  2142. + const BridgeTextReader bigValueFilePath(
  2143. + nonRtServerCtrl, valueSize);
  2144. +
  2145. + QString realBigValueFilePath(QString::fromUtf8(
  2146. + bigValueFilePath.text));
  2147. +
  2148. + QFile bigValueFile(realBigValueFilePath);
  2149. + CARLA_SAFE_ASSERT_BREAK(bigValueFile.exists());
  2150. +
  2151. + if (bigValueFile.open(QIODevice::ReadOnly)) {
  2152. + add_custom_data(type.text, key.text,
  2153. + bigValueFile.readAll()
  2154. + .constData(),
  2155. + false);
  2156. + bigValueFile.remove();
  2157. + }
  2158. + } else {
  2159. + const BridgeTextReader value(nonRtServerCtrl,
  2160. + valueSize);
  2161. +
  2162. + add_custom_data(type.text, key.text, value.text,
  2163. + false);
  2164. + }
  2165. +
  2166. + } break;
  2167. +
  2168. + // uint/size, str[] (filename, base64 content)
  2169. + case kPluginBridgeNonRtServerSetChunkDataFile: {
  2170. + // chunkFilePath
  2171. + const BridgeTextReader chunkFilePath(nonRtServerCtrl);
  2172. +
  2173. + QString realChunkFilePath(
  2174. + QString::fromUtf8(chunkFilePath.text));
  2175. +
  2176. + QFile chunkFile(realChunkFilePath);
  2177. + CARLA_SAFE_ASSERT_BREAK(chunkFile.exists());
  2178. +
  2179. + if (chunkFile.open(QIODevice::ReadOnly)) {
  2180. + chunk = QByteArray::fromBase64(
  2181. + chunkFile.readAll());
  2182. + chunkFile.remove();
  2183. + }
  2184. + } break;
  2185. +
  2186. + // uint/latency
  2187. + case kPluginBridgeNonRtServerSetLatency:
  2188. + nonRtServerCtrl.readUInt();
  2189. + break;
  2190. +
  2191. + // uint/index, uint/size, str[] (name)
  2192. + case kPluginBridgeNonRtServerSetParameterText: {
  2193. + nonRtServerCtrl.readInt();
  2194. +
  2195. + if (const uint32_t size = nonRtServerCtrl.readUInt())
  2196. + nonRtServerCtrl.skipRead(size);
  2197. + } break;
  2198. +
  2199. + case kPluginBridgeNonRtServerReady:
  2200. + ready = true;
  2201. + break;
  2202. +
  2203. + case kPluginBridgeNonRtServerSaved:
  2204. + saved = true;
  2205. + break;
  2206. +
  2207. + // ulong/window-id
  2208. + case kPluginBridgeNonRtServerRespEmbedUI:
  2209. + nonRtServerCtrl.readULong();
  2210. + break;
  2211. +
  2212. + // uint/width, uint/height
  2213. + case kPluginBridgeNonRtServerResizeEmbedUI:
  2214. + nonRtServerCtrl.readUInt();
  2215. + nonRtServerCtrl.readUInt();
  2216. + break;
  2217. +
  2218. + case kPluginBridgeNonRtServerUiClosed:
  2219. + break;
  2220. +
  2221. + // uint/size, str[]
  2222. + case kPluginBridgeNonRtServerError: {
  2223. + const BridgeTextReader error(nonRtServerCtrl);
  2224. + timedErr = true;
  2225. + blog(LOG_ERROR, "[carla] %s", error.text);
  2226. + setLastError(error.text);
  2227. + } break;
  2228. + }
  2229. + }
  2230. +}
  2231. +
  2232. +// ----------------------------------------------------------------------------
  2233. +
  2234. +void carla_bridge::setLastError(const char *const error)
  2235. +{
  2236. + bfree(lastError);
  2237. + lastError = bstrdup(error);
  2238. +}
  2239. +
  2240. +// ----------------------------------------------------------------------------
  2241. diff --git a/plugins/carla/carla-bridge.hpp b/plugins/carla/carla-bridge.hpp
  2242. new file mode 100644
  2243. index 000000000..b354a2d57
  2244. --- /dev/null
  2245. +++ b/plugins/carla/carla-bridge.hpp
  2246. @@ -0,0 +1,204 @@
  2247. +/*
  2248. + * Carla plugin for OBS
  2249. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  2250. + * SPDX-License-Identifier: GPL-2.0-or-later
  2251. + */
  2252. +
  2253. +#pragma once
  2254. +
  2255. +#include <CarlaBackend.h>
  2256. +#include <CarlaBridgeUtils.hpp>
  2257. +
  2258. +#include <QtCore/QByteArray>
  2259. +#include <QtCore/QProcess>
  2260. +#include <QtCore/QString>
  2261. +
  2262. +#include <vector>
  2263. +
  2264. +#include <obs.h>
  2265. +
  2266. +CARLA_BACKEND_USE_NAMESPACE
  2267. +
  2268. +// ----------------------------------------------------------------------------
  2269. +// custom class for allowing QProcess usage outside the main thread
  2270. +
  2271. +class BridgeProcess : public QProcess {
  2272. + Q_OBJECT
  2273. +
  2274. +public:
  2275. + BridgeProcess(const char *shmIds);
  2276. +
  2277. +public Q_SLOTS:
  2278. + void start();
  2279. + void stop();
  2280. +};
  2281. +
  2282. +// ----------------------------------------------------------------------------
  2283. +// relevant information for an exposed plugin parameter
  2284. +
  2285. +struct carla_param_data {
  2286. + uint32_t hints = 0;
  2287. + float value = 0.f;
  2288. + float def = 0.f;
  2289. + float min = 0.f;
  2290. + float max = 1.f;
  2291. + float step = 0.01f;
  2292. + CarlaString name;
  2293. + CarlaString symbol;
  2294. + CarlaString unit;
  2295. +};
  2296. +
  2297. +// ----------------------------------------------------------------------------
  2298. +// information about the currently active plugin
  2299. +
  2300. +struct carla_bridge_info {
  2301. + BinaryType btype = BINARY_NONE;
  2302. + PluginType ptype = PLUGIN_NONE;
  2303. + uint32_t hints = 0;
  2304. + uint32_t options = PLUGIN_OPTIONS_NULL;
  2305. + bool hasCV = false;
  2306. + uint32_t numAudioIns = 0;
  2307. + uint32_t numAudioOuts = 0;
  2308. + int64_t uniqueId = 0;
  2309. + CarlaString filename;
  2310. + CarlaString label;
  2311. + CarlaString name;
  2312. +
  2313. + void clear()
  2314. + {
  2315. + btype = BINARY_NONE;
  2316. + ptype = PLUGIN_NONE;
  2317. + hints = 0;
  2318. + options = PLUGIN_OPTIONS_NULL;
  2319. + hasCV = false;
  2320. + numAudioIns = numAudioOuts = 0;
  2321. + uniqueId = 0;
  2322. + filename.clear();
  2323. + label.clear();
  2324. + name.clear();
  2325. + }
  2326. +};
  2327. +
  2328. +// ----------------------------------------------------------------------------
  2329. +// bridge callbacks, triggered during carla_bridge::idle()
  2330. +
  2331. +struct carla_bridge_callback {
  2332. + virtual ~carla_bridge_callback(){};
  2333. + virtual void bridge_parameter_changed(uint index, float value) = 0;
  2334. +};
  2335. +
  2336. +// ----------------------------------------------------------------------------
  2337. +// bridge implementation
  2338. +
  2339. +struct carla_bridge {
  2340. + carla_bridge_callback *callback = nullptr;
  2341. +
  2342. + // cached parameter info
  2343. + uint32_t paramCount = 0;
  2344. + carla_param_data *paramDetails = nullptr;
  2345. +
  2346. + // cached plugin info
  2347. + carla_bridge_info info;
  2348. + QByteArray chunk;
  2349. + std::vector<CustomData> customData;
  2350. +
  2351. + ~carla_bridge()
  2352. + {
  2353. + delete[] paramDetails;
  2354. + clear_custom_data();
  2355. + bfree(lastError);
  2356. + }
  2357. +
  2358. + // initialize bridge shared memory details
  2359. + bool init(uint32_t maxBufferSize, double sampleRate);
  2360. +
  2361. + // stop bridge process and cleanup shared memory
  2362. + void cleanup(bool clearPluginData = true);
  2363. +
  2364. + // start plugin bridge
  2365. + bool start(BinaryType btype, PluginType ptype, const char *label,
  2366. + const char *filename, int64_t uniqueId);
  2367. +
  2368. + // check if plugin bridge process is running
  2369. + // return status might be wrong when called outside the main thread
  2370. + bool is_running() const;
  2371. +
  2372. + // to be called at regular intervals, from the main thread
  2373. + // returns false if bridge process is not running
  2374. + bool idle();
  2375. +
  2376. + // wait on RT client, making sure it is still active
  2377. + // returns true on success
  2378. + // NOTE: plugin will be deactivated on next `idle()` if timed out
  2379. + bool wait(const char *action, uint msecs);
  2380. +
  2381. + // change a plugin parameter value
  2382. + void set_value(uint index, float value);
  2383. +
  2384. + // show the plugin's custom UI
  2385. + void show_ui();
  2386. +
  2387. + // [de]activate, a deactivated plugin does not process any audio
  2388. + bool is_active() const noexcept;
  2389. + void activate();
  2390. + void deactivate();
  2391. +
  2392. + // reactivate and reload plugin information
  2393. + void reload();
  2394. +
  2395. + // restore current state from known info, useful when bridge crashes
  2396. + void restore_state();
  2397. +
  2398. + // process plugin audio
  2399. + // frames must be <= `maxBufferSize` as passed during `init`
  2400. + void process(float *buffers[MAX_AV_PLANES], uint32_t frames);
  2401. +
  2402. + // add or replace custom data (non-parameter plugin values)
  2403. + void add_custom_data(const char *type, const char *key,
  2404. + const char *value, bool sendToPlugin = true);
  2405. +
  2406. + // inform plugin that all custom data has been loaded
  2407. + // required after loading plugin state
  2408. + void custom_data_loaded();
  2409. +
  2410. + // clear all custom data stored so far
  2411. + void clear_custom_data();
  2412. +
  2413. + // load plugin state as base64 chunk
  2414. + // NOTE: do not save parameter values for plugins using "chunks"
  2415. + void load_chunk(const char *b64chunk);
  2416. +
  2417. + // request plugin bridge to save and report back its internal state
  2418. + // must be called just before saving plugin state
  2419. + void save_and_wait();
  2420. +
  2421. + // change the maximum expected buffer size
  2422. + // plugin is temporarily deactivated during the change
  2423. + void set_buffer_size(uint32_t maxBufferSize);
  2424. +
  2425. + // get last known error, e.g. reason for last bridge start to fail
  2426. + const char *get_last_error() const noexcept;
  2427. +
  2428. +private:
  2429. + bool activated = false;
  2430. + bool pendingPing = false;
  2431. + bool ready = false;
  2432. + bool saved = false;
  2433. + bool timedErr = false;
  2434. + bool timedOut = false;
  2435. + uint32_t bufferSize = 0;
  2436. + uint32_t clientBridgeVersion = 0;
  2437. + char *lastError = nullptr;
  2438. +
  2439. + BridgeAudioPool audiopool;
  2440. + BridgeRtClientControl rtClientCtrl;
  2441. + BridgeNonRtClientControl nonRtClientCtrl;
  2442. + BridgeNonRtServerControl nonRtServerCtrl;
  2443. +
  2444. + BridgeProcess *childprocess = nullptr;
  2445. +
  2446. + void readMessages();
  2447. + void setLastError(const char *error);
  2448. +};
  2449. +
  2450. +// ----------------------------------------------------------------------------
  2451. diff --git a/plugins/carla/carla-patchbay-wrapper.c b/plugins/carla/carla-patchbay-wrapper.c
  2452. new file mode 100644
  2453. index 000000000..d3f2a6f18
  2454. --- /dev/null
  2455. +++ b/plugins/carla/carla-patchbay-wrapper.c
  2456. @@ -0,0 +1,517 @@
  2457. +/*
  2458. + * Carla plugin for OBS
  2459. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  2460. + * SPDX-License-Identifier: GPL-2.0-or-later
  2461. + */
  2462. +
  2463. +#include "carla-wrapper.h"
  2464. +#include "common.h"
  2465. +#include "qtutils.h"
  2466. +
  2467. +#include <util/platform.h>
  2468. +
  2469. +#include "CarlaNativePlugin.h"
  2470. +
  2471. +// If this changes we need to adapt Carla side for matching port count
  2472. +_Static_assert(MAX_AV_PLANES == 8, "expected 8 IO");
  2473. +
  2474. +// ----------------------------------------------------------------------------
  2475. +// helper methods
  2476. +
  2477. +struct carla_main_thread_param_change {
  2478. + const NativePluginDescriptor *descriptor;
  2479. + NativePluginHandle handle;
  2480. + uint32_t index;
  2481. + float value;
  2482. +};
  2483. +
  2484. +static void carla_main_thread_param_change(void *data)
  2485. +{
  2486. + struct carla_main_thread_param_change *priv = data;
  2487. + priv->descriptor->ui_set_parameter_value(priv->handle, priv->index,
  2488. + priv->value);
  2489. + bfree(data);
  2490. +}
  2491. +
  2492. +// ----------------------------------------------------------------------------
  2493. +// private data methods
  2494. +
  2495. +struct carla_param_data {
  2496. + uint32_t hints;
  2497. + float min, max;
  2498. +};
  2499. +
  2500. +struct carla_priv {
  2501. + obs_source_t *source;
  2502. + uint32_t bufferSize;
  2503. + double sampleRate;
  2504. + const NativePluginDescriptor *descriptor;
  2505. + NativePluginHandle handle;
  2506. + NativeHostDescriptor host;
  2507. + NativeTimeInfo timeInfo;
  2508. +
  2509. + // cached parameter info
  2510. + uint32_t paramCount;
  2511. + struct carla_param_data *paramDetails;
  2512. +
  2513. + // update properties when timeout is reached, 0 means do nothing
  2514. + uint64_t update_request;
  2515. +
  2516. + // keep track of active state
  2517. + volatile bool activated;
  2518. +};
  2519. +
  2520. +// ----------------------------------------------------------------------------
  2521. +// carla host methods
  2522. +
  2523. +static uint32_t host_get_buffer_size(NativeHostHandle handle)
  2524. +{
  2525. + const struct carla_priv *priv = handle;
  2526. + return priv->bufferSize;
  2527. +}
  2528. +
  2529. +static double host_get_sample_rate(NativeHostHandle handle)
  2530. +{
  2531. + const struct carla_priv *priv = handle;
  2532. + return priv->sampleRate;
  2533. +}
  2534. +
  2535. +static bool host_is_offline(NativeHostHandle handle)
  2536. +{
  2537. + UNUSED_PARAMETER(handle);
  2538. + return false;
  2539. +}
  2540. +
  2541. +static const NativeTimeInfo *host_get_time_info(NativeHostHandle handle)
  2542. +{
  2543. + const struct carla_priv *priv = handle;
  2544. + return &priv->timeInfo;
  2545. +}
  2546. +
  2547. +static bool host_write_midi_event(NativeHostHandle handle,
  2548. + const NativeMidiEvent *event)
  2549. +{
  2550. + UNUSED_PARAMETER(handle);
  2551. + UNUSED_PARAMETER(event);
  2552. + return false;
  2553. +}
  2554. +
  2555. +static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index,
  2556. + float value)
  2557. +{
  2558. + struct carla_priv *priv = handle;
  2559. +
  2560. + if (index >= priv->paramCount)
  2561. + return;
  2562. +
  2563. + // skip parameters that we do not show
  2564. + const uint32_t hints = priv->paramDetails[index].hints;
  2565. + if ((hints & NATIVE_PARAMETER_IS_ENABLED) == 0)
  2566. + return;
  2567. + if (hints & NATIVE_PARAMETER_IS_OUTPUT)
  2568. + return;
  2569. +
  2570. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  2571. + param_index_to_name(index, pname);
  2572. +
  2573. + obs_source_t *source = priv->source;
  2574. + obs_data_t *settings = obs_source_get_settings(source);
  2575. +
  2576. + /**/ if (hints & NATIVE_PARAMETER_IS_BOOLEAN)
  2577. + obs_data_set_bool(settings, pname, value > 0.5f ? 1.f : 0.f);
  2578. + else if (hints & NATIVE_PARAMETER_IS_INTEGER)
  2579. + obs_data_set_int(settings, pname, (int)value);
  2580. + else
  2581. + obs_data_set_double(settings, pname, value);
  2582. +
  2583. + obs_data_release(settings);
  2584. +
  2585. + postpone_update_request(&priv->update_request);
  2586. +}
  2587. +
  2588. +static void host_ui_midi_program_changed(NativeHostHandle handle,
  2589. + uint8_t channel, uint32_t bank,
  2590. + uint32_t program)
  2591. +{
  2592. + UNUSED_PARAMETER(handle);
  2593. + UNUSED_PARAMETER(channel);
  2594. + UNUSED_PARAMETER(bank);
  2595. + UNUSED_PARAMETER(program);
  2596. +}
  2597. +
  2598. +static void host_ui_custom_data_changed(NativeHostHandle handle,
  2599. + const char *key, const char *value)
  2600. +{
  2601. + UNUSED_PARAMETER(handle);
  2602. + UNUSED_PARAMETER(key);
  2603. + UNUSED_PARAMETER(value);
  2604. +}
  2605. +
  2606. +static void host_ui_closed(NativeHostHandle handle)
  2607. +{
  2608. + UNUSED_PARAMETER(handle);
  2609. +}
  2610. +
  2611. +static const char *host_ui_open_file(NativeHostHandle handle, bool isDir,
  2612. + const char *title, const char *filter)
  2613. +{
  2614. + UNUSED_PARAMETER(handle);
  2615. + return carla_qt_file_dialog(false, isDir, title, filter);
  2616. +}
  2617. +
  2618. +static const char *host_ui_save_file(NativeHostHandle handle, bool isDir,
  2619. + const char *title, const char *filter)
  2620. +{
  2621. + UNUSED_PARAMETER(handle);
  2622. + return carla_qt_file_dialog(true, isDir, title, filter);
  2623. +}
  2624. +
  2625. +static intptr_t host_dispatcher(NativeHostHandle handle,
  2626. + NativeHostDispatcherOpcode opcode,
  2627. + int32_t index, intptr_t value, void *ptr,
  2628. + float opt)
  2629. +{
  2630. + UNUSED_PARAMETER(index);
  2631. + UNUSED_PARAMETER(value);
  2632. + UNUSED_PARAMETER(ptr);
  2633. + UNUSED_PARAMETER(opt);
  2634. +
  2635. + struct carla_priv *priv = handle;
  2636. +
  2637. + switch (opcode) {
  2638. + case NATIVE_HOST_OPCODE_NULL:
  2639. + case NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS:
  2640. + case NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM:
  2641. + break;
  2642. + case NATIVE_HOST_OPCODE_UPDATE_PARAMETER:
  2643. + case NATIVE_HOST_OPCODE_RELOAD_PARAMETERS:
  2644. + case NATIVE_HOST_OPCODE_RELOAD_ALL:
  2645. + postpone_update_request(&priv->update_request);
  2646. + break;
  2647. + case NATIVE_HOST_OPCODE_GET_FILE_PATH:
  2648. + case NATIVE_HOST_OPCODE_HOST_IDLE:
  2649. + case NATIVE_HOST_OPCODE_INTERNAL_PLUGIN:
  2650. + case NATIVE_HOST_OPCODE_PREVIEW_BUFFER_DATA:
  2651. + case NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY:
  2652. + case NATIVE_HOST_OPCODE_REQUEST_IDLE:
  2653. + case NATIVE_HOST_OPCODE_UI_UNAVAILABLE:
  2654. + case NATIVE_HOST_OPCODE_UI_RESIZE:
  2655. + case NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER:
  2656. + break;
  2657. + }
  2658. +
  2659. + return 0;
  2660. +}
  2661. +
  2662. +// ----------------------------------------------------------------------------
  2663. +// carla + obs integration methods
  2664. +
  2665. +struct carla_priv *carla_priv_create(obs_source_t *source,
  2666. + enum buffer_size_mode bufsize,
  2667. + uint32_t srate)
  2668. +{
  2669. + const NativePluginDescriptor *descriptor =
  2670. + carla_get_native_patchbay_obs_plugin();
  2671. + if (descriptor == NULL)
  2672. + return NULL;
  2673. +
  2674. + struct carla_priv *priv = bzalloc(sizeof(struct carla_priv));
  2675. + if (priv == NULL)
  2676. + return NULL;
  2677. +
  2678. + priv->source = source;
  2679. + priv->bufferSize = bufsize_mode_to_frames(bufsize);
  2680. + priv->sampleRate = srate;
  2681. + priv->descriptor = descriptor;
  2682. +
  2683. + {
  2684. + // resource dir swaps .../lib/carla for .../share/carla/resources
  2685. + const char *const binpath = get_carla_bin_path();
  2686. + const size_t binlen = strlen(binpath);
  2687. + char *const respath = bmalloc(binlen + 13);
  2688. + memcpy(respath, binpath, binlen - 9);
  2689. + memcpy(respath + (binlen - 9), "share/carla/resources", 22);
  2690. +
  2691. + NativeHostDescriptor host = {
  2692. + .handle = priv,
  2693. + .resourceDir = respath,
  2694. + .uiName = "Carla-OBS",
  2695. + .uiParentId = 0,
  2696. + .get_buffer_size = host_get_buffer_size,
  2697. + .get_sample_rate = host_get_sample_rate,
  2698. + .is_offline = host_is_offline,
  2699. + .get_time_info = host_get_time_info,
  2700. + .write_midi_event = host_write_midi_event,
  2701. + .ui_parameter_changed = host_ui_parameter_changed,
  2702. + .ui_midi_program_changed = host_ui_midi_program_changed,
  2703. + .ui_custom_data_changed = host_ui_custom_data_changed,
  2704. + .ui_closed = host_ui_closed,
  2705. + .ui_open_file = host_ui_open_file,
  2706. + .ui_save_file = host_ui_save_file,
  2707. + .dispatcher = host_dispatcher};
  2708. + priv->host = host;
  2709. + }
  2710. +
  2711. + {
  2712. + NativeTimeInfo timeInfo = {
  2713. + .usecs = os_gettime_ns() / 1000,
  2714. + };
  2715. + priv->timeInfo = timeInfo;
  2716. + }
  2717. +
  2718. + priv->handle = descriptor->instantiate(&priv->host);
  2719. + if (priv->handle == NULL) {
  2720. + bfree(priv);
  2721. + return NULL;
  2722. + }
  2723. +
  2724. + return priv;
  2725. +}
  2726. +
  2727. +void carla_priv_destroy(struct carla_priv *priv)
  2728. +{
  2729. + if (priv->activated)
  2730. + carla_priv_deactivate(priv);
  2731. +
  2732. + priv->descriptor->cleanup(priv->handle);
  2733. + bfree(priv->paramDetails);
  2734. + bfree((char *)priv->host.resourceDir);
  2735. + bfree(priv);
  2736. +}
  2737. +
  2738. +// ----------------------------------------------------------------------------
  2739. +
  2740. +void carla_priv_activate(struct carla_priv *priv)
  2741. +{
  2742. + priv->descriptor->activate(priv->handle);
  2743. + priv->activated = true;
  2744. +}
  2745. +
  2746. +void carla_priv_deactivate(struct carla_priv *priv)
  2747. +{
  2748. + priv->activated = false;
  2749. + priv->descriptor->deactivate(priv->handle);
  2750. +}
  2751. +
  2752. +void carla_priv_process_audio(struct carla_priv *priv,
  2753. + float *buffers[MAX_AV_PLANES], uint32_t frames)
  2754. +{
  2755. + priv->timeInfo.usecs = os_gettime_ns() / 1000;
  2756. + priv->descriptor->process(priv->handle, buffers, buffers, frames, NULL,
  2757. + 0);
  2758. +}
  2759. +
  2760. +void carla_priv_idle(struct carla_priv *priv)
  2761. +{
  2762. + priv->descriptor->ui_idle(priv->handle);
  2763. + handle_update_request(priv->source, &priv->update_request);
  2764. +}
  2765. +
  2766. +void carla_priv_save(struct carla_priv *priv, obs_data_t *settings)
  2767. +{
  2768. + char *state = priv->descriptor->get_state(priv->handle);
  2769. + if (state) {
  2770. + obs_data_set_string(settings, "state", state);
  2771. + free(state);
  2772. + }
  2773. +}
  2774. +
  2775. +void carla_priv_load(struct carla_priv *priv, obs_data_t *settings)
  2776. +{
  2777. + const char *state = obs_data_get_string(settings, "state");
  2778. + if (state)
  2779. + priv->descriptor->set_state(priv->handle, state);
  2780. +}
  2781. +
  2782. +// ----------------------------------------------------------------------------
  2783. +
  2784. +uint32_t carla_priv_get_num_channels(struct carla_priv *priv)
  2785. +{
  2786. + UNUSED_PARAMETER(priv);
  2787. + return 8;
  2788. +}
  2789. +
  2790. +void carla_priv_set_buffer_size(struct carla_priv *priv,
  2791. + enum buffer_size_mode bufsize)
  2792. +{
  2793. + const uint32_t new_buffer_size = bufsize_mode_to_frames(bufsize);
  2794. + const bool activated = priv->activated;
  2795. +
  2796. + if (activated)
  2797. + carla_priv_deactivate(priv);
  2798. +
  2799. + priv->bufferSize = new_buffer_size;
  2800. + priv->descriptor->dispatcher(priv->handle,
  2801. + NATIVE_PLUGIN_OPCODE_BUFFER_SIZE_CHANGED,
  2802. + new_buffer_size, 0, NULL, 0.f);
  2803. +
  2804. + if (activated)
  2805. + carla_priv_activate(priv);
  2806. +}
  2807. +
  2808. +// ----------------------------------------------------------------------------
  2809. +
  2810. +static bool carla_priv_param_changed(void *data, obs_properties_t *props,
  2811. + obs_property_t *property,
  2812. + obs_data_t *settings)
  2813. +{
  2814. + UNUSED_PARAMETER(props);
  2815. +
  2816. + struct carla_priv *priv = data;
  2817. +
  2818. + const char *const pname = obs_property_name(property);
  2819. + if (pname == NULL)
  2820. + return false;
  2821. +
  2822. + const char *pname2 = pname + 1;
  2823. + while (*pname2 == '0')
  2824. + ++pname2;
  2825. +
  2826. + const int pindex = atoi(pname2);
  2827. +
  2828. + if (pindex < 0 || pindex >= (int)priv->paramCount)
  2829. + return false;
  2830. +
  2831. + const float min = priv->paramDetails[pindex].min;
  2832. + const float max = priv->paramDetails[pindex].max;
  2833. +
  2834. + float value;
  2835. + switch (obs_property_get_type(property)) {
  2836. + case OBS_PROPERTY_BOOL:
  2837. + value = obs_data_get_bool(settings, pname) ? max : min;
  2838. + break;
  2839. + case OBS_PROPERTY_INT:
  2840. + value = (float)obs_data_get_int(settings, pname);
  2841. + if (value < min)
  2842. + value = min;
  2843. + else if (value > max)
  2844. + value = max;
  2845. + break;
  2846. + case OBS_PROPERTY_FLOAT:
  2847. + value = (float)obs_data_get_double(settings, pname);
  2848. + if (value < min)
  2849. + value = min;
  2850. + else if (value > max)
  2851. + value = max;
  2852. + break;
  2853. + default:
  2854. + return false;
  2855. + }
  2856. +
  2857. + priv->descriptor->set_parameter_value(priv->handle, pindex, value);
  2858. +
  2859. + // UI param change notification needs to happen on main thread
  2860. + struct carla_main_thread_param_change mchange = {
  2861. + .descriptor = priv->descriptor,
  2862. + .handle = priv->handle,
  2863. + .index = pindex,
  2864. + .value = value};
  2865. + struct carla_main_thread_param_change *mchangeptr =
  2866. + bmalloc(sizeof(mchange));
  2867. + *mchangeptr = mchange;
  2868. + carla_qt_callback_on_main_thread(carla_main_thread_param_change,
  2869. + mchangeptr);
  2870. +
  2871. + return false;
  2872. +}
  2873. +
  2874. +static bool carla_priv_show_gui_callback(obs_properties_t *props,
  2875. + obs_property_t *property, void *data)
  2876. +{
  2877. + UNUSED_PARAMETER(props);
  2878. + UNUSED_PARAMETER(property);
  2879. +
  2880. + struct carla_priv *priv = data;
  2881. +
  2882. + priv->descriptor->ui_show(priv->handle, true);
  2883. +
  2884. + return false;
  2885. +}
  2886. +
  2887. +void carla_priv_readd_properties(struct carla_priv *priv,
  2888. + obs_properties_t *props, bool reset)
  2889. +{
  2890. + obs_data_t *settings = obs_source_get_settings(priv->source);
  2891. +
  2892. + if (priv->descriptor->hints & NATIVE_PLUGIN_HAS_UI) {
  2893. + obs_properties_add_button2(props, PROP_SHOW_GUI,
  2894. + obs_module_text("Show custom GUI"),
  2895. + carla_priv_show_gui_callback, priv);
  2896. + }
  2897. +
  2898. + uint32_t params = priv->descriptor->get_parameter_count(priv->handle);
  2899. + if (params > MAX_PARAMS)
  2900. + params = MAX_PARAMS;
  2901. +
  2902. + bfree(priv->paramDetails);
  2903. + priv->paramCount = params;
  2904. + priv->paramDetails = bzalloc(sizeof(struct carla_param_data) * params);
  2905. +
  2906. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  2907. +
  2908. + for (uint32_t i = 0; i < params; ++i) {
  2909. + const NativeParameter *const info =
  2910. + priv->descriptor->get_parameter_info(priv->handle, i);
  2911. +
  2912. + if ((info->hints & NATIVE_PARAMETER_IS_ENABLED) == 0)
  2913. + continue;
  2914. + if (info->hints & NATIVE_PARAMETER_IS_OUTPUT)
  2915. + continue;
  2916. +
  2917. + param_index_to_name(i, pname);
  2918. + priv->paramDetails[i].hints = info->hints;
  2919. + priv->paramDetails[i].min = info->ranges.min;
  2920. + priv->paramDetails[i].max = info->ranges.max;
  2921. +
  2922. + obs_property_t *prop;
  2923. +
  2924. + if (info->hints & NATIVE_PARAMETER_IS_BOOLEAN) {
  2925. + prop = obs_properties_add_bool(props, pname,
  2926. + info->name);
  2927. +
  2928. + obs_data_set_default_bool(settings, pname,
  2929. + info->ranges.def ==
  2930. + info->ranges.max);
  2931. +
  2932. + if (reset)
  2933. + obs_data_set_bool(settings, pname,
  2934. + info->ranges.def ==
  2935. + info->ranges.max);
  2936. + } else if (info->hints & NATIVE_PARAMETER_IS_INTEGER) {
  2937. + prop = obs_properties_add_int_slider(
  2938. + props, pname, info->name, (int)info->ranges.min,
  2939. + (int)info->ranges.max, (int)info->ranges.step);
  2940. +
  2941. + obs_data_set_default_int(settings, pname,
  2942. + (int)info->ranges.def);
  2943. +
  2944. + if (info->unit && *info->unit)
  2945. + obs_property_int_set_suffix(prop, info->unit);
  2946. +
  2947. + if (reset)
  2948. + obs_data_set_int(settings, pname,
  2949. + (int)info->ranges.def);
  2950. + } else {
  2951. + prop = obs_properties_add_float_slider(
  2952. + props, pname, info->name, info->ranges.min,
  2953. + info->ranges.max, info->ranges.step);
  2954. +
  2955. + obs_data_set_default_double(settings, pname,
  2956. + info->ranges.def);
  2957. +
  2958. + if (info->unit && *info->unit)
  2959. + obs_property_float_set_suffix(prop, info->unit);
  2960. +
  2961. + if (reset)
  2962. + obs_data_set_double(settings, pname,
  2963. + info->ranges.def);
  2964. + }
  2965. +
  2966. + obs_property_set_modified_callback2(
  2967. + prop, carla_priv_param_changed, priv);
  2968. + }
  2969. +
  2970. + obs_data_release(settings);
  2971. +}
  2972. +
  2973. +// ----------------------------------------------------------------------------
  2974. diff --git a/plugins/carla/carla-wrapper.h b/plugins/carla/carla-wrapper.h
  2975. new file mode 100644
  2976. index 000000000..f7dd37c9a
  2977. --- /dev/null
  2978. +++ b/plugins/carla/carla-wrapper.h
  2979. @@ -0,0 +1,73 @@
  2980. +/*
  2981. + * Carla plugin for OBS
  2982. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  2983. + * SPDX-License-Identifier: GPL-2.0-or-later
  2984. + */
  2985. +
  2986. +#pragma once
  2987. +
  2988. +#include <obs-module.h>
  2989. +
  2990. +// maximum buffer used, can be smaller
  2991. +#define MAX_AUDIO_BUFFER_SIZE 512
  2992. +
  2993. +enum buffer_size_mode {
  2994. + buffer_size_direct,
  2995. + buffer_size_buffered_128,
  2996. + buffer_size_buffered_256,
  2997. + buffer_size_buffered_512,
  2998. + buffer_size_buffered_max = buffer_size_buffered_512
  2999. +};
  3000. +
  3001. +// ----------------------------------------------------------------------------
  3002. +// helper methods
  3003. +
  3004. +static inline uint32_t bufsize_mode_to_frames(enum buffer_size_mode bufsize)
  3005. +{
  3006. + switch (bufsize) {
  3007. + case buffer_size_buffered_128:
  3008. + return 128;
  3009. + case buffer_size_buffered_256:
  3010. + return 256;
  3011. + default:
  3012. + return MAX_AUDIO_BUFFER_SIZE;
  3013. + }
  3014. +}
  3015. +
  3016. +// ----------------------------------------------------------------------------
  3017. +// carla + obs integration methods
  3018. +
  3019. +#ifdef __cplusplus
  3020. +extern "C" {
  3021. +#endif
  3022. +
  3023. +struct carla_priv;
  3024. +
  3025. +struct carla_priv *carla_priv_create(obs_source_t *source,
  3026. + enum buffer_size_mode bufsize,
  3027. + uint32_t srate);
  3028. +void carla_priv_destroy(struct carla_priv *carla);
  3029. +
  3030. +void carla_priv_activate(struct carla_priv *carla);
  3031. +void carla_priv_deactivate(struct carla_priv *carla);
  3032. +void carla_priv_process_audio(struct carla_priv *carla,
  3033. + float *buffers[MAX_AV_PLANES], uint32_t frames);
  3034. +
  3035. +void carla_priv_idle(struct carla_priv *carla);
  3036. +
  3037. +void carla_priv_save(struct carla_priv *carla, obs_data_t *settings);
  3038. +void carla_priv_load(struct carla_priv *carla, obs_data_t *settings);
  3039. +
  3040. +uint32_t carla_priv_get_num_channels(struct carla_priv *carla);
  3041. +
  3042. +void carla_priv_set_buffer_size(struct carla_priv *carla,
  3043. + enum buffer_size_mode bufsize);
  3044. +
  3045. +void carla_priv_readd_properties(struct carla_priv *carla,
  3046. + obs_properties_t *props, bool reset);
  3047. +
  3048. +#ifdef __cplusplus
  3049. +}
  3050. +#endif
  3051. +
  3052. +// ----------------------------------------------------------------------------
  3053. diff --git a/plugins/carla/carla.c b/plugins/carla/carla.c
  3054. new file mode 100644
  3055. index 000000000..f02470721
  3056. --- /dev/null
  3057. +++ b/plugins/carla/carla.c
  3058. @@ -0,0 +1,530 @@
  3059. +/*
  3060. + * Carla plugin for OBS
  3061. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  3062. + * SPDX-License-Identifier: GPL-2.0-or-later
  3063. + */
  3064. +
  3065. +// for audio generator thread
  3066. +#include <pthread.h>
  3067. +
  3068. +#include <obs-module.h>
  3069. +#include <util/platform.h>
  3070. +
  3071. +#include "carla-wrapper.h"
  3072. +#include "common.h"
  3073. +
  3074. +#ifndef CARLA_MODULE_ID
  3075. +#error CARLA_MODULE_ID undefined
  3076. +#endif
  3077. +
  3078. +#ifndef CARLA_MODULE_NAME
  3079. +#error CARLA_MODULE_NAME undefined
  3080. +#endif
  3081. +
  3082. +// --------------------------------------------------------------------------------------------------------------------
  3083. +
  3084. +struct carla_data {
  3085. + // carla host details, intentionally kept private so we can easily swap internals
  3086. + struct carla_priv *priv;
  3087. +
  3088. + // current OBS config
  3089. + bool activated;
  3090. + uint32_t sample_rate;
  3091. + obs_source_t *source;
  3092. +
  3093. + // filter related options
  3094. + size_t channels;
  3095. +
  3096. + // audio generator thread
  3097. + bool audiogen_enabled;
  3098. + volatile bool audiogen_running;
  3099. + pthread_t audiogen_thread;
  3100. +
  3101. + // internal buffering
  3102. + float *buffers[MAX_AV_PLANES];
  3103. + uint16_t buffer_head;
  3104. + uint16_t buffer_tail;
  3105. + enum buffer_size_mode buffer_size_mode;
  3106. +
  3107. + // dummy buffer for unused audio channels
  3108. + float *dummybuffer;
  3109. +};
  3110. +
  3111. +// --------------------------------------------------------------------------------------------------------------------
  3112. +// private methods
  3113. +
  3114. +static enum speaker_layout carla_obs_channels_to_speakers(const size_t channels)
  3115. +{
  3116. + switch (channels) {
  3117. + case 1:
  3118. + return SPEAKERS_MONO;
  3119. + case 2:
  3120. + return SPEAKERS_STEREO;
  3121. + case 3:
  3122. + return SPEAKERS_2POINT1;
  3123. + case 4:
  3124. + return SPEAKERS_4POINT0;
  3125. + case 5:
  3126. + return SPEAKERS_4POINT1;
  3127. + case 6:
  3128. + return SPEAKERS_5POINT1;
  3129. + // FIXME missing case for 7 channels
  3130. + case 8:
  3131. + return SPEAKERS_7POINT1;
  3132. + // use stereo as fallback
  3133. + default:
  3134. + return SPEAKERS_STEREO;
  3135. + }
  3136. +}
  3137. +
  3138. +static void *carla_obs_audio_gen_thread(void *data)
  3139. +{
  3140. + struct carla_data *carla = data;
  3141. +
  3142. + struct obs_source_audio out = {
  3143. + .format = AUDIO_FORMAT_FLOAT_PLANAR,
  3144. + .samples_per_sec = carla->sample_rate,
  3145. + };
  3146. +
  3147. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c)
  3148. + out.data[c] = (const uint8_t *)carla->buffers[c];
  3149. +
  3150. + const uint32_t sample_rate = carla->sample_rate;
  3151. + const uint64_t start_time = out.timestamp = os_gettime_ns();
  3152. + uint64_t total_samples = 0;
  3153. +
  3154. + while (carla->audiogen_running) {
  3155. + const uint32_t buffer_size =
  3156. + bufsize_mode_to_frames(carla->buffer_size_mode);
  3157. +
  3158. + out.frames = buffer_size;
  3159. + out.speakers = carla_obs_channels_to_speakers(
  3160. + carla_priv_get_num_channels(carla->priv));
  3161. + carla_priv_process_audio(carla->priv, carla->buffers,
  3162. + buffer_size);
  3163. + obs_source_output_audio(carla->source, &out);
  3164. +
  3165. + if (!carla->audiogen_running)
  3166. + break;
  3167. +
  3168. + total_samples += buffer_size;
  3169. + out.timestamp = start_time +
  3170. + audio_frames_to_ns(sample_rate, total_samples);
  3171. +
  3172. + os_sleepto_ns_fast(out.timestamp);
  3173. + }
  3174. +
  3175. + return NULL;
  3176. +}
  3177. +
  3178. +static void carla_obs_idle_callback(void *data, float unused)
  3179. +{
  3180. + UNUSED_PARAMETER(unused);
  3181. + struct carla_data *carla = data;
  3182. + carla_priv_idle(carla->priv);
  3183. +}
  3184. +
  3185. +// --------------------------------------------------------------------------------------------------------------------
  3186. +// obs plugin methods
  3187. +
  3188. +static void carla_obs_deactivate(void *data);
  3189. +
  3190. +static const char *carla_obs_get_name(void *data)
  3191. +{
  3192. + return !strcmp(data, "filter")
  3193. + ? obs_module_text(CARLA_MODULE_NAME " Filter")
  3194. + : obs_module_text(CARLA_MODULE_NAME " Generator/Source");
  3195. +}
  3196. +
  3197. +static void *carla_obs_create(obs_data_t *settings, obs_source_t *source,
  3198. + bool isFilter)
  3199. +{
  3200. + UNUSED_PARAMETER(settings);
  3201. +
  3202. + const audio_t *audio = obs_get_audio();
  3203. + const size_t channels = audio_output_get_channels(audio);
  3204. + const uint32_t sample_rate = audio_output_get_sample_rate(audio);
  3205. +
  3206. + if (sample_rate == 0 || (isFilter && channels == 0))
  3207. + return NULL;
  3208. +
  3209. + struct carla_data *carla = bzalloc(sizeof(*carla));
  3210. + if (carla == NULL)
  3211. + return NULL;
  3212. +
  3213. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) {
  3214. + carla->buffers[c] =
  3215. + bzalloc(sizeof(float) * MAX_AUDIO_BUFFER_SIZE);
  3216. + if (carla->buffers[c] == NULL)
  3217. + goto fail1;
  3218. + }
  3219. +
  3220. + carla->dummybuffer = bzalloc(sizeof(float) * MAX_AUDIO_BUFFER_SIZE);
  3221. + if (carla->dummybuffer == NULL)
  3222. + goto fail2;
  3223. +
  3224. + // prefer no-latency mode for filter, lowest latency for generator
  3225. + const enum buffer_size_mode bufsize =
  3226. + isFilter ? buffer_size_direct : buffer_size_buffered_128;
  3227. +
  3228. + struct carla_priv *priv =
  3229. + carla_priv_create(source, bufsize, sample_rate);
  3230. + if (carla == NULL)
  3231. + goto fail3;
  3232. +
  3233. + carla->priv = priv;
  3234. + carla->source = source;
  3235. + carla->channels = channels;
  3236. + carla->sample_rate = sample_rate;
  3237. +
  3238. + carla->buffer_head = 0;
  3239. + carla->buffer_tail = UINT16_MAX;
  3240. + carla->buffer_size_mode = bufsize;
  3241. +
  3242. + // audio generator, aka input source
  3243. + carla->audiogen_enabled = !isFilter;
  3244. +
  3245. + obs_add_tick_callback(carla_obs_idle_callback, carla);
  3246. +
  3247. + return carla;
  3248. +
  3249. +fail3:
  3250. + bfree(carla->dummybuffer);
  3251. +
  3252. +fail2:
  3253. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c)
  3254. + bfree(carla->buffers[c]);
  3255. +
  3256. +fail1:
  3257. + bfree(carla);
  3258. + return NULL;
  3259. +}
  3260. +
  3261. +static void *carla_obs_create_filter(obs_data_t *settings, obs_source_t *source)
  3262. +{
  3263. + return carla_obs_create(settings, source, true);
  3264. +}
  3265. +
  3266. +static void *carla_obs_create_input(obs_data_t *settings, obs_source_t *source)
  3267. +{
  3268. + return carla_obs_create(settings, source, false);
  3269. +}
  3270. +
  3271. +static void carla_obs_destroy(void *data)
  3272. +{
  3273. + struct carla_data *carla = data;
  3274. +
  3275. + if (carla->activated)
  3276. + carla_obs_deactivate(carla);
  3277. +
  3278. + obs_remove_tick_callback(carla_obs_idle_callback, carla);
  3279. +
  3280. + carla_priv_destroy(carla->priv);
  3281. +
  3282. + bfree(carla->dummybuffer);
  3283. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c)
  3284. + bfree(carla->buffers[c]);
  3285. + bfree(carla);
  3286. +}
  3287. +
  3288. +static bool carla_obs_bufsize_callback(void *data, obs_properties_t *props,
  3289. + obs_property_t *list,
  3290. + obs_data_t *settings)
  3291. +{
  3292. + UNUSED_PARAMETER(props);
  3293. + UNUSED_PARAMETER(list);
  3294. +
  3295. + struct carla_data *carla = data;
  3296. +
  3297. + enum buffer_size_mode bufsize;
  3298. + const char *const value =
  3299. + obs_data_get_string(settings, PROP_BUFFER_SIZE);
  3300. +
  3301. + /**/ if (!strcmp(value, "direct"))
  3302. + bufsize = buffer_size_direct;
  3303. + else if (!strcmp(value, "128"))
  3304. + bufsize = buffer_size_buffered_128;
  3305. + else if (!strcmp(value, "256"))
  3306. + bufsize = buffer_size_buffered_256;
  3307. + else if (!strcmp(value, "512"))
  3308. + bufsize = buffer_size_buffered_512;
  3309. + else
  3310. + return false;
  3311. +
  3312. + if (carla->buffer_size_mode == bufsize)
  3313. + return false;
  3314. +
  3315. + // deactivate first, to stop audio from processing
  3316. + carla_priv_deactivate(carla->priv);
  3317. +
  3318. + // safely change to new buffer size
  3319. + carla->buffer_size_mode = bufsize;
  3320. + carla_priv_set_buffer_size(carla->priv, bufsize);
  3321. +
  3322. + // activate again
  3323. + carla_priv_activate(carla->priv);
  3324. +
  3325. + return false;
  3326. +}
  3327. +
  3328. +static obs_properties_t *carla_obs_get_properties(void *data)
  3329. +{
  3330. + struct carla_data *carla = data;
  3331. +
  3332. + obs_properties_t *props = obs_properties_create();
  3333. +
  3334. + obs_property_t *list = obs_properties_add_list(
  3335. + props, PROP_BUFFER_SIZE, obs_module_text("Buffer Size"),
  3336. + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
  3337. +
  3338. + if (carla->audiogen_enabled) {
  3339. + obs_property_list_add_string(
  3340. + list, obs_module_text("128 samples"), "128");
  3341. + obs_property_list_add_string(
  3342. + list, obs_module_text("256 samples"), "256");
  3343. + obs_property_list_add_string(
  3344. + list, obs_module_text("512 samples"), "512");
  3345. + } else {
  3346. + obs_property_list_add_string(
  3347. + list, obs_module_text("Direct (variable buffer)"),
  3348. + "direct");
  3349. + obs_property_list_add_string(
  3350. + list,
  3351. + obs_module_text(
  3352. + "128 samples (fixed buffer with latency)"),
  3353. + "128");
  3354. + obs_property_list_add_string(
  3355. + list,
  3356. + obs_module_text(
  3357. + "256 samples (fixed buffer with latency)"),
  3358. + "256");
  3359. + obs_property_list_add_string(
  3360. + list,
  3361. + obs_module_text(
  3362. + "512 samples (fixed buffer with latency)"),
  3363. + "512");
  3364. + }
  3365. +
  3366. + obs_property_set_modified_callback2(list, carla_obs_bufsize_callback,
  3367. + carla);
  3368. +
  3369. + carla_priv_readd_properties(carla->priv, props, false);
  3370. +
  3371. + return props;
  3372. +}
  3373. +
  3374. +static void carla_obs_activate(void *data)
  3375. +{
  3376. + struct carla_data *carla = data;
  3377. + assert(!carla->activated);
  3378. +
  3379. + if (carla->activated)
  3380. + return;
  3381. +
  3382. + carla->activated = true;
  3383. +
  3384. + carla_priv_activate(carla->priv);
  3385. +
  3386. + if (carla->audiogen_enabled) {
  3387. + assert(!carla->audiogen_running);
  3388. + carla->audiogen_running = true;
  3389. + pthread_create(&carla->audiogen_thread, NULL,
  3390. + carla_obs_audio_gen_thread, carla);
  3391. + }
  3392. +}
  3393. +
  3394. +static void carla_obs_deactivate(void *data)
  3395. +{
  3396. + struct carla_data *carla = data;
  3397. + assert(carla->activated);
  3398. +
  3399. + if (!carla->activated)
  3400. + return;
  3401. +
  3402. + carla->activated = false;
  3403. +
  3404. + if (carla->audiogen_running) {
  3405. + carla->audiogen_running = false;
  3406. + pthread_join(carla->audiogen_thread, NULL);
  3407. + }
  3408. +
  3409. + carla_priv_deactivate(carla->priv);
  3410. +}
  3411. +
  3412. +static void carla_obs_filter_audio_direct(struct carla_data *carla,
  3413. + struct obs_audio_data *audio)
  3414. +{
  3415. + uint32_t frames = audio->frames;
  3416. + float *obsbuffers[MAX_AV_PLANES];
  3417. +
  3418. + // process in blocks up to MAX_AUDIO_BUFFER_SIZE
  3419. + for (uint32_t i = 0; frames != 0;) {
  3420. + const uint32_t stepframes = frames >= MAX_AUDIO_BUFFER_SIZE
  3421. + ? MAX_AUDIO_BUFFER_SIZE
  3422. + : frames;
  3423. +
  3424. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c)
  3425. + obsbuffers[c] = audio->data[c]
  3426. + ? ((float *)audio->data[c] + i)
  3427. + : carla->dummybuffer;
  3428. +
  3429. + carla_priv_process_audio(carla->priv, obsbuffers, stepframes);
  3430. +
  3431. + memset(carla->dummybuffer, 0, sizeof(float) * stepframes);
  3432. +
  3433. + i += stepframes;
  3434. + frames -= stepframes;
  3435. + }
  3436. +}
  3437. +
  3438. +static void carla_obs_filter_audio_buffered(struct carla_data *carla,
  3439. + struct obs_audio_data *audio)
  3440. +{
  3441. + const uint32_t buffer_size =
  3442. + bufsize_mode_to_frames(carla->buffer_size_mode);
  3443. + const size_t channels = carla->channels;
  3444. + const uint32_t frames = audio->frames;
  3445. +
  3446. + // cast audio buffers to correct type
  3447. + float *obsbuffers[MAX_AV_PLANES];
  3448. +
  3449. + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c)
  3450. + obsbuffers[c] = audio->data[c] ? (float *)audio->data[c]
  3451. + : carla->dummybuffer;
  3452. +
  3453. + // preload some variables before looping section
  3454. + uint16_t buffer_head = carla->buffer_head;
  3455. + uint16_t buffer_tail = carla->buffer_tail;
  3456. +
  3457. + for (uint32_t i = 0, h, t; i < frames; ++i) {
  3458. + // OBS -> plugin internal buffering
  3459. + h = buffer_head++;
  3460. +
  3461. + for (uint8_t c = 0; c < channels; ++c)
  3462. + carla->buffers[c][h] = obsbuffers[c][i];
  3463. +
  3464. + // when we reach the target buffer size, do audio processing
  3465. + if (buffer_head == buffer_size) {
  3466. + buffer_head = 0;
  3467. + carla_priv_process_audio(carla->priv, carla->buffers,
  3468. + buffer_size);
  3469. + memset(carla->dummybuffer, 0,
  3470. + sizeof(float) * buffer_size);
  3471. +
  3472. + // we can now begin to copy back the buffer into OBS
  3473. + if (buffer_tail == UINT16_MAX)
  3474. + buffer_tail = 0;
  3475. + }
  3476. +
  3477. + if (buffer_tail == UINT16_MAX) {
  3478. + // buffering still taking place, skip until first audio cycle
  3479. + for (uint8_t c = 0; c < channels; ++c)
  3480. + obsbuffers[c][i] = 0.f;
  3481. + } else {
  3482. + // plugin -> OBS buffer copy
  3483. + t = buffer_tail++;
  3484. +
  3485. + for (uint8_t c = 0; c < channels; ++c)
  3486. + obsbuffers[c][i] = carla->buffers[c][t];
  3487. +
  3488. + if (buffer_tail == buffer_size)
  3489. + buffer_tail = 0;
  3490. + }
  3491. + }
  3492. +
  3493. + carla->buffer_head = buffer_head;
  3494. + carla->buffer_tail = buffer_tail;
  3495. +}
  3496. +
  3497. +static struct obs_audio_data *
  3498. +carla_obs_filter_audio(void *data, struct obs_audio_data *audio)
  3499. +{
  3500. + struct carla_data *carla = data;
  3501. +
  3502. + switch (carla->buffer_size_mode) {
  3503. + case buffer_size_direct:
  3504. + carla_obs_filter_audio_direct(carla, audio);
  3505. + break;
  3506. + case buffer_size_buffered_128:
  3507. + case buffer_size_buffered_256:
  3508. + case buffer_size_buffered_512:
  3509. + carla_obs_filter_audio_buffered(carla, audio);
  3510. + break;
  3511. + }
  3512. +
  3513. + return audio;
  3514. +}
  3515. +
  3516. +static void carla_obs_save(void *data, obs_data_t *settings)
  3517. +{
  3518. + struct carla_data *carla = data;
  3519. + carla_priv_save(carla->priv, settings);
  3520. +}
  3521. +
  3522. +static void carla_obs_load(void *data, obs_data_t *settings)
  3523. +{
  3524. + struct carla_data *carla = data;
  3525. + carla_priv_load(carla->priv, settings);
  3526. +}
  3527. +
  3528. +// --------------------------------------------------------------------------------------------------------------------
  3529. +
  3530. +OBS_DECLARE_MODULE()
  3531. +OBS_MODULE_USE_DEFAULT_LOCALE("carla", "en-US")
  3532. +OBS_MODULE_AUTHOR("Filipe Coelho")
  3533. +const char *obs_module_name(void)
  3534. +{
  3535. + return CARLA_MODULE_NAME;
  3536. +}
  3537. +
  3538. +bool obs_module_load(void)
  3539. +{
  3540. + const char *carla_bin_path = get_carla_bin_path();
  3541. + if (!carla_bin_path) {
  3542. + blog(LOG_WARNING,
  3543. + "[" CARLA_MODULE_ID "]"
  3544. + " failed to find binaries, will not load module");
  3545. + return false;
  3546. + }
  3547. + blog(LOG_INFO, "[" CARLA_MODULE_ID "] using binary path %s",
  3548. + carla_bin_path);
  3549. +
  3550. + static const struct obs_source_info filter = {
  3551. + .id = CARLA_MODULE_ID "-filter",
  3552. + .type = OBS_SOURCE_TYPE_FILTER,
  3553. + .output_flags = OBS_SOURCE_AUDIO,
  3554. + .get_name = carla_obs_get_name,
  3555. + .create = carla_obs_create_filter,
  3556. + .destroy = carla_obs_destroy,
  3557. + .get_properties = carla_obs_get_properties,
  3558. + .activate = carla_obs_activate,
  3559. + .deactivate = carla_obs_deactivate,
  3560. + .filter_audio = carla_obs_filter_audio,
  3561. + .save = carla_obs_save,
  3562. + .load = carla_obs_load,
  3563. + .type_data = "filter",
  3564. + .icon_type = OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT,
  3565. + };
  3566. + obs_register_source(&filter);
  3567. +
  3568. + static const struct obs_source_info input = {
  3569. + .id = CARLA_MODULE_ID "-input",
  3570. + .type = OBS_SOURCE_TYPE_INPUT,
  3571. + .output_flags = OBS_SOURCE_AUDIO,
  3572. + .get_name = carla_obs_get_name,
  3573. + .create = carla_obs_create_input,
  3574. + .destroy = carla_obs_destroy,
  3575. + .get_properties = carla_obs_get_properties,
  3576. + .activate = carla_obs_activate,
  3577. + .deactivate = carla_obs_deactivate,
  3578. + .save = carla_obs_save,
  3579. + .load = carla_obs_load,
  3580. + .type_data = "input",
  3581. + .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
  3582. + };
  3583. + obs_register_source(&input);
  3584. +
  3585. + return true;
  3586. +}
  3587. +
  3588. +// --------------------------------------------------------------------------------------------------------------------
  3589. diff --git a/plugins/carla/cmake/macos/Info.plist.in b/plugins/carla/cmake/macos/Info.plist.in
  3590. new file mode 100644
  3591. index 000000000..c2d597444
  3592. --- /dev/null
  3593. +++ b/plugins/carla/cmake/macos/Info.plist.in
  3594. @@ -0,0 +1,28 @@
  3595. +<?xml version="1.0" encoding="UTF-8"?>
  3596. +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  3597. +<plist version="1.0">
  3598. +<dict>
  3599. + <key>CFBundleName</key>
  3600. + <string>obs-carla</string>
  3601. + <key>CFBundleIdentifier</key>
  3602. + <string>com.obsproject.carla-bridge</string>
  3603. + <key>CFBundleVersion</key>
  3604. + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
  3605. + <key>CFBundleShortVersionString</key>
  3606. + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
  3607. + <key>CFBundleInfoDictionaryVersion</key>
  3608. + <string>6.0</string>
  3609. + <key>CFBundleExecutable</key>
  3610. + <string>carla-bridge</string>
  3611. + <key>CFBundlePackageType</key>
  3612. + <string>BNDL</string>
  3613. + <key>CFBundleSupportedPlatforms</key>
  3614. + <array>
  3615. + <string>MacOSX</string>
  3616. + </array>
  3617. + <key>LSMinimumSystemVersion</key>
  3618. + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
  3619. + <key>NSHumanReadableCopyright</key>
  3620. + <string>(c) 2023 Filipe Coelho</string>
  3621. +</dict>
  3622. +</plist>
  3623. diff --git a/plugins/carla/common.c b/plugins/carla/common.c
  3624. new file mode 100644
  3625. index 000000000..36470deb5
  3626. --- /dev/null
  3627. +++ b/plugins/carla/common.c
  3628. @@ -0,0 +1,156 @@
  3629. +/*
  3630. + * Carla plugin for OBS
  3631. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  3632. + * SPDX-License-Identifier: GPL-2.0-or-later
  3633. + */
  3634. +
  3635. +#ifndef _WIN32
  3636. +// needed for libdl stuff and strcasestr
  3637. +#ifndef _GNU_SOURCE
  3638. +#define _GNU_SOURCE
  3639. +#endif
  3640. +#include <dlfcn.h>
  3641. +#include <limits.h>
  3642. +#include <stdlib.h>
  3643. +#endif
  3644. +
  3645. +#include <CarlaUtils.h>
  3646. +
  3647. +#include <obs-module.h>
  3648. +#include <util/platform.h>
  3649. +
  3650. +#include "common.h"
  3651. +
  3652. +// ----------------------------------------------------------------------------
  3653. +
  3654. +static char *module_path = NULL;
  3655. +
  3656. +const char *get_carla_bin_path(void)
  3657. +{
  3658. + if (module_path != NULL)
  3659. + return module_path;
  3660. +
  3661. + char *mpath;
  3662. +
  3663. + // check path of linked carla-utils library first
  3664. + const char *const utilspath = carla_get_library_folder();
  3665. + const size_t utilslen = strlen(utilspath);
  3666. +
  3667. + mpath = bmalloc(utilslen + 28);
  3668. + memcpy(mpath, utilspath, utilslen);
  3669. + memcpy(mpath + utilslen, CARLA_OS_SEP_STR "carla-discovery-native", 24);
  3670. +#ifdef _WIN32
  3671. + memcpy(mpath + utilslen + 24, ".exe", 5);
  3672. +#endif
  3673. +
  3674. + if (os_file_exists(mpath)) {
  3675. + mpath[utilslen] = '\0';
  3676. + module_path = mpath;
  3677. + return module_path;
  3678. + }
  3679. +
  3680. + free(mpath);
  3681. +
  3682. +#ifndef _WIN32
  3683. + // check path of this OBS plugin as fallback
  3684. + Dl_info info;
  3685. + dladdr(get_carla_bin_path, &info);
  3686. + mpath = realpath(info.dli_fname, NULL);
  3687. +
  3688. + if (mpath == NULL)
  3689. + return NULL;
  3690. +
  3691. + // truncate to last separator
  3692. + char *lastsep = strrchr(mpath, '/');
  3693. + if (lastsep == NULL)
  3694. + goto free;
  3695. + *lastsep = '\0';
  3696. +
  3697. +#ifdef __APPLE__
  3698. + // running as macOS app bundle, use its binary dir
  3699. + char *appbundlesep = strcasestr(mpath, "/PlugIns/" CARLA_MODULE_ID
  3700. + ".plugin/Contents/MacOS");
  3701. + if (appbundlesep == NULL)
  3702. + goto free;
  3703. + strcpy(appbundlesep, "/MacOS");
  3704. +#endif
  3705. +
  3706. + if (os_file_exists(mpath)) {
  3707. + module_path = bstrdup(mpath);
  3708. + free(mpath);
  3709. + return module_path;
  3710. + }
  3711. +
  3712. +free:
  3713. + free(mpath);
  3714. +#endif // !_WIN32
  3715. +
  3716. + return module_path;
  3717. +}
  3718. +
  3719. +void param_index_to_name(uint32_t index, char name[PARAM_NAME_SIZE])
  3720. +{
  3721. + name[1] = '0' + ((index / 100) % 10);
  3722. + name[2] = '0' + ((index / 10) % 10);
  3723. + name[3] = '0' + ((index / 1) % 10);
  3724. +}
  3725. +
  3726. +void remove_all_props(obs_properties_t *props, obs_data_t *settings)
  3727. +{
  3728. + obs_data_erase(settings, PROP_RELOAD_PLUGIN);
  3729. + obs_properties_remove_by_name(props, PROP_RELOAD_PLUGIN);
  3730. +
  3731. + obs_data_erase(settings, PROP_SHOW_GUI);
  3732. + obs_properties_remove_by_name(props, PROP_SHOW_GUI);
  3733. +
  3734. + obs_data_erase(settings, PROP_CHUNK);
  3735. + obs_properties_remove_by_name(props, PROP_CHUNK);
  3736. +
  3737. + obs_data_erase(settings, PROP_CUSTOM_DATA);
  3738. + obs_properties_remove_by_name(props, PROP_CUSTOM_DATA);
  3739. +
  3740. + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT;
  3741. +
  3742. + for (uint32_t i = 0; i < MAX_PARAMS; ++i) {
  3743. + param_index_to_name(i, pname);
  3744. + obs_data_unset_default_value(settings, pname);
  3745. + obs_data_erase(settings, pname);
  3746. + obs_properties_remove_by_name(props, pname);
  3747. + }
  3748. +}
  3749. +
  3750. +void postpone_update_request(uint64_t *update_req)
  3751. +{
  3752. + *update_req = os_gettime_ns();
  3753. +}
  3754. +
  3755. +void handle_update_request(obs_source_t *source, uint64_t *update_req)
  3756. +{
  3757. + const uint64_t old_update_req = *update_req;
  3758. +
  3759. + if (old_update_req == 0)
  3760. + return;
  3761. +
  3762. + const uint64_t now = os_gettime_ns();
  3763. +
  3764. + // request in the future?
  3765. + if (now < old_update_req) {
  3766. + *update_req = now;
  3767. + return;
  3768. + }
  3769. +
  3770. + if (now - old_update_req >= 100000000ULL) // 100ms
  3771. + {
  3772. + *update_req = 0;
  3773. + signal_handler_signal(obs_source_get_signal_handler(source),
  3774. + "update_properties", NULL);
  3775. + }
  3776. +}
  3777. +
  3778. +void obs_module_unload(void)
  3779. +{
  3780. + bfree(module_path);
  3781. + module_path = NULL;
  3782. +}
  3783. +
  3784. +// ----------------------------------------------------------------------------
  3785. diff --git a/plugins/carla/common.h b/plugins/carla/common.h
  3786. new file mode 100644
  3787. index 000000000..42659702f
  3788. --- /dev/null
  3789. +++ b/plugins/carla/common.h
  3790. @@ -0,0 +1,47 @@
  3791. +/*
  3792. + * Carla plugin for OBS
  3793. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  3794. + * SPDX-License-Identifier: GPL-2.0-or-later
  3795. + */
  3796. +
  3797. +#pragma once
  3798. +
  3799. +#include <obs-module.h>
  3800. +
  3801. +#define MAX_PARAMS 100
  3802. +
  3803. +#define PARAM_NAME_SIZE 5
  3804. +#define PARAM_NAME_INIT \
  3805. + { \
  3806. + 'p', '0', '0', '0', '\0' \
  3807. + }
  3808. +
  3809. +// property names
  3810. +#define PROP_LOAD_FILE "load-file"
  3811. +#define PROP_SELECT_PLUGIN "select-plugin"
  3812. +#define PROP_RELOAD_PLUGIN "reload"
  3813. +#define PROP_BUFFER_SIZE "buffer-size"
  3814. +#define PROP_SHOW_GUI "show-gui"
  3815. +
  3816. +#define PROP_CHUNK "chunk"
  3817. +#define PROP_CUSTOM_DATA "customdata"
  3818. +
  3819. +// ----------------------------------------------------------------------------
  3820. +
  3821. +#ifdef __cplusplus
  3822. +extern "C" {
  3823. +#endif
  3824. +
  3825. +const char *get_carla_bin_path(void);
  3826. +
  3827. +void param_index_to_name(uint32_t index, char name[PARAM_NAME_SIZE]);
  3828. +void remove_all_props(obs_properties_t *props, obs_data_t *settings);
  3829. +
  3830. +void postpone_update_request(uint64_t *update_req);
  3831. +void handle_update_request(obs_source_t *source, uint64_t *update_req);
  3832. +
  3833. +#ifdef __cplusplus
  3834. +}
  3835. +#endif
  3836. +
  3837. +// ----------------------------------------------------------------------------
  3838. diff --git a/plugins/carla/pluginlistdialog.cpp b/plugins/carla/pluginlistdialog.cpp
  3839. new file mode 100644
  3840. index 000000000..9bb292d9c
  3841. --- /dev/null
  3842. +++ b/plugins/carla/pluginlistdialog.cpp
  3843. @@ -0,0 +1,1660 @@
  3844. +/*
  3845. + * Carla plugin host, adjusted for OBS
  3846. + * Copyright (C) 2011-2023 Filipe Coelho <falktx@falktx.com>
  3847. + * SPDX-License-Identifier: GPL-2.0-or-later
  3848. + */
  3849. +
  3850. +#include <CarlaBackendUtils.hpp>
  3851. +#include <CarlaString.hpp>
  3852. +#include <CarlaUtils.h>
  3853. +
  3854. +#include <QtCore/QDir>
  3855. +#include <QtCore/QFileInfo>
  3856. +#include <QtCore/QPointer>
  3857. +
  3858. +#include "pluginlistdialog.hpp"
  3859. +#include "pluginrefreshdialog.hpp"
  3860. +
  3861. +#include "common.h"
  3862. +#include "qtutils.h"
  3863. +
  3864. +CARLA_BACKEND_USE_NAMESPACE
  3865. +
  3866. +// ----------------------------------------------------------------------------
  3867. +// check if the plugin IO makes sense for OBS
  3868. +
  3869. +template<class T> static bool isSupportedIO(const T &info)
  3870. +{
  3871. + return info.cvIns == 0 && info.cvOuts == 0 &&
  3872. + info.audioIns <= MAX_AV_PLANES &&
  3873. + info.audioOuts <= MAX_AV_PLANES;
  3874. +}
  3875. +
  3876. +// ----------------------------------------------------------------------------
  3877. +// getenv with a fallback value if unset
  3878. +
  3879. +static inline const char *getEnvWithFallback(const char *const env,
  3880. + const char *const fallback)
  3881. +{
  3882. + if (const char *const value = std::getenv(env))
  3883. + return value;
  3884. +
  3885. + return fallback;
  3886. +}
  3887. +
  3888. +// ----------------------------------------------------------------------------
  3889. +// Plugin paths (from env vars first, then default locations)
  3890. +
  3891. +struct PluginPaths {
  3892. + QUtf8String ladspa;
  3893. + QUtf8String lv2;
  3894. + QUtf8String vst2;
  3895. + QUtf8String vst3;
  3896. + QUtf8String clap;
  3897. + QUtf8String jsfx;
  3898. +
  3899. + PluginPaths()
  3900. + {
  3901. + // get common env vars
  3902. + const QString HOME = QDir::toNativeSeparators(QDir::homePath());
  3903. +
  3904. +#if defined(CARLA_OS_WIN)
  3905. + const char *const envAPPDATA = std::getenv("APPDATA");
  3906. + const char *const envLOCALAPPDATA =
  3907. + getEnvWithFallback("LOCALAPPDATA", envAPPDATA);
  3908. + const char *const envPROGRAMFILES = std::getenv("PROGRAMFILES");
  3909. + const char *const envCOMMONPROGRAMFILES =
  3910. + std::getenv("COMMONPROGRAMFILES");
  3911. +
  3912. + // Small integrity tests
  3913. + if (envAPPDATA == nullptr) {
  3914. + qFatal("APPDATA variable not set, cannot continue");
  3915. + abort();
  3916. + }
  3917. +
  3918. + if (envPROGRAMFILES == nullptr) {
  3919. + qFatal("PROGRAMFILES variable not set, cannot continue");
  3920. + abort();
  3921. + }
  3922. +
  3923. + if (envCOMMONPROGRAMFILES == nullptr) {
  3924. + qFatal("COMMONPROGRAMFILES variable not set, cannot continue");
  3925. + abort();
  3926. + }
  3927. +
  3928. + const QUtf8String APPDATA(envAPPDATA);
  3929. + const QUtf8String LOCALAPPDATA(envLOCALAPPDATA);
  3930. + const QUtf8String PROGRAMFILES(envPROGRAMFILES);
  3931. + const QUtf8String COMMONPROGRAMFILES(envCOMMONPROGRAMFILES);
  3932. +#elif !defined(CARLA_OS_MAC)
  3933. + const QUtf8String CONFIG_HOME(getEnvWithFallback(
  3934. + "XDG_CONFIG_HOME", (HOME + "/.config").toUtf8()));
  3935. +#endif
  3936. +
  3937. + // now set paths, listing format path spec if available
  3938. + if (const char *const envLADSPA = std::getenv("LADSPA_PATH")) {
  3939. + ladspa = envLADSPA;
  3940. + } else {
  3941. + // no official spec, use common paths
  3942. +#if defined(CARLA_OS_WIN)
  3943. + ladspa = APPDATA + "\\LADSPA";
  3944. + ladspa += ";" + PROGRAMFILES + "\\LADSPA";
  3945. +#elif defined(CARLA_OS_MAC)
  3946. + ladspa = HOME + "/Library/Audio/Plug-Ins/LADSPA";
  3947. + ladspa += ":/Library/Audio/Plug-Ins/LADSPA";
  3948. +#else
  3949. + ladspa = HOME + "/.ladspa";
  3950. + ladspa += ":/usr/local/lib/ladspa";
  3951. + ladspa += ":/usr/lib/ladspa";
  3952. +#endif
  3953. + }
  3954. +
  3955. + if (const char *const envLV2 = std::getenv("LV2_PATH")) {
  3956. + lv2 = envLV2;
  3957. + } else {
  3958. + // https://lv2plug.in/pages/filesystem-hierarchy-standard.html
  3959. +#if defined(CARLA_OS_WIN)
  3960. + lv2 = APPDATA + "\\LV2";
  3961. + lv2 += ";" + COMMONPROGRAMFILES + "\\LV2";
  3962. +#elif defined(CARLA_OS_MAC)
  3963. + lv2 = HOME + "/Library/Audio/Plug-Ins/LV2";
  3964. + lv2 += ":/Library/Audio/Plug-Ins/LV2";
  3965. +#else
  3966. + lv2 = HOME + "/.lv2";
  3967. + lv2 += ":/usr/local/lib/lv2";
  3968. + lv2 += ":/usr/lib/lv2";
  3969. +#endif
  3970. + }
  3971. +
  3972. + if (const char *const envVST2 = std::getenv("VST_PATH")) {
  3973. + vst2 = envVST2;
  3974. + } else {
  3975. +#if defined(CARLA_OS_WIN)
  3976. + // https://helpcenter.steinberg.de/hc/en-us/articles/115000177084
  3977. + vst2 = PROGRAMFILES + "\\VSTPlugins";
  3978. + vst2 += ";" + PROGRAMFILES + "\\Steinberg\\VSTPlugins";
  3979. + vst2 += ";" + COMMONPROGRAMFILES + "\\VST2";
  3980. + vst2 += ";" + COMMONPROGRAMFILES + "\\Steinberg\\VST2";
  3981. +#elif defined(CARLA_OS_MAC)
  3982. + // https://helpcenter.steinberg.de/hc/en-us/articles/115000171310
  3983. + vst2 = HOME + "/Library/Audio/Plug-Ins/VST";
  3984. + vst2 += ":/Library/Audio/Plug-Ins/VST";
  3985. +#else
  3986. + // no official spec, use common paths
  3987. + vst2 = HOME + "/.vst";
  3988. + vst2 += ":" + HOME + "/.lxvst";
  3989. + vst2 += ":/usr/local/lib/vst";
  3990. + vst2 += ":/usr/local/lib/lxvst";
  3991. + vst2 += ":/usr/lib/vst";
  3992. + vst2 += ":/usr/lib/lxvst";
  3993. +#endif
  3994. + }
  3995. +
  3996. + if (const char *const envVST3 = std::getenv("VST3_PATH")) {
  3997. + vst3 = envVST3;
  3998. + } else {
  3999. + // https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html
  4000. +#if defined(CARLA_OS_WIN)
  4001. + vst3 = LOCALAPPDATA + "\\Programs\\Common\\VST3";
  4002. + vst3 += ";" + COMMONPROGRAMFILES + "\\VST3";
  4003. +#elif defined(CARLA_OS_MAC)
  4004. + vst3 = HOME + "/Library/Audio/Plug-Ins/VST3";
  4005. + vst3 += ":/Library/Audio/Plug-Ins/VST3";
  4006. +#else
  4007. + vst3 = HOME + "/.vst3";
  4008. + vst3 += ":/usr/local/lib/vst3";
  4009. + vst3 += ":/usr/lib/vst3";
  4010. +#endif
  4011. + }
  4012. +
  4013. + if (const char *const envCLAP = std::getenv("CLAP_PATH")) {
  4014. + clap = envCLAP;
  4015. + } else {
  4016. + // https://github.com/free-audio/clap/blob/main/include/clap/entry.h
  4017. +#if defined(CARLA_OS_WIN)
  4018. + clap = LOCALAPPDATA + "\\Programs\\Common\\CLAP";
  4019. + clap += ";" + COMMONPROGRAMFILES + "\\CLAP";
  4020. +#elif defined(CARLA_OS_MAC)
  4021. + clap = HOME + "/Library/Audio/Plug-Ins/CLAP";
  4022. + clap += ":/Library/Audio/Plug-Ins/CLAP";
  4023. +#else
  4024. + clap = HOME + "/.clap";
  4025. + clap += ":/usr/local/lib/clap";
  4026. + clap += ":/usr/lib/clap";
  4027. +#endif
  4028. + }
  4029. +
  4030. + if (const char *const envJSFX = std::getenv("JSFX_PATH")) {
  4031. + jsfx = envJSFX;
  4032. + } else {
  4033. + // REAPER user data directory
  4034. +#if defined(CARLA_OS_WIN)
  4035. + jsfx = APPDATA + "\\REAPER\\Effects";
  4036. +#elif defined(CARLA_OS_MAC)
  4037. + jsfx = HOME +
  4038. + "/Library/Application Support/REAPER/Effects";
  4039. +#else
  4040. + jsfx = CONFIG_HOME + "/REAPER/Effects";
  4041. +#endif
  4042. + }
  4043. + }
  4044. +};
  4045. +
  4046. +// ----------------------------------------------------------------------------
  4047. +// Qt-compatible plugin info
  4048. +
  4049. +// base details, nicely packed and POD-only so we can directly use as binary
  4050. +struct PluginInfoHeader {
  4051. + uint16_t build;
  4052. + uint16_t type;
  4053. + uint32_t hints;
  4054. + uint64_t uniqueId;
  4055. + uint16_t audioIns;
  4056. + uint16_t audioOuts;
  4057. + uint16_t cvIns;
  4058. + uint16_t cvOuts;
  4059. + uint16_t midiIns;
  4060. + uint16_t midiOuts;
  4061. + uint16_t parameterIns;
  4062. + uint16_t parameterOuts;
  4063. +};
  4064. +
  4065. +// full details, now with non-POD types
  4066. +struct PluginInfo : PluginInfoHeader {
  4067. + QString category;
  4068. + QString filename;
  4069. + QString name;
  4070. + QString label;
  4071. + QString maker;
  4072. +};
  4073. +
  4074. +// convert PluginInfo to Qt types
  4075. +static QVariant asByteArray(const PluginInfo &info)
  4076. +{
  4077. + QByteArray qdata;
  4078. +
  4079. + // start with the POD data, stored as-is
  4080. + qdata.append(
  4081. + static_cast<const char *>(static_cast<const void *>(&info)),
  4082. + sizeof(PluginInfoHeader));
  4083. +
  4084. + // then all the strings, with a null terminating byte
  4085. + {
  4086. + const QByteArray qcategory(info.category.toUtf8());
  4087. + qdata += qcategory.constData();
  4088. + qdata += '\0';
  4089. + }
  4090. +
  4091. + {
  4092. + const QByteArray qfilename(info.filename.toUtf8());
  4093. + qdata += qfilename.constData();
  4094. + qdata += '\0';
  4095. + }
  4096. +
  4097. + {
  4098. + const QByteArray qname(info.name.toUtf8());
  4099. + qdata += qname.constData();
  4100. + qdata += '\0';
  4101. + }
  4102. +
  4103. + {
  4104. + const QByteArray qlabel(info.label.toUtf8());
  4105. + qdata += qlabel.constData();
  4106. + qdata += '\0';
  4107. + }
  4108. +
  4109. + {
  4110. + const QByteArray qmaker(info.maker.toUtf8());
  4111. + qdata += qmaker.constData();
  4112. + qdata += '\0';
  4113. + }
  4114. +
  4115. + return qdata;
  4116. +}
  4117. +
  4118. +static QVariant asVariant(const PluginInfo &info)
  4119. +{
  4120. + return QVariant(asByteArray(info));
  4121. +}
  4122. +
  4123. +// convert Qt types to PluginInfo
  4124. +static PluginInfo asPluginInfo(const QByteArray &qdata)
  4125. +{
  4126. + // make sure data is big enough to fit POD data + 5 strings
  4127. + CARLA_SAFE_ASSERT_RETURN(static_cast<size_t>(qdata.size()) >=
  4128. + sizeof(PluginInfoHeader) +
  4129. + sizeof(char) * 5,
  4130. + {});
  4131. +
  4132. + // read POD data first
  4133. + const PluginInfoHeader *const data =
  4134. + static_cast<const PluginInfoHeader *>(
  4135. + static_cast<const void *>(qdata.constData()));
  4136. + PluginInfo info = {data->build,
  4137. + data->type,
  4138. + data->hints,
  4139. + data->uniqueId,
  4140. + data->audioIns,
  4141. + data->audioOuts,
  4142. + data->cvIns,
  4143. + data->cvOuts,
  4144. + data->midiIns,
  4145. + data->midiOuts,
  4146. + data->parameterIns,
  4147. + data->parameterOuts,
  4148. + {},
  4149. + {},
  4150. + {},
  4151. + {},
  4152. + {}};
  4153. +
  4154. + // then all the strings, keeping the same order as in `asVariant`
  4155. + const char *sdata =
  4156. + static_cast<const char *>(static_cast<const void *>(data + 1));
  4157. +
  4158. + info.category = QString::fromUtf8(sdata);
  4159. + sdata += info.category.size() + 1;
  4160. +
  4161. + info.filename = QString::fromUtf8(sdata);
  4162. + sdata += info.filename.size() + 1;
  4163. +
  4164. + info.name = QString::fromUtf8(sdata);
  4165. + sdata += info.name.size() + 1;
  4166. +
  4167. + info.label = QString::fromUtf8(sdata);
  4168. + sdata += info.label.size() + 1;
  4169. +
  4170. + info.maker = QString::fromUtf8(sdata);
  4171. + sdata += info.maker.size() + 1;
  4172. +
  4173. + return info;
  4174. +}
  4175. +
  4176. +static PluginInfo asPluginInfo(const QVariant &var)
  4177. +{
  4178. + return asPluginInfo(var.toByteArray());
  4179. +}
  4180. +
  4181. +static QList<PluginInfo> asPluginInfoList(const QVariant &var)
  4182. +{
  4183. + QCompatByteArray qdata(var.toByteArray());
  4184. +
  4185. + QList<PluginInfo> plist;
  4186. +
  4187. + while (!qdata.isEmpty()) {
  4188. + const PluginInfo info = asPluginInfo(qdata);
  4189. + CARLA_SAFE_ASSERT_RETURN(info.build != BINARY_NONE, {});
  4190. +
  4191. + plist.append(info);
  4192. + qdata = qdata.sliced(sizeof(PluginInfoHeader) +
  4193. + info.category.size() +
  4194. + info.filename.size() + info.name.size() +
  4195. + info.label.size() + info.maker.size() + 5);
  4196. + }
  4197. +
  4198. + return plist;
  4199. +}
  4200. +
  4201. +#ifndef CARLA_2_6_FEATURES
  4202. +// convert cached plugin stuff to PluginInfo
  4203. +static PluginInfo asPluginInfo(const CarlaCachedPluginInfo *const desc,
  4204. + const PluginType ptype)
  4205. +{
  4206. + PluginInfo pinfo = {};
  4207. + pinfo.build = BINARY_NATIVE;
  4208. + pinfo.type = ptype;
  4209. + pinfo.hints = desc->hints;
  4210. + pinfo.name = desc->name;
  4211. + pinfo.label = desc->label;
  4212. + pinfo.maker = desc->maker;
  4213. + pinfo.category = getPluginCategoryAsString(desc->category);
  4214. +
  4215. + pinfo.audioIns = desc->audioIns;
  4216. + pinfo.audioOuts = desc->audioOuts;
  4217. +
  4218. + pinfo.cvIns = desc->cvIns;
  4219. + pinfo.cvOuts = desc->cvOuts;
  4220. +
  4221. + pinfo.midiIns = desc->midiIns;
  4222. + pinfo.midiOuts = desc->midiOuts;
  4223. +
  4224. + pinfo.parameterIns = desc->parameterIns;
  4225. + pinfo.parameterOuts = desc->parameterOuts;
  4226. +
  4227. + if (ptype == PLUGIN_LV2) {
  4228. + const QString label(desc->label);
  4229. + pinfo.filename = label.split(CARLA_OS_SEP).first();
  4230. + pinfo.label = label.section(CARLA_OS_SEP, 1);
  4231. + }
  4232. +
  4233. + return pinfo;
  4234. +}
  4235. +#endif
  4236. +
  4237. +// ----------------------------------------------------------------------------
  4238. +// Qt-compatible plugin favorite
  4239. +
  4240. +// base details, nicely packed and POD-only so we can directly use as binary
  4241. +struct PluginFavoriteHeader {
  4242. + uint16_t type;
  4243. + uint64_t uniqueId;
  4244. +};
  4245. +
  4246. +// full details, now with non-POD types
  4247. +struct PluginFavorite : PluginFavoriteHeader {
  4248. + QString filename;
  4249. + QString label;
  4250. +
  4251. + bool operator==(const PluginFavorite &other) const
  4252. + {
  4253. + return type == other.type && uniqueId == other.uniqueId &&
  4254. + filename == other.filename && label == other.label;
  4255. + }
  4256. +};
  4257. +
  4258. +// convert PluginFavorite to Qt types
  4259. +static QByteArray asByteArray(const PluginFavorite &fav)
  4260. +{
  4261. + QByteArray qdata;
  4262. +
  4263. + // start with the POD data, stored as-is
  4264. + qdata.append(static_cast<const char *>(static_cast<const void *>(&fav)),
  4265. + sizeof(PluginFavoriteHeader));
  4266. +
  4267. + // then all the strings, with a null terminating byte
  4268. + {
  4269. + const QByteArray qfilename(fav.filename.toUtf8());
  4270. + qdata += qfilename.constData();
  4271. + qdata += '\0';
  4272. + }
  4273. +
  4274. + {
  4275. + const QByteArray qlabel(fav.label.toUtf8());
  4276. + qdata += qlabel.constData();
  4277. + qdata += '\0';
  4278. + }
  4279. +
  4280. + return qdata;
  4281. +}
  4282. +
  4283. +static QVariant asVariant(const QList<PluginFavorite> &favlist)
  4284. +{
  4285. + QByteArray qdata;
  4286. +
  4287. + for (const PluginFavorite &fav : favlist)
  4288. + qdata += asByteArray(fav);
  4289. +
  4290. + return QVariant(qdata);
  4291. +}
  4292. +
  4293. +// convert Qt types to PluginInfo
  4294. +static PluginFavorite asPluginFavorite(const QByteArray &qdata)
  4295. +{
  4296. + // make sure data is big enough to fit POD data + 3 strings
  4297. + CARLA_SAFE_ASSERT_RETURN(static_cast<size_t>(qdata.size()) >=
  4298. + sizeof(PluginFavoriteHeader) +
  4299. + sizeof(char) * 3,
  4300. + {});
  4301. +
  4302. + // read POD data first
  4303. + const PluginFavoriteHeader *const data =
  4304. + static_cast<const PluginFavoriteHeader *>(
  4305. + static_cast<const void *>(qdata.constData()));
  4306. + PluginFavorite fav = {data->type, data->uniqueId, {}, {}};
  4307. +
  4308. + // then all the strings, keeping the same order as in `asVariant`
  4309. + const char *sdata =
  4310. + static_cast<const char *>(static_cast<const void *>(data + 1));
  4311. +
  4312. + fav.filename = QString::fromUtf8(sdata);
  4313. + sdata += fav.filename.size() + 1;
  4314. +
  4315. + fav.label = QString::fromUtf8(sdata);
  4316. + sdata += fav.label.size() + 1;
  4317. +
  4318. + return fav;
  4319. +}
  4320. +
  4321. +static QList<PluginFavorite> asPluginFavoriteList(const QVariant &var)
  4322. +{
  4323. + QCompatByteArray qdata(var.toByteArray());
  4324. +
  4325. + QList<PluginFavorite> favlist;
  4326. +
  4327. + while (!qdata.isEmpty()) {
  4328. + const PluginFavorite fav = asPluginFavorite(qdata);
  4329. + CARLA_SAFE_ASSERT_RETURN(fav.type != PLUGIN_NONE, {});
  4330. +
  4331. + favlist.append(fav);
  4332. + qdata = qdata.sliced(sizeof(PluginFavoriteHeader) +
  4333. + fav.filename.size() + fav.label.size() +
  4334. + 2);
  4335. + }
  4336. +
  4337. + return favlist;
  4338. +}
  4339. +
  4340. +// create PluginFavorite from PluginInfo data
  4341. +static PluginFavorite asPluginFavorite(const PluginInfo &info)
  4342. +{
  4343. + return PluginFavorite{info.type, info.uniqueId, info.filename,
  4344. + info.label};
  4345. +}
  4346. +
  4347. +#ifdef CARLA_2_6_FEATURES
  4348. +// ----------------------------------------------------------------------------
  4349. +// discovery callbacks
  4350. +
  4351. +static void discoveryCallback(void *const ptr,
  4352. + const CarlaPluginDiscoveryInfo *const info,
  4353. + const char *const sha1sum)
  4354. +{
  4355. + static_cast<PluginListDialog *>(ptr)->addPluginInfo(info, sha1sum);
  4356. +}
  4357. +
  4358. +static bool checkCacheCallback(void *const ptr, const char *const filename,
  4359. + const char *const sha1sum)
  4360. +{
  4361. + if (sha1sum == nullptr)
  4362. + return false;
  4363. +
  4364. + return static_cast<PluginListDialog *>(ptr)->checkPluginCache(filename,
  4365. + sha1sum);
  4366. +}
  4367. +#endif // CARLA_2_6_FEATURES
  4368. +
  4369. +// ----------------------------------------------------------------------------
  4370. +
  4371. +struct PluginListDialog::PrivateData {
  4372. + int lastTableWidgetIndex = 0;
  4373. + int timerId = 0;
  4374. + PluginInfo retPlugin;
  4375. +
  4376. + struct Discovery {
  4377. + PluginType ptype = PLUGIN_NONE;
  4378. + bool firstInit = true;
  4379. +#ifdef CARLA_2_6_FEATURES
  4380. + bool ignoreCache = false;
  4381. + bool checkInvalid = false;
  4382. + CarlaPluginDiscoveryHandle handle = nullptr;
  4383. + QUtf8String tool;
  4384. + QPointer<PluginRefreshDialog> dialog;
  4385. + Discovery()
  4386. + {
  4387. + tool = get_carla_bin_path();
  4388. + tool += CARLA_OS_SEP_STR "carla-discovery-native";
  4389. +#ifdef CARLA_OS_WIN
  4390. + tool += ".exe";
  4391. +#endif
  4392. + }
  4393. +
  4394. + ~Discovery()
  4395. + {
  4396. + if (handle != nullptr)
  4397. + carla_plugin_discovery_stop(handle);
  4398. + }
  4399. +#endif
  4400. + } discovery;
  4401. +
  4402. + PluginPaths paths;
  4403. +
  4404. + struct {
  4405. + std::vector<PluginInfo> internal;
  4406. + std::vector<PluginInfo> lv2;
  4407. + std::vector<PluginInfo> jsfx;
  4408. +#ifdef CARLA_2_6_FEATURES
  4409. + std::vector<PluginInfo> ladspa;
  4410. + std::vector<PluginInfo> vst2;
  4411. + std::vector<PluginInfo> vst3;
  4412. + std::vector<PluginInfo> clap;
  4413. + QMap<QString, QList<PluginInfo>> cache;
  4414. +#endif
  4415. + QList<PluginFavorite> favorites;
  4416. +
  4417. + bool add(const PluginInfo &pinfo)
  4418. + {
  4419. + switch (pinfo.type) {
  4420. + case PLUGIN_INTERNAL:
  4421. + internal.push_back(pinfo);
  4422. + return true;
  4423. + case PLUGIN_LV2:
  4424. + lv2.push_back(pinfo);
  4425. + return true;
  4426. + case PLUGIN_JSFX:
  4427. + jsfx.push_back(pinfo);
  4428. + return true;
  4429. +#ifdef CARLA_2_6_FEATURES
  4430. + case PLUGIN_LADSPA:
  4431. + ladspa.push_back(pinfo);
  4432. + return true;
  4433. + case PLUGIN_VST2:
  4434. + vst2.push_back(pinfo);
  4435. + return true;
  4436. + case PLUGIN_VST3:
  4437. + vst3.push_back(pinfo);
  4438. + return true;
  4439. + case PLUGIN_CLAP:
  4440. + clap.push_back(pinfo);
  4441. + return true;
  4442. +#endif
  4443. + default:
  4444. + return false;
  4445. + }
  4446. + }
  4447. + } plugins;
  4448. +};
  4449. +
  4450. +// ----------------------------------------------------------------------------
  4451. +
  4452. +PluginListDialog::PluginListDialog(QWidget *const parent)
  4453. + : QDialog(parent), p(new PrivateData)
  4454. +{
  4455. + ui.setupUi(this);
  4456. +
  4457. + // --------------------------------------------------------------------
  4458. + // Set-up GUI
  4459. +
  4460. + ui.b_load->setEnabled(false);
  4461. +
  4462. + // do not resize info frame so much
  4463. + const QLayout *const infoLayout = ui.frame_info->layout();
  4464. + const QMargins infoMargins = infoLayout->contentsMargins();
  4465. + ui.frame_info->setMinimumWidth(
  4466. + infoMargins.left() + infoMargins.right() +
  4467. + infoLayout->spacing() * 3 +
  4468. + ui.la_id->fontMetrics().horizontalAdvance(
  4469. + "Has Custom GUI: 9999999999"));
  4470. +
  4471. +#ifndef CARLA_2_6_FEATURES
  4472. + ui.ch_ladspa->hide();
  4473. + ui.ch_vst->hide();
  4474. + ui.ch_vst3->hide();
  4475. + ui.ch_clap->hide();
  4476. +#endif
  4477. +
  4478. + // start with no plugin selected
  4479. + checkPlugin(-1);
  4480. +
  4481. + // custom action that listens for Ctrl+F shortcut
  4482. + addAction(ui.act_focus_search);
  4483. +
  4484. + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  4485. +#ifdef CARLA_OS_MAC
  4486. + setWindowModality(Qt::WindowModal);
  4487. +#endif
  4488. +
  4489. + // --------------------------------------------------------------------
  4490. + // Load settings
  4491. +
  4492. + loadSettings();
  4493. +
  4494. + // --------------------------------------------------------------------
  4495. + // Set-up Icons
  4496. +
  4497. + ui.b_clear_filters->setProperty("themeID", "clearIconSmall");
  4498. + ui.b_refresh->setProperty("themeID", "refreshIconSmall");
  4499. +
  4500. + /* FIXME get a star/bookmark/favorite icon
  4501. + QTableWidgetItem* const hhi = ui.tableWidget->horizontalHeaderItem(TW_FAVORITE);
  4502. + hhi->setProperty("themeID", "starIconSmall");
  4503. + */
  4504. +
  4505. + // --------------------------------------------------------------------
  4506. + // Set-up connections
  4507. +
  4508. + QObject::connect(this, &QDialog::finished, this,
  4509. + &PluginListDialog::saveSettings);
  4510. + QObject::connect(ui.b_load, &QPushButton::clicked, this,
  4511. + &QDialog::accept);
  4512. + QObject::connect(ui.b_cancel, &QPushButton::clicked, this,
  4513. + &QDialog::reject);
  4514. +
  4515. + QObject::connect(ui.b_refresh, &QPushButton::clicked, this,
  4516. + &PluginListDialog::refreshPlugins);
  4517. + QObject::connect(ui.b_clear_filters, &QPushButton::clicked, this,
  4518. + &PluginListDialog::clearFilters);
  4519. + QObject::connect(ui.lineEdit, &QLineEdit::textChanged, this,
  4520. + &PluginListDialog::checkFilters);
  4521. + QObject::connect(ui.tableWidget, &QTableWidget::currentCellChanged,
  4522. + this, &PluginListDialog::checkPlugin);
  4523. + QObject::connect(ui.tableWidget, &QTableWidget::cellClicked, this,
  4524. + &PluginListDialog::cellClicked);
  4525. + QObject::connect(ui.tableWidget, &QTableWidget::cellDoubleClicked, this,
  4526. + &PluginListDialog::cellDoubleClicked);
  4527. +
  4528. + QObject::connect(ui.ch_internal, &QCheckBox::clicked, this,
  4529. + &PluginListDialog::checkFilters);
  4530. + QObject::connect(ui.ch_ladspa, &QCheckBox::clicked, this,
  4531. + &PluginListDialog::checkFilters);
  4532. + QObject::connect(ui.ch_lv2, &QCheckBox::clicked, this,
  4533. + &PluginListDialog::checkFilters);
  4534. + QObject::connect(ui.ch_vst, &QCheckBox::clicked, this,
  4535. + &PluginListDialog::checkFilters);
  4536. + QObject::connect(ui.ch_vst3, &QCheckBox::clicked, this,
  4537. + &PluginListDialog::checkFilters);
  4538. + QObject::connect(ui.ch_clap, &QCheckBox::clicked, this,
  4539. + &PluginListDialog::checkFilters);
  4540. + QObject::connect(ui.ch_jsfx, &QCheckBox::clicked, this,
  4541. + &PluginListDialog::checkFilters);
  4542. + QObject::connect(ui.ch_effects, &QCheckBox::clicked, this,
  4543. + &PluginListDialog::checkFilters);
  4544. + QObject::connect(ui.ch_instruments, &QCheckBox::clicked, this,
  4545. + &PluginListDialog::checkFilters);
  4546. + QObject::connect(ui.ch_midi, &QCheckBox::clicked, this,
  4547. + &PluginListDialog::checkFilters);
  4548. + QObject::connect(ui.ch_other, &QCheckBox::clicked, this,
  4549. + &PluginListDialog::checkFilters);
  4550. + QObject::connect(ui.ch_favorites, &QCheckBox::clicked, this,
  4551. + &PluginListDialog::checkFilters);
  4552. + QObject::connect(ui.ch_gui, &QCheckBox::clicked, this,
  4553. + &PluginListDialog::checkFilters);
  4554. + QObject::connect(ui.ch_stereo, &QCheckBox::clicked, this,
  4555. + &PluginListDialog::checkFilters);
  4556. + QObject::connect(ui.ch_cat_all, &QCheckBox::clicked, this,
  4557. + &PluginListDialog::checkFiltersCategoryAll);
  4558. + QObject::connect(ui.ch_cat_delay, &QCheckBox::clicked, this,
  4559. + &PluginListDialog::checkFiltersCategorySpecific);
  4560. + QObject::connect(ui.ch_cat_distortion, &QCheckBox::clicked, this,
  4561. + &PluginListDialog::checkFiltersCategorySpecific);
  4562. + QObject::connect(ui.ch_cat_dynamics, &QCheckBox::clicked, this,
  4563. + &PluginListDialog::checkFiltersCategorySpecific);
  4564. + QObject::connect(ui.ch_cat_eq, &QCheckBox::clicked, this,
  4565. + &PluginListDialog::checkFiltersCategorySpecific);
  4566. + QObject::connect(ui.ch_cat_filter, &QCheckBox::clicked, this,
  4567. + &PluginListDialog::checkFiltersCategorySpecific);
  4568. + QObject::connect(ui.ch_cat_modulator, &QCheckBox::clicked, this,
  4569. + &PluginListDialog::checkFiltersCategorySpecific);
  4570. + QObject::connect(ui.ch_cat_synth, &QCheckBox::clicked, this,
  4571. + &PluginListDialog::checkFiltersCategorySpecific);
  4572. + QObject::connect(ui.ch_cat_utility, &QCheckBox::clicked, this,
  4573. + &PluginListDialog::checkFiltersCategorySpecific);
  4574. + QObject::connect(ui.ch_cat_other, &QCheckBox::clicked, this,
  4575. + &PluginListDialog::checkFiltersCategorySpecific);
  4576. +
  4577. + QObject::connect(ui.act_focus_search, &QAction::triggered, this,
  4578. + &PluginListDialog::focusSearchFieldAndSelectAll);
  4579. +}
  4580. +
  4581. +PluginListDialog::~PluginListDialog()
  4582. +{
  4583. + if (p->timerId != 0)
  4584. + killTimer(p->timerId);
  4585. +
  4586. + delete p;
  4587. +}
  4588. +
  4589. +// ----------------------------------------------------------------------------
  4590. +// public methods
  4591. +
  4592. +const PluginInfo &PluginListDialog::getSelectedPluginInfo() const
  4593. +{
  4594. + return p->retPlugin;
  4595. +}
  4596. +
  4597. +#ifdef CARLA_2_6_FEATURES
  4598. +void PluginListDialog::addPluginInfo(const CarlaPluginDiscoveryInfo *const info,
  4599. + const char *const sha1sum)
  4600. +{
  4601. + if (info == nullptr) {
  4602. + if (sha1sum != nullptr) {
  4603. + QSafeSettings settings;
  4604. + settings.setValue(
  4605. + QString("PluginCache/%1").arg(sha1sum),
  4606. + QByteArray());
  4607. +
  4608. + p->plugins.cache[QString(sha1sum)] = {};
  4609. + }
  4610. + return;
  4611. + }
  4612. +
  4613. + const PluginInfo pinfo = {
  4614. + static_cast<uint16_t>(info->btype),
  4615. + static_cast<uint16_t>(info->ptype),
  4616. + info->metadata.hints,
  4617. + info->uniqueId,
  4618. + static_cast<uint16_t>(info->io.audioIns),
  4619. + static_cast<uint16_t>(info->io.audioOuts),
  4620. + static_cast<uint16_t>(info->io.cvIns),
  4621. + static_cast<uint16_t>(info->io.cvOuts),
  4622. + static_cast<uint16_t>(info->io.midiIns),
  4623. + static_cast<uint16_t>(info->io.midiOuts),
  4624. + static_cast<uint16_t>(info->io.parameterIns),
  4625. + static_cast<uint16_t>(info->io.parameterOuts),
  4626. + getPluginCategoryAsString(info->metadata.category),
  4627. + QString::fromUtf8(info->filename),
  4628. + QString::fromUtf8(info->metadata.name),
  4629. + QString::fromUtf8(info->label),
  4630. + QString::fromUtf8(info->metadata.maker),
  4631. + };
  4632. +
  4633. + if (sha1sum != nullptr) {
  4634. + QSafeSettings settings;
  4635. + const QString qsha1sum(sha1sum);
  4636. + const QString key = QString("PluginCache/%1").arg(sha1sum);
  4637. +
  4638. + // single sha1sum can contain >1 plugin
  4639. + QByteArray qdata;
  4640. + if (p->plugins.cache.contains(qsha1sum))
  4641. + qdata = settings.valueByteArray(key);
  4642. + qdata += asVariant(pinfo).toByteArray();
  4643. +
  4644. + settings.setValue(key, qdata);
  4645. +
  4646. + p->plugins.cache[qsha1sum].append(pinfo);
  4647. + }
  4648. +
  4649. + if (isSupportedIO(pinfo))
  4650. + p->plugins.add(pinfo);
  4651. +}
  4652. +
  4653. +bool PluginListDialog::checkPluginCache(const char *const filename,
  4654. + const char *const sha1sum)
  4655. +{
  4656. + // sha1sum is always valid for this call
  4657. + const QString qsha1sum(sha1sum);
  4658. +
  4659. + if (filename != nullptr)
  4660. + p->discovery.dialog->progressBar->setFormat(filename);
  4661. +
  4662. + if (!p->plugins.cache.contains(qsha1sum))
  4663. + return false;
  4664. +
  4665. + const QList<PluginInfo> &plist(p->plugins.cache[qsha1sum]);
  4666. +
  4667. + if (plist.isEmpty())
  4668. + return p->discovery.ignoreCache || !p->discovery.checkInvalid;
  4669. +
  4670. + // if filename does not match, abort (hash collision?)
  4671. + if (filename == nullptr || plist.first().filename != filename) {
  4672. + p->plugins.cache.remove(qsha1sum);
  4673. + return false;
  4674. + }
  4675. +
  4676. + for (const PluginInfo &info : plist) {
  4677. + if (isSupportedIO(info))
  4678. + p->plugins.add(info);
  4679. + }
  4680. +
  4681. + return true;
  4682. +}
  4683. +#endif
  4684. +
  4685. +// ----------------------------------------------------------------------------
  4686. +// protected methods
  4687. +
  4688. +void PluginListDialog::done(const int r)
  4689. +{
  4690. + if (r == QDialog::Accepted && ui.tableWidget->currentRow() >= 0) {
  4691. + p->retPlugin = asPluginInfo(
  4692. + ui.tableWidget
  4693. + ->item(ui.tableWidget->currentRow(), TW_NAME)
  4694. + ->data(Qt::UserRole + UR_PLUGIN_INFO));
  4695. + } else {
  4696. + p->retPlugin = {};
  4697. + }
  4698. +
  4699. + QDialog::done(r);
  4700. +}
  4701. +
  4702. +void PluginListDialog::showEvent(QShowEvent *const event)
  4703. +{
  4704. + focusSearchFieldAndSelectAll();
  4705. + QDialog::showEvent(event);
  4706. +
  4707. + // Set up initial discovery
  4708. + if (p->discovery.firstInit) {
  4709. + p->discovery.firstInit = false;
  4710. +
  4711. +#ifdef CARLA_2_6_FEATURES
  4712. + p->discovery.dialog = new PluginRefreshDialog(this);
  4713. + p->discovery.dialog->b_start->setEnabled(false);
  4714. + p->discovery.dialog->b_skip->setEnabled(true);
  4715. + p->discovery.dialog->ch_updated->setChecked(true);
  4716. + p->discovery.dialog->ch_invalid->setChecked(false);
  4717. + p->discovery.dialog->group->setEnabled(false);
  4718. + p->discovery.dialog->progressBar->setFormat(
  4719. + "Starting initial discovery...");
  4720. + p->discovery.dialog->show();
  4721. +
  4722. + QObject::connect(p->discovery.dialog->b_skip,
  4723. + &QPushButton::clicked, this,
  4724. + &PluginListDialog::refreshPluginsSkip);
  4725. + QObject::connect(p->discovery.dialog, &QDialog::finished, this,
  4726. + &PluginListDialog::refreshPluginsStop);
  4727. +#endif
  4728. +
  4729. + p->timerId = startTimer(0);
  4730. + }
  4731. +}
  4732. +
  4733. +void PluginListDialog::timerEvent(QTimerEvent *const event)
  4734. +{
  4735. + if (event->timerId() == p->timerId) {
  4736. + do {
  4737. +#ifdef CARLA_2_6_FEATURES
  4738. + // discovery in progress, keep it going
  4739. + if (p->discovery.handle != nullptr) {
  4740. + if (!carla_plugin_discovery_idle(
  4741. + p->discovery.handle)) {
  4742. + carla_plugin_discovery_stop(
  4743. + p->discovery.handle);
  4744. + p->discovery.handle = nullptr;
  4745. + }
  4746. + break;
  4747. + }
  4748. +#endif
  4749. + // start next discovery
  4750. + QUtf8String path;
  4751. + switch (p->discovery.ptype) {
  4752. + case PLUGIN_NONE:
  4753. + ui.label->setText(
  4754. + tr("Discovering internal plugins..."));
  4755. + p->discovery.ptype = PLUGIN_INTERNAL;
  4756. + break;
  4757. + case PLUGIN_INTERNAL:
  4758. + ui.label->setText(
  4759. + tr("Discovering LV2 plugins..."));
  4760. + path = p->paths.lv2;
  4761. + p->discovery.ptype = PLUGIN_LV2;
  4762. + break;
  4763. + case PLUGIN_LV2:
  4764. + if (p->paths.jsfx.isNotEmpty()) {
  4765. + ui.label->setText(tr(
  4766. + "Discovering JSFX plugins..."));
  4767. + path = p->paths.jsfx;
  4768. + p->discovery.ptype = PLUGIN_JSFX;
  4769. + break;
  4770. + }
  4771. + [[fallthrough]];
  4772. +#ifdef CARLA_2_6_FEATURES
  4773. + case PLUGIN_JSFX:
  4774. + ui.label->setText(
  4775. + tr("Discovering LADSPA plugins..."));
  4776. + path = p->paths.ladspa;
  4777. + p->discovery.ptype = PLUGIN_LADSPA;
  4778. + break;
  4779. + case PLUGIN_LADSPA:
  4780. + ui.label->setText(
  4781. + tr("Discovering VST2 plugins..."));
  4782. + path = p->paths.vst2;
  4783. + p->discovery.ptype = PLUGIN_VST2;
  4784. + break;
  4785. + case PLUGIN_VST2:
  4786. + ui.label->setText(
  4787. + tr("Discovering VST3 plugins..."));
  4788. + path = p->paths.vst3;
  4789. + p->discovery.ptype = PLUGIN_VST3;
  4790. + break;
  4791. + case PLUGIN_VST3:
  4792. + ui.label->setText(
  4793. + tr("Discovering CLAP plugins..."));
  4794. + path = p->paths.clap;
  4795. + p->discovery.ptype = PLUGIN_CLAP;
  4796. + break;
  4797. +#endif
  4798. + default:
  4799. + // discovery complete
  4800. + refreshPluginsStop();
  4801. + }
  4802. +
  4803. + if (p->timerId == 0)
  4804. + break;
  4805. +
  4806. +#ifdef CARLA_2_6_FEATURES
  4807. + p->discovery.handle = carla_plugin_discovery_start(
  4808. + p->discovery.tool.toUtf8().constData(),
  4809. + p->discovery.ptype, path.toUtf8().constData(),
  4810. + discoveryCallback, checkCacheCallback, this);
  4811. +#else
  4812. + if (const uint count = carla_get_cached_plugin_count(
  4813. + p->discovery.ptype,
  4814. + path.toUtf8().constData())) {
  4815. + for (uint i = 0; i < count; ++i) {
  4816. + const CarlaCachedPluginInfo *const info =
  4817. + carla_get_cached_plugin_info(
  4818. + p->discovery.ptype, i);
  4819. +
  4820. + if (!info || !info->valid)
  4821. + continue;
  4822. +
  4823. + // ignore plugins with non-compatible IO
  4824. + if (isSupportedIO(*info))
  4825. + p->plugins.add(asPluginInfo(
  4826. + info,
  4827. + p->discovery.ptype));
  4828. + }
  4829. + }
  4830. +#endif
  4831. + } while (false);
  4832. + }
  4833. +
  4834. + QDialog::timerEvent(event);
  4835. +}
  4836. +
  4837. +// ----------------------------------------------------------------------------
  4838. +// private methods
  4839. +
  4840. +void PluginListDialog::addPluginsToTable()
  4841. +{
  4842. + // --------------------------------------------------------------------
  4843. + // sum plugins first, creating all needed rows in advance
  4844. +
  4845. + ui.tableWidget->setSortingEnabled(false);
  4846. + ui.tableWidget->clearContents();
  4847. +
  4848. +#ifdef CARLA_2_6_FEATURES
  4849. + ui.tableWidget->setRowCount(
  4850. + int(p->plugins.internal.size() + p->plugins.ladspa.size() +
  4851. + p->plugins.lv2.size() + p->plugins.vst2.size() +
  4852. + p->plugins.vst3.size() + p->plugins.clap.size() +
  4853. + p->plugins.jsfx.size()));
  4854. +
  4855. + ui.label->setText(
  4856. + tr("Have %1 Internal, %2 LADSPA, %3 LV2, %4 VST2, %5 VST3, %6 CLAP and %7 JSFX plugins")
  4857. + .arg(QString::number(p->plugins.internal.size()))
  4858. + .arg(QString::number(p->plugins.ladspa.size()))
  4859. + .arg(QString::number(p->plugins.lv2.size()))
  4860. + .arg(QString::number(p->plugins.vst2.size()))
  4861. + .arg(QString::number(p->plugins.vst3.size()))
  4862. + .arg(QString::number(p->plugins.clap.size()))
  4863. + .arg(QString::number(p->plugins.jsfx.size())));
  4864. +#else
  4865. + ui.tableWidget->setRowCount(int(p->plugins.internal.size() +
  4866. + p->plugins.lv2.size() +
  4867. + p->plugins.jsfx.size()));
  4868. +
  4869. + ui.label->setText(
  4870. + tr("Have %1 Internal, %2 LV2 and %3 JSFX plugins")
  4871. + .arg(QString::number(p->plugins.internal.size()))
  4872. + .arg(QString::number(p->plugins.lv2.size()))
  4873. + .arg(QString::number(p->plugins.jsfx.size())));
  4874. +#endif
  4875. +
  4876. + // --------------------------------------------------------------------
  4877. + // now add all plugins to the table
  4878. +
  4879. + auto addPluginToTable = [=](const PluginInfo &info) {
  4880. + const int index = p->lastTableWidgetIndex++;
  4881. + const bool isFav =
  4882. + p->plugins.favorites.contains(asPluginFavorite(info));
  4883. +
  4884. + QTableWidgetItem *const itemFav = new QTableWidgetItem;
  4885. + itemFav->setCheckState(isFav ? Qt::Checked : Qt::Unchecked);
  4886. + itemFav->setText(isFav ? " " : " ");
  4887. +
  4888. + const QString pluginText =
  4889. + (info.name + info.label + info.maker + info.filename)
  4890. + .toLower();
  4891. + ui.tableWidget->setItem(index, TW_FAVORITE, itemFav);
  4892. + ui.tableWidget->setItem(index, TW_NAME,
  4893. + new QTableWidgetItem(info.name));
  4894. + ui.tableWidget->setItem(index, TW_LABEL,
  4895. + new QTableWidgetItem(info.label));
  4896. + ui.tableWidget->setItem(index, TW_MAKER,
  4897. + new QTableWidgetItem(info.maker));
  4898. + ui.tableWidget->setItem(
  4899. + index, TW_BINARY,
  4900. + new QTableWidgetItem(
  4901. + QFileInfo(info.filename).fileName()));
  4902. +
  4903. + QTableWidgetItem *const itemName =
  4904. + ui.tableWidget->item(index, TW_NAME);
  4905. + itemName->setData(Qt::UserRole + UR_PLUGIN_INFO,
  4906. + asVariant(info));
  4907. + itemName->setData(Qt::UserRole + UR_SEARCH_TEXT, pluginText);
  4908. + };
  4909. +
  4910. + p->lastTableWidgetIndex = 0;
  4911. +
  4912. + for (const PluginInfo &plugin : p->plugins.internal)
  4913. + addPluginToTable(plugin);
  4914. +
  4915. + for (const PluginInfo &plugin : p->plugins.lv2)
  4916. + addPluginToTable(plugin);
  4917. +
  4918. + for (const PluginInfo &plugin : p->plugins.jsfx)
  4919. + addPluginToTable(plugin);
  4920. +
  4921. +#ifdef CARLA_2_6_FEATURES
  4922. + for (const PluginInfo &plugin : p->plugins.ladspa)
  4923. + addPluginToTable(plugin);
  4924. +
  4925. + for (const PluginInfo &plugin : p->plugins.vst2)
  4926. + addPluginToTable(plugin);
  4927. +
  4928. + for (const PluginInfo &plugin : p->plugins.vst3)
  4929. + addPluginToTable(plugin);
  4930. +
  4931. + for (const PluginInfo &plugin : p->plugins.clap)
  4932. + addPluginToTable(plugin);
  4933. +#endif
  4934. +
  4935. + CARLA_SAFE_ASSERT_INT2(
  4936. + p->lastTableWidgetIndex == ui.tableWidget->rowCount(),
  4937. + p->lastTableWidgetIndex, ui.tableWidget->rowCount());
  4938. +
  4939. + // --------------------------------------------------------------------
  4940. + // and reenable sorting + filtering
  4941. +
  4942. + ui.tableWidget->setSortingEnabled(true);
  4943. +
  4944. + checkFilters();
  4945. + checkPlugin(ui.tableWidget->currentRow());
  4946. +}
  4947. +
  4948. +void PluginListDialog::loadSettings()
  4949. +{
  4950. + const QSafeSettings settings;
  4951. +
  4952. + restoreGeometry(settings.valueByteArray("PluginListDialog/Geometry"));
  4953. + ui.ch_effects->setChecked(
  4954. + settings.valueBool("PluginListDialog/ShowEffects", true));
  4955. + ui.ch_instruments->setChecked(
  4956. + settings.valueBool("PluginListDialog/ShowInstruments", true));
  4957. + ui.ch_midi->setChecked(
  4958. + settings.valueBool("PluginListDialog/ShowMIDI", true));
  4959. + ui.ch_other->setChecked(
  4960. + settings.valueBool("PluginListDialog/ShowOther", true));
  4961. + ui.ch_internal->setChecked(
  4962. + settings.valueBool("PluginListDialog/ShowInternal", true));
  4963. + ui.ch_ladspa->setChecked(
  4964. + settings.valueBool("PluginListDialog/ShowLADSPA", true));
  4965. + ui.ch_lv2->setChecked(
  4966. + settings.valueBool("PluginListDialog/ShowLV2", true));
  4967. + ui.ch_vst->setChecked(
  4968. + settings.valueBool("PluginListDialog/ShowVST2", true));
  4969. + ui.ch_vst3->setChecked(
  4970. + settings.valueBool("PluginListDialog/ShowVST3", true));
  4971. + ui.ch_clap->setChecked(
  4972. + settings.valueBool("PluginListDialog/ShowCLAP", true));
  4973. + ui.ch_jsfx->setChecked(
  4974. + settings.valueBool("PluginListDialog/ShowJSFX", true));
  4975. + ui.ch_favorites->setChecked(
  4976. + settings.valueBool("PluginListDialog/ShowFavorites", false));
  4977. + ui.ch_gui->setChecked(
  4978. + settings.valueBool("PluginListDialog/ShowHasGUI", false));
  4979. + ui.ch_stereo->setChecked(
  4980. + settings.valueBool("PluginListDialog/ShowStereoOnly", false));
  4981. + ui.lineEdit->setText(
  4982. + settings.valueString("PluginListDialog/SearchText", ""));
  4983. +
  4984. + const QString categories =
  4985. + settings.valueString("PluginListDialog/ShowCategory", "all");
  4986. + if (categories == "all" or categories.length() < 2) {
  4987. + ui.ch_cat_all->setChecked(true);
  4988. + ui.ch_cat_delay->setChecked(false);
  4989. + ui.ch_cat_distortion->setChecked(false);
  4990. + ui.ch_cat_dynamics->setChecked(false);
  4991. + ui.ch_cat_eq->setChecked(false);
  4992. + ui.ch_cat_filter->setChecked(false);
  4993. + ui.ch_cat_modulator->setChecked(false);
  4994. + ui.ch_cat_synth->setChecked(false);
  4995. + ui.ch_cat_utility->setChecked(false);
  4996. + ui.ch_cat_other->setChecked(false);
  4997. + } else {
  4998. + ui.ch_cat_all->setChecked(false);
  4999. + ui.ch_cat_delay->setChecked(categories.contains(":delay:"));
  5000. + ui.ch_cat_distortion->setChecked(
  5001. + categories.contains(":distortion:"));
  5002. + ui.ch_cat_dynamics->setChecked(
  5003. + categories.contains(":dynamics:"));
  5004. + ui.ch_cat_eq->setChecked(categories.contains(":eq:"));
  5005. + ui.ch_cat_filter->setChecked(categories.contains(":filter:"));
  5006. + ui.ch_cat_modulator->setChecked(
  5007. + categories.contains(":modulator:"));
  5008. + ui.ch_cat_synth->setChecked(categories.contains(":synth:"));
  5009. + ui.ch_cat_utility->setChecked(categories.contains(":utility:"));
  5010. + ui.ch_cat_other->setChecked(categories.contains(":other:"));
  5011. + }
  5012. +
  5013. + const QByteArray tableGeometry =
  5014. + settings.valueByteArray("PluginListDialog/TableGeometry");
  5015. + QHeaderView *const horizontalHeader =
  5016. + ui.tableWidget->horizontalHeader();
  5017. + if (!tableGeometry.isNull()) {
  5018. + horizontalHeader->restoreState(tableGeometry);
  5019. + } else {
  5020. + ui.tableWidget->setColumnWidth(TW_NAME, 250);
  5021. + ui.tableWidget->setColumnWidth(TW_LABEL, 200);
  5022. + ui.tableWidget->setColumnWidth(TW_MAKER, 150);
  5023. + ui.tableWidget->sortByColumn(TW_NAME, Qt::AscendingOrder);
  5024. + }
  5025. +
  5026. + horizontalHeader->setSectionResizeMode(TW_FAVORITE, QHeaderView::Fixed);
  5027. + ui.tableWidget->setColumnWidth(TW_FAVORITE, 24);
  5028. + ui.tableWidget->setSortingEnabled(true);
  5029. +
  5030. + p->plugins.favorites = asPluginFavoriteList(
  5031. + settings.valueByteArray("PluginListDialog/Favorites"));
  5032. +
  5033. +#ifdef CARLA_2_6_FEATURES
  5034. + // load entire plugin cache
  5035. + const QStringList keys = settings.allKeys();
  5036. + for (const QUtf8String key : keys) {
  5037. + if (!key.startsWith("PluginCache/"))
  5038. + continue;
  5039. +
  5040. + const QByteArray data(settings.valueByteArray(key));
  5041. +
  5042. + if (data.isEmpty())
  5043. + p->plugins.cache.insert(key.sliced(12), {});
  5044. + else
  5045. + p->plugins.cache.insert(key.sliced(12),
  5046. + asPluginInfoList(data));
  5047. + }
  5048. +#endif
  5049. +}
  5050. +
  5051. +// ----------------------------------------------------------------------------
  5052. +// private slots
  5053. +
  5054. +void PluginListDialog::cellClicked(const int row, const int column)
  5055. +{
  5056. + if (column != TW_FAVORITE)
  5057. + return;
  5058. +
  5059. + const PluginInfo info =
  5060. + asPluginInfo(ui.tableWidget->item(row, TW_NAME)
  5061. + ->data(Qt::UserRole + UR_PLUGIN_INFO));
  5062. + const PluginFavorite fav = asPluginFavorite(info);
  5063. + const bool isFavorite = p->plugins.favorites.contains(fav);
  5064. +
  5065. + if (ui.tableWidget->item(row, TW_FAVORITE)->checkState() ==
  5066. + Qt::Checked) {
  5067. + if (!isFavorite)
  5068. + p->plugins.favorites.append(fav);
  5069. + } else if (isFavorite) {
  5070. + p->plugins.favorites.removeAll(fav);
  5071. + }
  5072. +
  5073. + QSafeSettings settings;
  5074. + settings.setValue("PluginListDialog/Favorites",
  5075. + asVariant(p->plugins.favorites));
  5076. +}
  5077. +
  5078. +void PluginListDialog::cellDoubleClicked(int, const int column)
  5079. +{
  5080. + if (column != TW_FAVORITE)
  5081. + done(QDialog::Accepted);
  5082. +}
  5083. +
  5084. +void PluginListDialog::focusSearchFieldAndSelectAll()
  5085. +{
  5086. + ui.lineEdit->setFocus();
  5087. + ui.lineEdit->selectAll();
  5088. +}
  5089. +
  5090. +void PluginListDialog::checkFilters()
  5091. +{
  5092. + const QUtf8String text = ui.lineEdit->text().toLower();
  5093. +
  5094. + const bool hideEffects = !ui.ch_effects->isChecked();
  5095. + const bool hideInstruments = !ui.ch_instruments->isChecked();
  5096. + const bool hideMidi = !ui.ch_midi->isChecked();
  5097. + const bool hideOther = !ui.ch_other->isChecked();
  5098. +
  5099. + const bool hideInternal = !ui.ch_internal->isChecked();
  5100. + const bool hideLV2 = !ui.ch_lv2->isChecked();
  5101. + const bool hideJSFX = !ui.ch_jsfx->isChecked();
  5102. +#ifdef CARLA_2_6_FEATURES
  5103. + const bool hideLadspa = !ui.ch_ladspa->isChecked();
  5104. + const bool hideVST2 = !ui.ch_vst->isChecked();
  5105. + const bool hideVST3 = !ui.ch_vst3->isChecked();
  5106. + const bool hideCLAP = !ui.ch_clap->isChecked();
  5107. +#endif
  5108. +
  5109. + const bool hideNonFavs = ui.ch_favorites->isChecked();
  5110. + const bool hideNonGui = ui.ch_gui->isChecked();
  5111. + const bool hideNonStereo = ui.ch_stereo->isChecked();
  5112. +
  5113. + for (int i = 0, c = ui.tableWidget->rowCount(); i < c; ++i) {
  5114. + const PluginInfo info = asPluginInfo(
  5115. + ui.tableWidget->item(i, TW_NAME)
  5116. + ->data(Qt::UserRole + UR_PLUGIN_INFO));
  5117. + const QString ptext =
  5118. + ui.tableWidget->item(i, TW_NAME)
  5119. + ->data(Qt::UserRole + UR_SEARCH_TEXT)
  5120. + .toString();
  5121. + const uint16_t ptype = info.type;
  5122. + const uint32_t phints = info.hints;
  5123. + const uint16_t aIns = info.audioIns;
  5124. + const uint16_t aOuts = info.audioOuts;
  5125. + const uint16_t mIns = info.midiIns;
  5126. + const uint16_t mOuts = info.midiOuts;
  5127. + const QString categ = info.category;
  5128. + const bool isSynth = phints & PLUGIN_IS_SYNTH;
  5129. + const bool isEffect = aIns > 0 && aOuts > 0 && !isSynth;
  5130. + const bool isMidi = aIns == 0 && aOuts == 0 && mIns > 0 &&
  5131. + mOuts > 0;
  5132. + const bool isOther = !(isEffect || isSynth || isMidi);
  5133. + const bool isStereo = (aIns == 2 && aOuts == 2) ||
  5134. + (isSynth && aOuts == 2);
  5135. + const bool hasGui = phints & PLUGIN_HAS_CUSTOM_UI;
  5136. +
  5137. + const auto hasText = [text, ptext]() {
  5138. + const QStringList textSplit = text.strip().split(' ');
  5139. + for (const QString &t : textSplit)
  5140. + if (ptext.contains(t))
  5141. + return true;
  5142. + return false;
  5143. + };
  5144. +
  5145. + /**/ if (hideEffects && isEffect)
  5146. + ui.tableWidget->hideRow(i);
  5147. + else if (hideInstruments && isSynth)
  5148. + ui.tableWidget->hideRow(i);
  5149. + else if (hideMidi && isMidi)
  5150. + ui.tableWidget->hideRow(i);
  5151. + else if (hideOther && isOther)
  5152. + ui.tableWidget->hideRow(i);
  5153. + else if (hideInternal && ptype == PLUGIN_INTERNAL)
  5154. + ui.tableWidget->hideRow(i);
  5155. + else if (hideLV2 && ptype == PLUGIN_LV2)
  5156. + ui.tableWidget->hideRow(i);
  5157. + else if (hideJSFX && ptype == PLUGIN_JSFX)
  5158. + ui.tableWidget->hideRow(i);
  5159. +#ifdef CARLA_2_6_FEATURES
  5160. + else if (hideLadspa && ptype == PLUGIN_LADSPA)
  5161. + ui.tableWidget->hideRow(i);
  5162. + else if (hideVST2 && ptype == PLUGIN_VST2)
  5163. + ui.tableWidget->hideRow(i);
  5164. + else if (hideVST3 && ptype == PLUGIN_VST3)
  5165. + ui.tableWidget->hideRow(i);
  5166. + else if (hideCLAP && ptype == PLUGIN_CLAP)
  5167. + ui.tableWidget->hideRow(i);
  5168. +#endif
  5169. + else if (hideNonGui && not hasGui)
  5170. + ui.tableWidget->hideRow(i);
  5171. + else if (hideNonStereo && not isStereo)
  5172. + ui.tableWidget->hideRow(i);
  5173. + else if (text.isNotEmpty() && !hasText())
  5174. + ui.tableWidget->hideRow(i);
  5175. + else if (hideNonFavs &&
  5176. + !p->plugins.favorites.contains(asPluginFavorite(info)))
  5177. + ui.tableWidget->hideRow(i);
  5178. + else if (ui.ch_cat_all->isChecked() or
  5179. + (ui.ch_cat_delay->isChecked() && categ == "delay") or
  5180. + (ui.ch_cat_distortion->isChecked() &&
  5181. + categ == "distortion") or
  5182. + (ui.ch_cat_dynamics->isChecked() &&
  5183. + categ == "dynamics") or
  5184. + (ui.ch_cat_eq->isChecked() && categ == "eq") or
  5185. + (ui.ch_cat_filter->isChecked() && categ == "filter") or
  5186. + (ui.ch_cat_modulator->isChecked() &&
  5187. + categ == "modulator") or
  5188. + (ui.ch_cat_synth->isChecked() && categ == "synth") or
  5189. + (ui.ch_cat_utility->isChecked() &&
  5190. + categ == "utility") or
  5191. + (ui.ch_cat_other->isChecked() && categ == "other"))
  5192. + ui.tableWidget->showRow(i);
  5193. + else
  5194. + ui.tableWidget->hideRow(i);
  5195. + }
  5196. +}
  5197. +
  5198. +void PluginListDialog::checkFiltersCategoryAll(const bool clicked)
  5199. +{
  5200. + const bool notClicked = !clicked;
  5201. + ui.ch_cat_delay->setChecked(notClicked);
  5202. + ui.ch_cat_distortion->setChecked(notClicked);
  5203. + ui.ch_cat_dynamics->setChecked(notClicked);
  5204. + ui.ch_cat_eq->setChecked(notClicked);
  5205. + ui.ch_cat_filter->setChecked(notClicked);
  5206. + ui.ch_cat_modulator->setChecked(notClicked);
  5207. + ui.ch_cat_synth->setChecked(notClicked);
  5208. + ui.ch_cat_utility->setChecked(notClicked);
  5209. + ui.ch_cat_other->setChecked(notClicked);
  5210. + checkFilters();
  5211. +}
  5212. +
  5213. +void PluginListDialog::checkFiltersCategorySpecific(bool clicked)
  5214. +{
  5215. + if (clicked) {
  5216. + ui.ch_cat_all->setChecked(false);
  5217. + } else if (!(ui.ch_cat_delay->isChecked() ||
  5218. + ui.ch_cat_distortion->isChecked() ||
  5219. + ui.ch_cat_dynamics->isChecked() ||
  5220. + ui.ch_cat_eq->isChecked() ||
  5221. + ui.ch_cat_filter->isChecked() ||
  5222. + ui.ch_cat_modulator->isChecked() ||
  5223. + ui.ch_cat_synth->isChecked() ||
  5224. + ui.ch_cat_utility->isChecked() ||
  5225. + ui.ch_cat_other->isChecked())) {
  5226. + ui.ch_cat_all->setChecked(true);
  5227. + }
  5228. + checkFilters();
  5229. +}
  5230. +
  5231. +void PluginListDialog::clearFilters()
  5232. +{
  5233. + auto setCheckedWithoutSignaling = [](auto &w, bool checked) {
  5234. + w->blockSignals(true);
  5235. + w->setChecked(checked);
  5236. + w->blockSignals(false);
  5237. + };
  5238. +
  5239. + setCheckedWithoutSignaling(ui.ch_internal, true);
  5240. + setCheckedWithoutSignaling(ui.ch_ladspa, true);
  5241. + setCheckedWithoutSignaling(ui.ch_lv2, true);
  5242. + setCheckedWithoutSignaling(ui.ch_vst, true);
  5243. + setCheckedWithoutSignaling(ui.ch_vst3, true);
  5244. + setCheckedWithoutSignaling(ui.ch_clap, true);
  5245. + setCheckedWithoutSignaling(ui.ch_jsfx, true);
  5246. +
  5247. + setCheckedWithoutSignaling(ui.ch_instruments, true);
  5248. + setCheckedWithoutSignaling(ui.ch_effects, true);
  5249. + setCheckedWithoutSignaling(ui.ch_midi, true);
  5250. + setCheckedWithoutSignaling(ui.ch_other, true);
  5251. +
  5252. + setCheckedWithoutSignaling(ui.ch_favorites, false);
  5253. + setCheckedWithoutSignaling(ui.ch_stereo, false);
  5254. + setCheckedWithoutSignaling(ui.ch_gui, false);
  5255. +
  5256. + setCheckedWithoutSignaling(ui.ch_cat_all, true);
  5257. + setCheckedWithoutSignaling(ui.ch_cat_delay, false);
  5258. + setCheckedWithoutSignaling(ui.ch_cat_distortion, false);
  5259. + setCheckedWithoutSignaling(ui.ch_cat_dynamics, false);
  5260. + setCheckedWithoutSignaling(ui.ch_cat_eq, false);
  5261. + setCheckedWithoutSignaling(ui.ch_cat_filter, false);
  5262. + setCheckedWithoutSignaling(ui.ch_cat_modulator, false);
  5263. + setCheckedWithoutSignaling(ui.ch_cat_synth, false);
  5264. + setCheckedWithoutSignaling(ui.ch_cat_utility, false);
  5265. + setCheckedWithoutSignaling(ui.ch_cat_other, false);
  5266. +
  5267. + ui.lineEdit->blockSignals(true);
  5268. + ui.lineEdit->clear();
  5269. + ui.lineEdit->blockSignals(false);
  5270. +
  5271. + checkFilters();
  5272. +}
  5273. +
  5274. +// ----------------------------------------------------------------------------
  5275. +
  5276. +void PluginListDialog::checkPlugin(const int row)
  5277. +{
  5278. + if (row >= 0) {
  5279. + ui.b_load->setEnabled(true);
  5280. +
  5281. + const PluginInfo info = asPluginInfo(
  5282. + ui.tableWidget->item(row, TW_NAME)
  5283. + ->data(Qt::UserRole + UR_PLUGIN_INFO));
  5284. +
  5285. + const bool isSynth = info.hints & PLUGIN_IS_SYNTH;
  5286. + const bool isEffect = info.audioIns > 0 && info.audioOuts > 0 &&
  5287. + !isSynth;
  5288. + const bool isMidi = info.audioIns == 0 && info.audioOuts == 0 &&
  5289. + info.midiIns > 0 && info.midiOuts > 0;
  5290. +
  5291. + QString ptype;
  5292. + /**/ if (isSynth)
  5293. + ptype = "Instrument";
  5294. + else if (isEffect)
  5295. + ptype = "Effect";
  5296. + else if (isMidi)
  5297. + ptype = "MIDI Plugin";
  5298. + else
  5299. + ptype = "Other";
  5300. +
  5301. + ui.l_format->setText(getPluginTypeAsString(
  5302. + static_cast<PluginType>(info.type)));
  5303. +
  5304. + ui.l_type->setText(ptype);
  5305. + ui.l_id->setText(QString::number(info.uniqueId));
  5306. + ui.l_ains->setText(QString::number(info.audioIns));
  5307. + ui.l_aouts->setText(QString::number(info.audioOuts));
  5308. + ui.l_mins->setText(QString::number(info.midiIns));
  5309. + ui.l_mouts->setText(QString::number(info.midiOuts));
  5310. + ui.l_pins->setText(QString::number(info.parameterIns));
  5311. + ui.l_pouts->setText(QString::number(info.parameterOuts));
  5312. + ui.l_gui->setText(info.hints & PLUGIN_HAS_CUSTOM_UI ? tr("Yes")
  5313. + : tr("No"));
  5314. + ui.l_synth->setText(isSynth ? tr("Yes") : tr("No"));
  5315. + } else {
  5316. + ui.b_load->setEnabled(false);
  5317. + ui.l_format->setText("---");
  5318. + ui.l_type->setText("---");
  5319. + ui.l_id->setText("---");
  5320. + ui.l_ains->setText("---");
  5321. + ui.l_aouts->setText("---");
  5322. + ui.l_mins->setText("---");
  5323. + ui.l_mouts->setText("---");
  5324. + ui.l_pins->setText("---");
  5325. + ui.l_pouts->setText("---");
  5326. + ui.l_gui->setText("---");
  5327. + ui.l_synth->setText("---");
  5328. + }
  5329. +}
  5330. +
  5331. +// ----------------------------------------------------------------------------
  5332. +
  5333. +void PluginListDialog::refreshPlugins()
  5334. +{
  5335. + refreshPluginsStop();
  5336. +
  5337. +#ifdef CARLA_2_6_FEATURES
  5338. + p->discovery.dialog = new PluginRefreshDialog(this);
  5339. + p->discovery.dialog->show();
  5340. +
  5341. + QObject::connect(p->discovery.dialog->b_start, &QPushButton::clicked,
  5342. + this, &PluginListDialog::refreshPluginsStart);
  5343. + QObject::connect(p->discovery.dialog->b_skip, &QPushButton::clicked,
  5344. + this, &PluginListDialog::refreshPluginsSkip);
  5345. + QObject::connect(p->discovery.dialog, &QDialog::finished, this,
  5346. + &PluginListDialog::refreshPluginsStop);
  5347. +#else
  5348. + refreshPluginsStart();
  5349. +#endif
  5350. +}
  5351. +
  5352. +void PluginListDialog::refreshPluginsStart()
  5353. +{
  5354. + // remove old plugins
  5355. + p->plugins.internal.clear();
  5356. + p->plugins.lv2.clear();
  5357. + p->plugins.jsfx.clear();
  5358. +#ifdef CARLA_2_6_FEATURES
  5359. + p->plugins.ladspa.clear();
  5360. + p->plugins.vst2.clear();
  5361. + p->plugins.vst3.clear();
  5362. + p->plugins.clap.clear();
  5363. + p->discovery.dialog->b_start->setEnabled(false);
  5364. + p->discovery.dialog->b_skip->setEnabled(true);
  5365. + p->discovery.ignoreCache = p->discovery.dialog->ch_all->isChecked();
  5366. + p->discovery.checkInvalid =
  5367. + p->discovery.dialog->ch_invalid->isChecked();
  5368. + if (p->discovery.ignoreCache)
  5369. + p->plugins.cache.clear();
  5370. +#endif
  5371. +
  5372. + // start discovery again
  5373. + p->discovery.ptype = PLUGIN_NONE;
  5374. +
  5375. + if (p->timerId == 0)
  5376. + p->timerId = startTimer(0);
  5377. +}
  5378. +
  5379. +void PluginListDialog::refreshPluginsStop()
  5380. +{
  5381. +#ifdef CARLA_2_6_FEATURES
  5382. + // stop previous discovery if still running
  5383. + if (p->discovery.handle != nullptr) {
  5384. + carla_plugin_discovery_stop(p->discovery.handle);
  5385. + p->discovery.handle = nullptr;
  5386. + }
  5387. +
  5388. + if (p->discovery.dialog) {
  5389. + p->discovery.dialog->close();
  5390. + p->discovery.dialog = nullptr;
  5391. + }
  5392. +#endif
  5393. +
  5394. + if (p->timerId != 0) {
  5395. + killTimer(p->timerId);
  5396. + p->timerId = 0;
  5397. + addPluginsToTable();
  5398. + }
  5399. +}
  5400. +
  5401. +void PluginListDialog::refreshPluginsSkip()
  5402. +{
  5403. +#ifdef CARLA_2_6_FEATURES
  5404. + if (p->discovery.handle != nullptr)
  5405. + carla_plugin_discovery_skip(p->discovery.handle);
  5406. +#endif
  5407. +}
  5408. +
  5409. +// ----------------------------------------------------------------------------
  5410. +
  5411. +void PluginListDialog::saveSettings()
  5412. +{
  5413. + QSafeSettings settings;
  5414. + settings.setValue("PluginListDialog/Geometry", saveGeometry());
  5415. + settings.setValue("PluginListDialog/TableGeometry",
  5416. + ui.tableWidget->horizontalHeader()->saveState());
  5417. + settings.setValue("PluginListDialog/ShowEffects",
  5418. + ui.ch_effects->isChecked());
  5419. + settings.setValue("PluginListDialog/ShowInstruments",
  5420. + ui.ch_instruments->isChecked());
  5421. + settings.setValue("PluginListDialog/ShowMIDI", ui.ch_midi->isChecked());
  5422. + settings.setValue("PluginListDialog/ShowOther",
  5423. + ui.ch_other->isChecked());
  5424. + settings.setValue("PluginListDialog/ShowInternal",
  5425. + ui.ch_internal->isChecked());
  5426. + settings.setValue("PluginListDialog/ShowLADSPA",
  5427. + ui.ch_ladspa->isChecked());
  5428. + settings.setValue("PluginListDialog/ShowLV2", ui.ch_lv2->isChecked());
  5429. + settings.setValue("PluginListDialog/ShowVST2", ui.ch_vst->isChecked());
  5430. + settings.setValue("PluginListDialog/ShowVST3", ui.ch_vst3->isChecked());
  5431. + settings.setValue("PluginListDialog/ShowCLAP", ui.ch_clap->isChecked());
  5432. + settings.setValue("PluginListDialog/ShowJSFX", ui.ch_jsfx->isChecked());
  5433. + settings.setValue("PluginListDialog/ShowFavorites",
  5434. + ui.ch_favorites->isChecked());
  5435. + settings.setValue("PluginListDialog/ShowHasGUI",
  5436. + ui.ch_gui->isChecked());
  5437. + settings.setValue("PluginListDialog/ShowStereoOnly",
  5438. + ui.ch_stereo->isChecked());
  5439. + settings.setValue("PluginListDialog/SearchText", ui.lineEdit->text());
  5440. +
  5441. + if (ui.ch_cat_all->isChecked()) {
  5442. + settings.setValue("PluginListDialog/ShowCategory", "all");
  5443. + } else {
  5444. + QUtf8String categories;
  5445. + if (ui.ch_cat_delay->isChecked())
  5446. + categories += ":delay";
  5447. + if (ui.ch_cat_distortion->isChecked())
  5448. + categories += ":distortion";
  5449. + if (ui.ch_cat_dynamics->isChecked())
  5450. + categories += ":dynamics";
  5451. + if (ui.ch_cat_eq->isChecked())
  5452. + categories += ":eq";
  5453. + if (ui.ch_cat_filter->isChecked())
  5454. + categories += ":filter";
  5455. + if (ui.ch_cat_modulator->isChecked())
  5456. + categories += ":modulator";
  5457. + if (ui.ch_cat_synth->isChecked())
  5458. + categories += ":synth";
  5459. + if (ui.ch_cat_utility->isChecked())
  5460. + categories += ":utility";
  5461. + if (ui.ch_cat_other->isChecked())
  5462. + categories += ":other";
  5463. + if (categories.isNotEmpty())
  5464. + categories += ":";
  5465. + settings.setValue("PluginListDialog/ShowCategory", categories);
  5466. + }
  5467. +
  5468. + settings.setValue("PluginListDialog/Favorites",
  5469. + asVariant(p->plugins.favorites));
  5470. +}
  5471. +
  5472. +// ----------------------------------------------------------------------------
  5473. +
  5474. +const PluginListDialogResults *carla_exec_plugin_list_dialog()
  5475. +{
  5476. + // create and keep dialog around, as recreating the dialog means doing
  5477. + // a rescan. Qt will delete it later together with the main window
  5478. + static PluginListDialog *const gui =
  5479. + new PluginListDialog(carla_qt_get_main_window());
  5480. +
  5481. + if (gui->exec()) {
  5482. + static PluginListDialogResults ret;
  5483. + static CarlaString filename;
  5484. + static CarlaString label;
  5485. +
  5486. + const PluginInfo &plugin(gui->getSelectedPluginInfo());
  5487. +
  5488. + filename = plugin.filename.toUtf8();
  5489. + label = plugin.label.toUtf8();
  5490. +
  5491. + ret.build = plugin.build;
  5492. + ret.type = plugin.type;
  5493. + ret.filename = filename;
  5494. + ret.label = label;
  5495. + ret.uniqueId = plugin.uniqueId;
  5496. +
  5497. + return &ret;
  5498. + }
  5499. +
  5500. + return nullptr;
  5501. +}
  5502. +
  5503. +// ----------------------------------------------------------------------------
  5504. diff --git a/plugins/carla/pluginlistdialog.hpp b/plugins/carla/pluginlistdialog.hpp
  5505. new file mode 100644
  5506. index 000000000..a3768c99a
  5507. --- /dev/null
  5508. +++ b/plugins/carla/pluginlistdialog.hpp
  5509. @@ -0,0 +1,91 @@
  5510. +/*
  5511. + * Carla plugin host, adjusted for OBS
  5512. + * Copyright (C) 2011-2023 Filipe Coelho <falktx@falktx.com>
  5513. + * SPDX-License-Identifier: GPL-2.0-or-later
  5514. + */
  5515. +
  5516. +#pragma once
  5517. +
  5518. +#include <CarlaDefines.h>
  5519. +
  5520. +#include "ui_pluginlistdialog.h"
  5521. +
  5522. +#if CARLA_VERSION_HEX >= 0x020591
  5523. +#define CARLA_2_6_FEATURES
  5524. +#endif
  5525. +
  5526. +class QSafeSettings;
  5527. +typedef struct _CarlaPluginDiscoveryInfo CarlaPluginDiscoveryInfo;
  5528. +struct PluginInfo;
  5529. +
  5530. +// ----------------------------------------------------------------------------
  5531. +// Plugin List Dialog
  5532. +
  5533. +class PluginListDialog : public QDialog {
  5534. + enum TableIndex {
  5535. + TW_FAVORITE,
  5536. + TW_NAME,
  5537. + TW_LABEL,
  5538. + TW_MAKER,
  5539. + TW_BINARY,
  5540. + };
  5541. +
  5542. + enum UserRoles {
  5543. + UR_PLUGIN_INFO = 1,
  5544. + UR_SEARCH_TEXT,
  5545. + };
  5546. +
  5547. + struct PrivateData;
  5548. + PrivateData *const p;
  5549. +
  5550. + Ui_PluginListDialog ui;
  5551. +
  5552. + // --------------------------------------------------------------------
  5553. + // public methods
  5554. +
  5555. +public:
  5556. + explicit PluginListDialog(QWidget *parent);
  5557. + ~PluginListDialog() override;
  5558. +
  5559. + const PluginInfo &getSelectedPluginInfo() const;
  5560. +#ifdef CARLA_2_6_FEATURES
  5561. + void addPluginInfo(const CarlaPluginDiscoveryInfo *info,
  5562. + const char *sha1sum);
  5563. + bool checkPluginCache(const char *filename, const char *sha1sum);
  5564. +#endif
  5565. +
  5566. + // --------------------------------------------------------------------
  5567. + // protected methods
  5568. +
  5569. +protected:
  5570. + void done(int) override;
  5571. + void showEvent(QShowEvent *) override;
  5572. + void timerEvent(QTimerEvent *) override;
  5573. +
  5574. + // --------------------------------------------------------------------
  5575. + // private methods
  5576. +
  5577. +private:
  5578. + void addPluginsToTable();
  5579. + void loadSettings();
  5580. +
  5581. + // --------------------------------------------------------------------
  5582. + // private slots
  5583. +
  5584. +private Q_SLOTS:
  5585. + void cellClicked(int row, int column);
  5586. + void cellDoubleClicked(int row, int column);
  5587. + void focusSearchFieldAndSelectAll();
  5588. + void checkFilters();
  5589. + void checkFiltersCategoryAll(bool clicked);
  5590. + void checkFiltersCategorySpecific(bool clicked);
  5591. + void clearFilters();
  5592. + void checkPlugin(int row);
  5593. + void refreshPlugins();
  5594. + void refreshPluginsStart();
  5595. + void refreshPluginsStop();
  5596. + void refreshPluginsSkip();
  5597. + void saveSettings();
  5598. +};
  5599. +
  5600. +// ----------------------------------------------------------------------------
  5601. diff --git a/plugins/carla/pluginlistdialog.ui b/plugins/carla/pluginlistdialog.ui
  5602. new file mode 100644
  5603. index 000000000..fd6579ff3
  5604. --- /dev/null
  5605. +++ b/plugins/carla/pluginlistdialog.ui
  5606. @@ -0,0 +1,765 @@
  5607. +<?xml version="1.0" encoding="UTF-8"?>
  5608. +<ui version="4.0">
  5609. + <class>PluginListDialog</class>
  5610. + <widget class="QDialog" name="PluginListDialog">
  5611. + <property name="geometry">
  5612. + <rect>
  5613. + <x>0</x>
  5614. + <y>0</y>
  5615. + <width>1100</width>
  5616. + <height>738</height>
  5617. + </rect>
  5618. + </property>
  5619. + <property name="windowTitle">
  5620. + <string>Plugin List</string>
  5621. + </property>
  5622. + <layout class="QGridLayout" name="gridLayout_3">
  5623. + <item row="2" column="0">
  5624. + <spacer name="verticalSpacer_6">
  5625. + <property name="orientation">
  5626. + <enum>Qt::Vertical</enum>
  5627. + </property>
  5628. + <property name="sizeHint" stdset="0">
  5629. + <size>
  5630. + <width>20</width>
  5631. + <height>40</height>
  5632. + </size>
  5633. + </property>
  5634. + </spacer>
  5635. + </item>
  5636. + <item row="4" column="0" colspan="3">
  5637. + <layout class="QHBoxLayout" name="horizontalLayout_2">
  5638. + <item>
  5639. + <widget class="QLabel" name="label">
  5640. + <property name="text">
  5641. + <string/>
  5642. + </property>
  5643. + <property name="textInteractionFlags">
  5644. + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
  5645. + </property>
  5646. + </widget>
  5647. + </item>
  5648. + <item>
  5649. + <spacer name="horizontalSpacer">
  5650. + <property name="orientation">
  5651. + <enum>Qt::Horizontal</enum>
  5652. + </property>
  5653. + <property name="sizeHint" stdset="0">
  5654. + <size>
  5655. + <width>40</width>
  5656. + <height>20</height>
  5657. + </size>
  5658. + </property>
  5659. + </spacer>
  5660. + </item>
  5661. + <item>
  5662. + <widget class="QPushButton" name="b_load">
  5663. + <property name="text">
  5664. + <string>&amp;Load Plugin</string>
  5665. + </property>
  5666. + </widget>
  5667. + </item>
  5668. + <item>
  5669. + <widget class="QPushButton" name="b_cancel">
  5670. + <property name="text">
  5671. + <string>Cancel</string>
  5672. + </property>
  5673. + </widget>
  5674. + </item>
  5675. + </layout>
  5676. + </item>
  5677. + <item row="0" column="0" colspan="3">
  5678. + <layout class="QHBoxLayout" name="horizontalLayout">
  5679. + <item>
  5680. + <widget class="QLineEdit" name="lineEdit">
  5681. + <property name="clearButtonEnabled">
  5682. + <bool>true</bool>
  5683. + </property>
  5684. + </widget>
  5685. + </item>
  5686. + <item>
  5687. + <widget class="QPushButton" name="b_refresh">
  5688. + <property name="text">
  5689. + <string>Refresh</string>
  5690. + </property>
  5691. + </widget>
  5692. + </item>
  5693. + <item>
  5694. + <widget class="QPushButton" name="b_clear_filters">
  5695. + <property name="text">
  5696. + <string>Reset filters</string>
  5697. + </property>
  5698. + </widget>
  5699. + </item>
  5700. + </layout>
  5701. + </item>
  5702. + <item row="1" column="1" rowspan="3">
  5703. + <widget class="QTableWidget" name="tableWidget">
  5704. + <property name="sizePolicy">
  5705. + <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
  5706. + <horstretch>1</horstretch>
  5707. + <verstretch>0</verstretch>
  5708. + </sizepolicy>
  5709. + </property>
  5710. + <property name="editTriggers">
  5711. + <set>QAbstractItemView::NoEditTriggers</set>
  5712. + </property>
  5713. + <property name="showDropIndicator" stdset="0">
  5714. + <bool>false</bool>
  5715. + </property>
  5716. + <property name="dragDropOverwriteMode">
  5717. + <bool>false</bool>
  5718. + </property>
  5719. + <property name="alternatingRowColors">
  5720. + <bool>true</bool>
  5721. + </property>
  5722. + <property name="selectionMode">
  5723. + <enum>QAbstractItemView::SingleSelection</enum>
  5724. + </property>
  5725. + <property name="selectionBehavior">
  5726. + <enum>QAbstractItemView::SelectRows</enum>
  5727. + </property>
  5728. + <property name="showGrid">
  5729. + <bool>false</bool>
  5730. + </property>
  5731. + <property name="gridStyle">
  5732. + <enum>Qt::NoPen</enum>
  5733. + </property>
  5734. + <property name="sortingEnabled">
  5735. + <bool>true</bool>
  5736. + </property>
  5737. + <property name="wordWrap">
  5738. + <bool>false</bool>
  5739. + </property>
  5740. + <attribute name="horizontalHeaderMinimumSectionSize">
  5741. + <number>24</number>
  5742. + </attribute>
  5743. + <attribute name="horizontalHeaderStretchLastSection">
  5744. + <bool>true</bool>
  5745. + </attribute>
  5746. + <attribute name="verticalHeaderVisible">
  5747. + <bool>false</bool>
  5748. + </attribute>
  5749. + <attribute name="verticalHeaderMinimumSectionSize">
  5750. + <number>12</number>
  5751. + </attribute>
  5752. + <attribute name="verticalHeaderDefaultSectionSize">
  5753. + <number>22</number>
  5754. + </attribute>
  5755. + <column>
  5756. + <property name="text">
  5757. + <string notr="true"/>
  5758. + </property>
  5759. + <property name="toolTip">
  5760. + <string notr="true"/>
  5761. + </property>
  5762. + <property name="whatsThis">
  5763. + <string notr="true"/>
  5764. + </property>
  5765. + <property name="icon">
  5766. + <iconset>
  5767. + <normaloff>:/16x16/bookmarks.svgz</normaloff>:/16x16/bookmarks.svgz</iconset>
  5768. + </property>
  5769. + </column>
  5770. + <column>
  5771. + <property name="text">
  5772. + <string>Name</string>
  5773. + </property>
  5774. + </column>
  5775. + <column>
  5776. + <property name="text">
  5777. + <string>Label/Id/URI</string>
  5778. + </property>
  5779. + </column>
  5780. + <column>
  5781. + <property name="text">
  5782. + <string>Maker</string>
  5783. + </property>
  5784. + </column>
  5785. + <column>
  5786. + <property name="text">
  5787. + <string>Binary/Filename</string>
  5788. + </property>
  5789. + </column>
  5790. + </widget>
  5791. + </item>
  5792. + <item row="1" column="0">
  5793. + <widget class="QToolBox" name="toolBox">
  5794. + <property name="currentIndex">
  5795. + <number>0</number>
  5796. + </property>
  5797. + <widget class="QWidget" name="p_format">
  5798. + <property name="geometry">
  5799. + <rect>
  5800. + <x>0</x>
  5801. + <y>0</y>
  5802. + <width>141</width>
  5803. + <height>241</height>
  5804. + </rect>
  5805. + </property>
  5806. + <attribute name="label">
  5807. + <string>Format</string>
  5808. + </attribute>
  5809. + <layout class="QVBoxLayout" name="verticalLayout">
  5810. + <item>
  5811. + <widget class="QCheckBox" name="ch_internal">
  5812. + <property name="text">
  5813. + <string>Internal</string>
  5814. + </property>
  5815. + </widget>
  5816. + </item>
  5817. + <item>
  5818. + <widget class="QCheckBox" name="ch_ladspa">
  5819. + <property name="text">
  5820. + <string>LADSPA</string>
  5821. + </property>
  5822. + </widget>
  5823. + </item>
  5824. + <item>
  5825. + <widget class="QCheckBox" name="ch_lv2">
  5826. + <property name="text">
  5827. + <string>LV2</string>
  5828. + </property>
  5829. + </widget>
  5830. + </item>
  5831. + <item>
  5832. + <widget class="QCheckBox" name="ch_vst">
  5833. + <property name="text">
  5834. + <string>VST2</string>
  5835. + </property>
  5836. + </widget>
  5837. + </item>
  5838. + <item>
  5839. + <widget class="QCheckBox" name="ch_vst3">
  5840. + <property name="text">
  5841. + <string>VST3</string>
  5842. + </property>
  5843. + </widget>
  5844. + </item>
  5845. + <item>
  5846. + <widget class="QCheckBox" name="ch_clap">
  5847. + <property name="text">
  5848. + <string>CLAP</string>
  5849. + </property>
  5850. + </widget>
  5851. + </item>
  5852. + <item>
  5853. + <widget class="QCheckBox" name="ch_jsfx">
  5854. + <property name="text">
  5855. + <string>JSFX</string>
  5856. + </property>
  5857. + </widget>
  5858. + </item>
  5859. + <item>
  5860. + <spacer name="verticalSpacer_5">
  5861. + <property name="orientation">
  5862. + <enum>Qt::Vertical</enum>
  5863. + </property>
  5864. + <property name="sizeHint" stdset="0">
  5865. + <size>
  5866. + <width>20</width>
  5867. + <height>40</height>
  5868. + </size>
  5869. + </property>
  5870. + </spacer>
  5871. + </item>
  5872. + </layout>
  5873. + </widget>
  5874. + <widget class="QWidget" name="p_type">
  5875. + <property name="geometry">
  5876. + <rect>
  5877. + <x>0</x>
  5878. + <y>0</y>
  5879. + <width>141</width>
  5880. + <height>168</height>
  5881. + </rect>
  5882. + </property>
  5883. + <attribute name="label">
  5884. + <string>Type</string>
  5885. + </attribute>
  5886. + <layout class="QVBoxLayout" name="verticalLayout_2">
  5887. + <item>
  5888. + <widget class="QCheckBox" name="ch_effects">
  5889. + <property name="text">
  5890. + <string>Effects</string>
  5891. + </property>
  5892. + </widget>
  5893. + </item>
  5894. + <item>
  5895. + <widget class="QCheckBox" name="ch_instruments">
  5896. + <property name="text">
  5897. + <string>Instruments</string>
  5898. + </property>
  5899. + </widget>
  5900. + </item>
  5901. + <item>
  5902. + <widget class="QCheckBox" name="ch_midi">
  5903. + <property name="text">
  5904. + <string>MIDI Plugins</string>
  5905. + </property>
  5906. + </widget>
  5907. + </item>
  5908. + <item>
  5909. + <widget class="QCheckBox" name="ch_other">
  5910. + <property name="text">
  5911. + <string>Other/Misc</string>
  5912. + </property>
  5913. + </widget>
  5914. + </item>
  5915. + <item>
  5916. + <spacer name="verticalSpacer_3">
  5917. + <property name="orientation">
  5918. + <enum>Qt::Vertical</enum>
  5919. + </property>
  5920. + <property name="sizeHint" stdset="0">
  5921. + <size>
  5922. + <width>20</width>
  5923. + <height>40</height>
  5924. + </size>
  5925. + </property>
  5926. + </spacer>
  5927. + </item>
  5928. + </layout>
  5929. + </widget>
  5930. + <widget class="QWidget" name="p_category">
  5931. + <property name="geometry">
  5932. + <rect>
  5933. + <x>0</x>
  5934. + <y>0</y>
  5935. + <width>141</width>
  5936. + <height>305</height>
  5937. + </rect>
  5938. + </property>
  5939. + <attribute name="label">
  5940. + <string>Category</string>
  5941. + </attribute>
  5942. + <layout class="QVBoxLayout" name="verticalLayout_4">
  5943. + <item>
  5944. + <widget class="QCheckBox" name="ch_cat_all">
  5945. + <property name="text">
  5946. + <string>All</string>
  5947. + </property>
  5948. + </widget>
  5949. + </item>
  5950. + <item>
  5951. + <widget class="QCheckBox" name="ch_cat_delay">
  5952. + <property name="text">
  5953. + <string>Delay</string>
  5954. + </property>
  5955. + </widget>
  5956. + </item>
  5957. + <item>
  5958. + <widget class="QCheckBox" name="ch_cat_distortion">
  5959. + <property name="text">
  5960. + <string>Distortion</string>
  5961. + </property>
  5962. + </widget>
  5963. + </item>
  5964. + <item>
  5965. + <widget class="QCheckBox" name="ch_cat_dynamics">
  5966. + <property name="text">
  5967. + <string>Dynamics</string>
  5968. + </property>
  5969. + </widget>
  5970. + </item>
  5971. + <item>
  5972. + <widget class="QCheckBox" name="ch_cat_eq">
  5973. + <property name="text">
  5974. + <string>EQ</string>
  5975. + </property>
  5976. + </widget>
  5977. + </item>
  5978. + <item>
  5979. + <widget class="QCheckBox" name="ch_cat_filter">
  5980. + <property name="text">
  5981. + <string>Filter</string>
  5982. + </property>
  5983. + </widget>
  5984. + </item>
  5985. + <item>
  5986. + <widget class="QCheckBox" name="ch_cat_modulator">
  5987. + <property name="text">
  5988. + <string>Modulator</string>
  5989. + </property>
  5990. + </widget>
  5991. + </item>
  5992. + <item>
  5993. + <widget class="QCheckBox" name="ch_cat_synth">
  5994. + <property name="text">
  5995. + <string>Synth</string>
  5996. + </property>
  5997. + </widget>
  5998. + </item>
  5999. + <item>
  6000. + <widget class="QCheckBox" name="ch_cat_utility">
  6001. + <property name="text">
  6002. + <string>Utility</string>
  6003. + </property>
  6004. + </widget>
  6005. + </item>
  6006. + <item>
  6007. + <widget class="QCheckBox" name="ch_cat_other">
  6008. + <property name="text">
  6009. + <string>Other</string>
  6010. + </property>
  6011. + </widget>
  6012. + </item>
  6013. + <item>
  6014. + <spacer name="verticalSpacer_7">
  6015. + <property name="orientation">
  6016. + <enum>Qt::Vertical</enum>
  6017. + </property>
  6018. + <property name="sizeHint" stdset="0">
  6019. + <size>
  6020. + <width>20</width>
  6021. + <height>23</height>
  6022. + </size>
  6023. + </property>
  6024. + </spacer>
  6025. + </item>
  6026. + </layout>
  6027. + </widget>
  6028. + </widget>
  6029. + </item>
  6030. + <item row="3" column="0">
  6031. + <widget class="QFrame" name="frame_reqs">
  6032. + <property name="sizePolicy">
  6033. + <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
  6034. + <horstretch>0</horstretch>
  6035. + <verstretch>0</verstretch>
  6036. + </sizepolicy>
  6037. + </property>
  6038. + <layout class="QGridLayout" name="gridLayout">
  6039. + <item row="2" column="1">
  6040. + <widget class="QCheckBox" name="ch_stereo">
  6041. + <property name="text">
  6042. + <string>Stereo only</string>
  6043. + </property>
  6044. + </widget>
  6045. + </item>
  6046. + <item row="3" column="1">
  6047. + <widget class="QCheckBox" name="ch_gui">
  6048. + <property name="text">
  6049. + <string>With Custom GUI </string>
  6050. + </property>
  6051. + </widget>
  6052. + </item>
  6053. + <item row="0" column="1">
  6054. + <widget class="QLabel" name="l_reqs">
  6055. + <property name="font">
  6056. + <font>
  6057. + <weight>75</weight>
  6058. + <bold>true</bold>
  6059. + </font>
  6060. + </property>
  6061. + <property name="text">
  6062. + <string>Requirements</string>
  6063. + </property>
  6064. + </widget>
  6065. + </item>
  6066. + <item row="1" column="1">
  6067. + <widget class="QCheckBox" name="ch_favorites">
  6068. + <property name="text">
  6069. + <string>Favorites only</string>
  6070. + </property>
  6071. + </widget>
  6072. + </item>
  6073. + </layout>
  6074. + </widget>
  6075. + </item>
  6076. + <item row="1" column="2" rowspan="3">
  6077. + <widget class="QFrame" name="frame_info">
  6078. + <layout class="QGridLayout" name="gridLayout_2">
  6079. + <item row="9" column="0">
  6080. + <widget class="QLabel" name="label_14">
  6081. + <property name="text">
  6082. + <string>Parameter Ins:</string>
  6083. + </property>
  6084. + <property name="alignment">
  6085. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6086. + </property>
  6087. + </widget>
  6088. + </item>
  6089. + <item row="3" column="0">
  6090. + <widget class="QLabel" name="la_id">
  6091. + <property name="text">
  6092. + <string>UniqueID:</string>
  6093. + </property>
  6094. + <property name="alignment">
  6095. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6096. + </property>
  6097. + </widget>
  6098. + </item>
  6099. + <item row="1" column="1">
  6100. + <widget class="QLabel" name="l_format">
  6101. + <property name="text">
  6102. + <string>TextLabel</string>
  6103. + </property>
  6104. + </widget>
  6105. + </item>
  6106. + <item row="5" column="0">
  6107. + <widget class="QLabel" name="label_8">
  6108. + <property name="text">
  6109. + <string>Audio Ins:</string>
  6110. + </property>
  6111. + <property name="alignment">
  6112. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6113. + </property>
  6114. + </widget>
  6115. + </item>
  6116. + <item row="4" column="0" colspan="2">
  6117. + <widget class="Line" name="line">
  6118. + <property name="lineWidth">
  6119. + <number>0</number>
  6120. + </property>
  6121. + <property name="midLineWidth">
  6122. + <number>1</number>
  6123. + </property>
  6124. + <property name="orientation">
  6125. + <enum>Qt::Horizontal</enum>
  6126. + </property>
  6127. + </widget>
  6128. + </item>
  6129. + <item row="6" column="1">
  6130. + <widget class="QLabel" name="l_aouts">
  6131. + <property name="text">
  6132. + <string>TextLabel</string>
  6133. + </property>
  6134. + </widget>
  6135. + </item>
  6136. + <item row="10" column="0">
  6137. + <widget class="QLabel" name="label_15">
  6138. + <property name="text">
  6139. + <string>Parameter Outs:</string>
  6140. + </property>
  6141. + <property name="alignment">
  6142. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6143. + </property>
  6144. + </widget>
  6145. + </item>
  6146. + <item row="8" column="1">
  6147. + <widget class="QLabel" name="l_mouts">
  6148. + <property name="text">
  6149. + <string>TextLabel</string>
  6150. + </property>
  6151. + </widget>
  6152. + </item>
  6153. + <item row="14" column="0">
  6154. + <spacer name="verticalSpacer_2">
  6155. + <property name="orientation">
  6156. + <enum>Qt::Vertical</enum>
  6157. + </property>
  6158. + <property name="sizeHint" stdset="0">
  6159. + <size>
  6160. + <width>20</width>
  6161. + <height>40</height>
  6162. + </size>
  6163. + </property>
  6164. + </spacer>
  6165. + </item>
  6166. + <item row="9" column="1">
  6167. + <widget class="QLabel" name="l_pins">
  6168. + <property name="text">
  6169. + <string>TextLabel</string>
  6170. + </property>
  6171. + </widget>
  6172. + </item>
  6173. + <item row="5" column="1">
  6174. + <widget class="QLabel" name="l_ains">
  6175. + <property name="text">
  6176. + <string>TextLabel</string>
  6177. + </property>
  6178. + </widget>
  6179. + </item>
  6180. + <item row="7" column="1">
  6181. + <widget class="QLabel" name="l_mins">
  6182. + <property name="text">
  6183. + <string>TextLabel</string>
  6184. + </property>
  6185. + </widget>
  6186. + </item>
  6187. + <item row="14" column="1">
  6188. + <spacer name="verticalSpacer">
  6189. + <property name="orientation">
  6190. + <enum>Qt::Vertical</enum>
  6191. + </property>
  6192. + <property name="sizeHint" stdset="0">
  6193. + <size>
  6194. + <width>20</width>
  6195. + <height>40</height>
  6196. + </size>
  6197. + </property>
  6198. + </spacer>
  6199. + </item>
  6200. + <item row="7" column="0">
  6201. + <widget class="QLabel" name="label_12">
  6202. + <property name="text">
  6203. + <string>MIDI Ins:</string>
  6204. + </property>
  6205. + <property name="alignment">
  6206. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6207. + </property>
  6208. + </widget>
  6209. + </item>
  6210. + <item row="12" column="1">
  6211. + <widget class="QLabel" name="l_gui">
  6212. + <property name="text">
  6213. + <string>TextLabel</string>
  6214. + </property>
  6215. + </widget>
  6216. + </item>
  6217. + <item row="13" column="0">
  6218. + <widget class="QLabel" name="label_19">
  6219. + <property name="text">
  6220. + <string>Is Synth:</string>
  6221. + </property>
  6222. + <property name="alignment">
  6223. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6224. + </property>
  6225. + </widget>
  6226. + </item>
  6227. + <item row="0" column="0" colspan="2">
  6228. + <widget class="QLabel" name="label_3">
  6229. + <property name="font">
  6230. + <font>
  6231. + <weight>75</weight>
  6232. + <bold>true</bold>
  6233. + </font>
  6234. + </property>
  6235. + <property name="text">
  6236. + <string>Information</string>
  6237. + </property>
  6238. + <property name="alignment">
  6239. + <set>Qt::AlignCenter</set>
  6240. + </property>
  6241. + </widget>
  6242. + </item>
  6243. + <item row="8" column="0">
  6244. + <widget class="QLabel" name="label_13">
  6245. + <property name="text">
  6246. + <string>MIDI Outs:</string>
  6247. + </property>
  6248. + <property name="alignment">
  6249. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6250. + </property>
  6251. + </widget>
  6252. + </item>
  6253. + <item row="11" column="0" colspan="2">
  6254. + <widget class="Line" name="line_2">
  6255. + <property name="lineWidth">
  6256. + <number>0</number>
  6257. + </property>
  6258. + <property name="midLineWidth">
  6259. + <number>1</number>
  6260. + </property>
  6261. + <property name="orientation">
  6262. + <enum>Qt::Horizontal</enum>
  6263. + </property>
  6264. + </widget>
  6265. + </item>
  6266. + <item row="13" column="1">
  6267. + <widget class="QLabel" name="l_synth">
  6268. + <property name="text">
  6269. + <string>TextLabel</string>
  6270. + </property>
  6271. + </widget>
  6272. + </item>
  6273. + <item row="3" column="1">
  6274. + <widget class="QLabel" name="l_id">
  6275. + <property name="text">
  6276. + <string>TextLabel</string>
  6277. + </property>
  6278. + </widget>
  6279. + </item>
  6280. + <item row="6" column="0">
  6281. + <widget class="QLabel" name="label_9">
  6282. + <property name="text">
  6283. + <string>Audio Outs:</string>
  6284. + </property>
  6285. + <property name="alignment">
  6286. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6287. + </property>
  6288. + </widget>
  6289. + </item>
  6290. + <item row="12" column="0">
  6291. + <widget class="QLabel" name="label_17">
  6292. + <property name="text">
  6293. + <string>Has Custom GUI:</string>
  6294. + </property>
  6295. + <property name="alignment">
  6296. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6297. + </property>
  6298. + </widget>
  6299. + </item>
  6300. + <item row="2" column="0">
  6301. + <widget class="QLabel" name="label_6">
  6302. + <property name="text">
  6303. + <string>Type:</string>
  6304. + </property>
  6305. + <property name="alignment">
  6306. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6307. + </property>
  6308. + </widget>
  6309. + </item>
  6310. + <item row="1" column="0">
  6311. + <widget class="QLabel" name="label_2">
  6312. + <property name="text">
  6313. + <string>Format:</string>
  6314. + </property>
  6315. + <property name="alignment">
  6316. + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
  6317. + </property>
  6318. + </widget>
  6319. + </item>
  6320. + <item row="10" column="1">
  6321. + <widget class="QLabel" name="l_pouts">
  6322. + <property name="text">
  6323. + <string>TextLabel</string>
  6324. + </property>
  6325. + </widget>
  6326. + </item>
  6327. + <item row="2" column="1">
  6328. + <widget class="QLabel" name="l_type">
  6329. + <property name="text">
  6330. + <string>TextLabel</string>
  6331. + </property>
  6332. + </widget>
  6333. + </item>
  6334. + </layout>
  6335. + </widget>
  6336. + </item>
  6337. + </layout>
  6338. + <action name="act_focus_search">
  6339. + <property name="text">
  6340. + <string>Focus Text Search</string>
  6341. + </property>
  6342. + <property name="shortcut">
  6343. + <string>Ctrl+F</string>
  6344. + </property>
  6345. + </action>
  6346. + </widget>
  6347. + <tabstops>
  6348. + <tabstop>lineEdit</tabstop>
  6349. + <tabstop>tableWidget</tabstop>
  6350. + <tabstop>b_load</tabstop>
  6351. + <tabstop>b_cancel</tabstop>
  6352. + <tabstop>b_refresh</tabstop>
  6353. + <tabstop>b_clear_filters</tabstop>
  6354. + <tabstop>ch_internal</tabstop>
  6355. + <tabstop>ch_ladspa</tabstop>
  6356. + <tabstop>ch_lv2</tabstop>
  6357. + <tabstop>ch_vst</tabstop>
  6358. + <tabstop>ch_vst3</tabstop>
  6359. + <tabstop>ch_clap</tabstop>
  6360. + <tabstop>ch_effects</tabstop>
  6361. + <tabstop>ch_instruments</tabstop>
  6362. + <tabstop>ch_midi</tabstop>
  6363. + <tabstop>ch_other</tabstop>
  6364. + <tabstop>ch_stereo</tabstop>
  6365. + <tabstop>ch_gui</tabstop>
  6366. + <tabstop>frame_reqs</tabstop>
  6367. + <tabstop>frame_info</tabstop>
  6368. + </tabstops>
  6369. + <resources/>
  6370. + <connections/>
  6371. +</ui>
  6372. diff --git a/plugins/carla/pluginrefreshdialog.hpp b/plugins/carla/pluginrefreshdialog.hpp
  6373. new file mode 100644
  6374. index 000000000..961e2361c
  6375. --- /dev/null
  6376. +++ b/plugins/carla/pluginrefreshdialog.hpp
  6377. @@ -0,0 +1,77 @@
  6378. +/*
  6379. + * Carla plugin host, adjusted for OBS
  6380. + * Copyright (C) 2011-2023 Filipe Coelho <falktx@falktx.com>
  6381. + * SPDX-License-Identifier: GPL-2.0-or-later
  6382. + */
  6383. +
  6384. +#pragma once
  6385. +
  6386. +#include "ui_pluginrefreshdialog.h"
  6387. +
  6388. +#include "qtutils.h"
  6389. +
  6390. +// ----------------------------------------------------------------------------
  6391. +// Plugin Refresh Dialog
  6392. +
  6393. +struct PluginRefreshDialog : QDialog, Ui_PluginRefreshDialog {
  6394. + explicit PluginRefreshDialog(QWidget *const parent) : QDialog(parent)
  6395. + {
  6396. + setupUi(this);
  6397. +
  6398. + setWindowFlags(windowFlags() &
  6399. + ~Qt::WindowContextHelpButtonHint);
  6400. +#ifdef __APPLE__
  6401. + setWindowModality(Qt::WindowModal);
  6402. +#endif
  6403. +
  6404. + b_skip->setEnabled(false);
  6405. + ch_invalid->setEnabled(false);
  6406. +
  6407. + // ------------------------------------------------------------
  6408. + // Load settings
  6409. +
  6410. + {
  6411. + const QSafeSettings settings;
  6412. +
  6413. + restoreGeometry(settings.valueByteArray(
  6414. + "PluginRefreshDialog/Geometry"));
  6415. +
  6416. + if (settings.valueBool("PluginRefreshDialog/RefreshAll",
  6417. + false))
  6418. + ch_all->setChecked(true);
  6419. + else
  6420. + ch_updated->setChecked(true);
  6421. +
  6422. + ch_invalid->setChecked(settings.valueBool(
  6423. + "PluginRefreshDialog/CheckInvalid", false));
  6424. + }
  6425. +
  6426. + // ------------------------------------------------------------
  6427. + // Set-up Icons
  6428. +
  6429. + b_start->setProperty("themeID", "playIcon");
  6430. +
  6431. + // ------------------------------------------------------------
  6432. + // Set-up connections
  6433. +
  6434. + QObject::connect(this, &QDialog::finished, this,
  6435. + &PluginRefreshDialog::saveSettings);
  6436. + }
  6437. +
  6438. + // --------------------------------------------------------------------
  6439. + // private slots
  6440. +
  6441. +private Q_SLOTS:
  6442. + void saveSettings()
  6443. + {
  6444. + QSafeSettings settings;
  6445. + settings.setValue("PluginRefreshDialog/Geometry",
  6446. + saveGeometry());
  6447. + settings.setValue("PluginRefreshDialog/RefreshAll",
  6448. + ch_all->isChecked());
  6449. + settings.setValue("PluginRefreshDialog/CheckInvalid",
  6450. + ch_invalid->isChecked());
  6451. + }
  6452. +};
  6453. +
  6454. +// ----------------------------------------------------------------------------
  6455. diff --git a/plugins/carla/pluginrefreshdialog.ui b/plugins/carla/pluginrefreshdialog.ui
  6456. new file mode 100644
  6457. index 000000000..a47bc2770
  6458. --- /dev/null
  6459. +++ b/plugins/carla/pluginrefreshdialog.ui
  6460. @@ -0,0 +1,183 @@
  6461. +<?xml version="1.0" encoding="UTF-8"?>
  6462. +<ui version="4.0">
  6463. + <class>PluginRefreshDialog</class>
  6464. + <widget class="QDialog" name="PluginRefreshDialog">
  6465. + <property name="geometry">
  6466. + <rect>
  6467. + <x>0</x>
  6468. + <y>0</y>
  6469. + <width>873</width>
  6470. + <height>179</height>
  6471. + </rect>
  6472. + </property>
  6473. + <property name="windowTitle">
  6474. + <string>Plugin Refresh</string>
  6475. + </property>
  6476. + <layout class="QVBoxLayout" name="verticalLayout_5">
  6477. + <item>
  6478. + <layout class="QHBoxLayout" name="horizontalLayout_3">
  6479. + <item>
  6480. + <spacer name="horizontalSpacer">
  6481. + <property name="orientation">
  6482. + <enum>Qt::Horizontal</enum>
  6483. + </property>
  6484. + <property name="sizeType">
  6485. + <enum>QSizePolicy::Preferred</enum>
  6486. + </property>
  6487. + <property name="sizeHint" stdset="0">
  6488. + <size>
  6489. + <width>30</width>
  6490. + <height>20</height>
  6491. + </size>
  6492. + </property>
  6493. + </spacer>
  6494. + </item>
  6495. + <item>
  6496. + <widget class="QGroupBox" name="group">
  6497. + <property name="title">
  6498. + <string>Searching for:</string>
  6499. + </property>
  6500. + <property name="alignment">
  6501. + <set>Qt::AlignCenter</set>
  6502. + </property>
  6503. + <layout class="QHBoxLayout" name="horizontalLayout_2">
  6504. + <item>
  6505. + <layout class="QVBoxLayout" name="verticalLayout">
  6506. + <item>
  6507. + <widget class="QRadioButton" name="ch_all">
  6508. + <property name="text">
  6509. + <string>All plugins, ignoring cache</string>
  6510. + </property>
  6511. + </widget>
  6512. + </item>
  6513. + <item>
  6514. + <widget class="QRadioButton" name="ch_updated">
  6515. + <property name="text">
  6516. + <string>Updated plugins only</string>
  6517. + </property>
  6518. + </widget>
  6519. + </item>
  6520. + <item>
  6521. + <widget class="QCheckBox" name="ch_invalid">
  6522. + <property name="text">
  6523. + <string>Check previously invalid plugins</string>
  6524. + </property>
  6525. + </widget>
  6526. + </item>
  6527. + </layout>
  6528. + </item>
  6529. + </layout>
  6530. + </widget>
  6531. + </item>
  6532. + <item>
  6533. + <spacer name="horizontalSpacer_3">
  6534. + <property name="orientation">
  6535. + <enum>Qt::Horizontal</enum>
  6536. + </property>
  6537. + <property name="sizeType">
  6538. + <enum>QSizePolicy::Preferred</enum>
  6539. + </property>
  6540. + <property name="sizeHint" stdset="0">
  6541. + <size>
  6542. + <width>20</width>
  6543. + <height>20</height>
  6544. + </size>
  6545. + </property>
  6546. + </spacer>
  6547. + </item>
  6548. + </layout>
  6549. + </item>
  6550. + <item>
  6551. + <spacer name="verticalSpacer">
  6552. + <property name="orientation">
  6553. + <enum>Qt::Vertical</enum>
  6554. + </property>
  6555. + <property name="sizeHint" stdset="0">
  6556. + <size>
  6557. + <width>20</width>
  6558. + <height>6</height>
  6559. + </size>
  6560. + </property>
  6561. + </spacer>
  6562. + </item>
  6563. + <item>
  6564. + <layout class="QHBoxLayout" name="horizontalLayout">
  6565. + <item>
  6566. + <widget class="QProgressBar" name="progressBar">
  6567. + <property name="sizePolicy">
  6568. + <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
  6569. + <horstretch>0</horstretch>
  6570. + <verstretch>0</verstretch>
  6571. + </sizepolicy>
  6572. + </property>
  6573. + <property name="maximum">
  6574. + <number>100</number>
  6575. + </property>
  6576. + <property name="value">
  6577. + <number>0</number>
  6578. + </property>
  6579. + <property name="format">
  6580. + <string>Press 'Scan' to begin the search</string>
  6581. + </property>
  6582. + </widget>
  6583. + </item>
  6584. + <item>
  6585. + <widget class="QPushButton" name="b_start">
  6586. + <property name="text">
  6587. + <string>Scan</string>
  6588. + </property>
  6589. + </widget>
  6590. + </item>
  6591. + <item>
  6592. + <widget class="QPushButton" name="b_skip">
  6593. + <property name="text">
  6594. + <string>&gt;&gt; Skip</string>
  6595. + </property>
  6596. + </widget>
  6597. + </item>
  6598. + <item>
  6599. + <widget class="QPushButton" name="b_close">
  6600. + <property name="text">
  6601. + <string>Close</string>
  6602. + </property>
  6603. + </widget>
  6604. + </item>
  6605. + </layout>
  6606. + </item>
  6607. + </layout>
  6608. + </widget>
  6609. + <connections>
  6610. + <connection>
  6611. + <sender>b_close</sender>
  6612. + <signal>clicked()</signal>
  6613. + <receiver>PluginRefreshDialog</receiver>
  6614. + <slot>close()</slot>
  6615. + <hints>
  6616. + <hint type="sourcelabel">
  6617. + <x>426</x>
  6618. + <y>231</y>
  6619. + </hint>
  6620. + <hint type="destinationlabel">
  6621. + <x>236</x>
  6622. + <y>125</y>
  6623. + </hint>
  6624. + </hints>
  6625. + </connection>
  6626. + <connection>
  6627. + <sender>ch_updated</sender>
  6628. + <signal>toggled(bool)</signal>
  6629. + <receiver>ch_invalid</receiver>
  6630. + <slot>setEnabled(bool)</slot>
  6631. + <hints>
  6632. + <hint type="sourcelabel">
  6633. + <x>436</x>
  6634. + <y>78</y>
  6635. + </hint>
  6636. + <hint type="destinationlabel">
  6637. + <x>436</x>
  6638. + <y>105</y>
  6639. + </hint>
  6640. + </hints>
  6641. + </connection>
  6642. + </connections>
  6643. +</ui>
  6644. diff --git a/plugins/carla/qtutils.cpp b/plugins/carla/qtutils.cpp
  6645. new file mode 100644
  6646. index 000000000..6c7cb250a
  6647. --- /dev/null
  6648. +++ b/plugins/carla/qtutils.cpp
  6649. @@ -0,0 +1,158 @@
  6650. +/*
  6651. + * Carla plugin for OBS
  6652. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  6653. + * SPDX-License-Identifier: GPL-2.0-or-later
  6654. + */
  6655. +
  6656. +#include <QtCore/QTimer>
  6657. +#include <QtCore/QThread>
  6658. +#include <QtWidgets/QApplication>
  6659. +#include <QtWidgets/QFileDialog>
  6660. +#include <QtWidgets/QMessageBox>
  6661. +
  6662. +#include <obs.h>
  6663. +
  6664. +#include "qtutils.h"
  6665. +
  6666. +//-----------------------------------------------------------------------------
  6667. +// open a qt file dialog
  6668. +
  6669. +char *carla_qt_file_dialog(bool save, bool isDir, const char *title,
  6670. + const char *filter)
  6671. +{
  6672. + static QByteArray ret;
  6673. +
  6674. + QWidget *parent = carla_qt_get_main_window();
  6675. + QFileDialog::Options options;
  6676. +
  6677. + if (isDir)
  6678. + options |= QFileDialog::ShowDirsOnly;
  6679. +
  6680. + ret = save ? QFileDialog::getSaveFileName(parent, title, {}, filter,
  6681. + nullptr, options)
  6682. + .toUtf8()
  6683. + : QFileDialog::getOpenFileName(parent, title, {}, filter,
  6684. + nullptr, options)
  6685. + .toUtf8();
  6686. +
  6687. + return ret.data();
  6688. +}
  6689. +
  6690. +//-----------------------------------------------------------------------------
  6691. +// call a function on the main thread
  6692. +
  6693. +void carla_qt_callback_on_main_thread(void (*callback)(void *param),
  6694. + void *param)
  6695. +{
  6696. + if (QThread::currentThread() == qApp->thread()) {
  6697. + callback(param);
  6698. + return;
  6699. + }
  6700. +
  6701. + QTimer *const maintimer = new QTimer;
  6702. + maintimer->moveToThread(qApp->thread());
  6703. + maintimer->setSingleShot(true);
  6704. + QObject::connect(maintimer, &QTimer::timeout,
  6705. + [maintimer, callback, param]() {
  6706. + callback(param);
  6707. + maintimer->deleteLater();
  6708. + });
  6709. + QMetaObject::invokeMethod(maintimer, "start", Qt::QueuedConnection,
  6710. + Q_ARG(int, 0));
  6711. +}
  6712. +
  6713. +//-----------------------------------------------------------------------------
  6714. +// get the top-level qt main window
  6715. +
  6716. +QMainWindow *carla_qt_get_main_window(void)
  6717. +{
  6718. + for (QWidget *w : QApplication::topLevelWidgets()) {
  6719. + if (QMainWindow *mw = qobject_cast<QMainWindow *>(w))
  6720. + return mw;
  6721. + }
  6722. +
  6723. + return nullptr;
  6724. +}
  6725. +
  6726. +//-----------------------------------------------------------------------------
  6727. +// show an error dialog (on main thread and without blocking current scope)
  6728. +
  6729. +static void carla_show_error_dialog_later(void *const param)
  6730. +{
  6731. + char **const texts = static_cast<char **>(param);
  6732. + carla_show_error_dialog(texts[0], texts[1]);
  6733. + bfree(texts[0]);
  6734. + bfree(texts[1]);
  6735. + bfree(texts);
  6736. +}
  6737. +
  6738. +void carla_show_error_dialog(const char *const text1, const char *const text2)
  6739. +{
  6740. + // there is no point showing incomplete error messages
  6741. + if (text1 == nullptr || text2 == nullptr)
  6742. + return;
  6743. +
  6744. + // we cannot do Qt gui stuff outside the main thread
  6745. + // do a little dance so we call ourselves later on the main thread
  6746. + if (QThread::currentThread() != qApp->thread()) {
  6747. + char **const texts =
  6748. + static_cast<char **>(bmalloc(sizeof(char *) * 2));
  6749. + texts[0] = bstrdup(text1);
  6750. + texts[1] = bstrdup(text2);
  6751. + carla_qt_callback_on_main_thread(carla_show_error_dialog_later,
  6752. + texts);
  6753. + return;
  6754. + }
  6755. +
  6756. + QMessageBox *const box = new QMessageBox(carla_qt_get_main_window());
  6757. + box->setWindowTitle("Error");
  6758. + box->setText(QString("%1: %2").arg(text1).arg(text2));
  6759. + QObject::connect(box, &QDialog::finished, box, &QWidget::deleteLater);
  6760. + QMetaObject::invokeMethod(box, "show", Qt::QueuedConnection);
  6761. +}
  6762. +
  6763. +//-----------------------------------------------------------------------------
  6764. +
  6765. +#if QT_VERSION >= 0x60000
  6766. +static const auto q_meta_bool = QMetaType(QMetaType::Bool);
  6767. +static const auto q_meta_bytearray = QMetaType(QMetaType::QByteArray);
  6768. +static const auto q_meta_string = QMetaType(QMetaType::QString);
  6769. +#else
  6770. +constexpr auto q_meta_bool = QVariant::Bool;
  6771. +constexpr auto q_meta_bytearray = QVariant::ByteArray;
  6772. +constexpr auto q_meta_string = QVariant::String;
  6773. +#endif
  6774. +
  6775. +bool QSafeSettings::valueBool(const QString &key, const bool defaultValue) const
  6776. +{
  6777. + QVariant var(value(key, defaultValue));
  6778. +
  6779. + if (!var.isNull() && var.convert(q_meta_bool) && var.isValid())
  6780. + return var.toBool();
  6781. +
  6782. + return defaultValue;
  6783. +}
  6784. +
  6785. +QString QSafeSettings::valueString(const QString &key,
  6786. + const QString &defaultValue) const
  6787. +{
  6788. + QVariant var(value(key, defaultValue));
  6789. +
  6790. + if (!var.isNull() && var.convert(q_meta_string) && var.isValid())
  6791. + return var.toString();
  6792. +
  6793. + return defaultValue;
  6794. +}
  6795. +
  6796. +QByteArray QSafeSettings::valueByteArray(const QString &key,
  6797. + const QByteArray defaultValue) const
  6798. +{
  6799. + QVariant var(value(key, defaultValue));
  6800. +
  6801. + if (!var.isNull() && var.convert(q_meta_bytearray) && var.isValid())
  6802. + return var.toByteArray();
  6803. +
  6804. + return defaultValue;
  6805. +}
  6806. +
  6807. +//-----------------------------------------------------------------------------
  6808. diff --git a/plugins/carla/qtutils.h b/plugins/carla/qtutils.h
  6809. new file mode 100644
  6810. index 000000000..4120ce387
  6811. --- /dev/null
  6812. +++ b/plugins/carla/qtutils.h
  6813. @@ -0,0 +1,135 @@
  6814. +/*
  6815. + * Carla plugin for OBS
  6816. + * Copyright (C) 2023 Filipe Coelho <falktx@falktx.com>
  6817. + * SPDX-License-Identifier: GPL-2.0-or-later
  6818. + */
  6819. +
  6820. +#pragma once
  6821. +
  6822. +//-----------------------------------------------------------------------------
  6823. +
  6824. +#ifdef __cplusplus
  6825. +#include <cstdint>
  6826. +#include <QtCore/QSettings>
  6827. +#include <QtWidgets/QMainWindow>
  6828. +extern "C" {
  6829. +#else
  6830. +#include <stdint.h>
  6831. +typedef struct QMainWindow QMainWindow;
  6832. +#endif
  6833. +
  6834. +//-----------------------------------------------------------------------------
  6835. +
  6836. +typedef struct {
  6837. + uint build;
  6838. + uint type;
  6839. + const char *filename;
  6840. + const char *label;
  6841. + uint64_t uniqueId;
  6842. +} PluginListDialogResults;
  6843. +
  6844. +const PluginListDialogResults *carla_exec_plugin_list_dialog();
  6845. +
  6846. +//-----------------------------------------------------------------------------
  6847. +// open a qt file dialog
  6848. +
  6849. +char *carla_qt_file_dialog(bool save, bool isDir, const char *title,
  6850. + const char *filter);
  6851. +
  6852. +//-----------------------------------------------------------------------------
  6853. +// call a function on the main thread
  6854. +
  6855. +void carla_qt_callback_on_main_thread(void (*callback)(void *param),
  6856. + void *param);
  6857. +
  6858. +//-----------------------------------------------------------------------------
  6859. +// get the top-level qt main window
  6860. +
  6861. +QMainWindow *carla_qt_get_main_window(void);
  6862. +
  6863. +//-----------------------------------------------------------------------------
  6864. +// show an error dialog (on main thread and without blocking current scope)
  6865. +
  6866. +void carla_show_error_dialog(const char *text1, const char *text2);
  6867. +
  6868. +//-----------------------------------------------------------------------------
  6869. +
  6870. +#ifdef __cplusplus
  6871. +} // extern "C"
  6872. +
  6873. +//-----------------------------------------------------------------------------
  6874. +// Safer QSettings class, which does not throw if type mismatches
  6875. +
  6876. +class QSafeSettings : public QSettings {
  6877. +public:
  6878. + inline QSafeSettings() : QSettings("obs-studio", "obs") {}
  6879. +
  6880. + bool valueBool(const QString &key, bool defaultValue) const;
  6881. + QString valueString(const QString &key,
  6882. + const QString &defaultValue) const;
  6883. + QByteArray valueByteArray(const QString &key,
  6884. + QByteArray defaultValue = {}) const;
  6885. +};
  6886. +
  6887. +//-----------------------------------------------------------------------------
  6888. +// Custom QString class with default utf-8 mode and a few extra methods
  6889. +
  6890. +class QUtf8String : public QString {
  6891. +public:
  6892. + explicit inline QUtf8String() : QString() {}
  6893. +
  6894. + explicit inline QUtf8String(const char *const str)
  6895. + : QString(fromUtf8(str))
  6896. + {
  6897. + }
  6898. +
  6899. + inline QUtf8String(const QString &s) : QString(s) {}
  6900. +
  6901. + inline bool isNotEmpty() const { return !isEmpty(); }
  6902. +
  6903. + inline QUtf8String &operator=(const char *const str)
  6904. + {
  6905. + return (*this = fromUtf8(str));
  6906. + }
  6907. +
  6908. + inline QUtf8String strip() const { return simplified().remove(' '); }
  6909. +
  6910. +#if QT_VERSION < 0x60000
  6911. + explicit inline QUtf8String(const QChar *const str, const size_t size)
  6912. + : QString(str, size)
  6913. + {
  6914. + }
  6915. +
  6916. + inline QUtf8String sliced(const size_t pos) const
  6917. + {
  6918. + return QUtf8String(data() + pos, size() - pos);
  6919. + }
  6920. +#endif
  6921. +};
  6922. +
  6923. +//-----------------------------------------------------------------------------
  6924. +// Custom QByteArray class with a few extra methods for Qt5 compat
  6925. +
  6926. +#if QT_VERSION < 0x60000
  6927. +class QCompatByteArray : public QByteArray {
  6928. +public:
  6929. + explicit inline QCompatByteArray() : QByteArray() {}
  6930. +
  6931. + explicit inline QCompatByteArray(const char *const data,
  6932. + const size_t size)
  6933. + : QByteArray(data, size)
  6934. + {
  6935. + }
  6936. +
  6937. + inline QCompatByteArray(const QByteArray &b) : QByteArray(b) {}
  6938. +
  6939. + inline QCompatByteArray sliced(const size_t pos) const
  6940. + {
  6941. + return QCompatByteArray(data() + pos, size() - pos);
  6942. + }
  6943. +};
  6944. +#else
  6945. +typedef QByteArray QCompatByteArray;
  6946. +#endif
  6947. +
  6948. +#endif // __cplusplus