AudioMixer.cpp 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  1. /******************************************************************************
  2. Copyright (C) 2025 by Taylor Giampaolo <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "AudioMixer.hpp"
  15. #include <components/MenuCheckBox.hpp>
  16. #include <dialogs/NameDialog.hpp>
  17. #include <utility/item-widget-helpers.hpp>
  18. #include <widgets/OBSBasic.hpp>
  19. #include <Idian/Utils.hpp>
  20. #include <QAction>
  21. #include <QCheckBox>
  22. #include <QHBoxLayout>
  23. #include <QMenu>
  24. #include <QPointer>
  25. #include <QPushButton>
  26. #include <QScrollArea>
  27. #include <QStackedWidget>
  28. #include <QTimer>
  29. #include <QToolBar>
  30. #include <QVBoxLayout>
  31. #include <QWidgetAction>
  32. #include "moc_AudioMixer.cpp"
  33. constexpr int GLOBAL_SOURCE_TOTAL = 6;
  34. namespace {
  35. bool isHiddenInMixer(obs_source_t *source)
  36. {
  37. OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
  38. bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden");
  39. return hidden;
  40. }
  41. bool isPinnedInMixer(obs_source_t *source)
  42. {
  43. OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
  44. bool hidden = obs_data_get_bool(priv_settings, "mixer_pinned");
  45. return hidden;
  46. }
  47. bool isSourceAudioActive(obs_source_t *source)
  48. {
  49. bool active = obs_source_active(source) && obs_source_audio_active(source);
  50. return active;
  51. }
  52. bool isVolumeLocked(obs_source_t *source)
  53. {
  54. OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
  55. bool lock = obs_data_get_bool(priv_settings, "volume_locked");
  56. return lock;
  57. }
  58. } // namespace
  59. AudioMixer::AudioMixer(QWidget *parent) : QFrame(parent)
  60. {
  61. mixerVertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolumeControl");
  62. showInactive = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowInactive");
  63. keepInactiveLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepInactiveLast");
  64. showHidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowHidden");
  65. keepHiddenLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepHiddenLast");
  66. mainLayout = new QVBoxLayout(this);
  67. mainLayout->setContentsMargins(0, 0, 0, 0);
  68. mainLayout->setSpacing(0);
  69. setLayout(mainLayout);
  70. setFrameShape(QFrame::NoFrame);
  71. setLineWidth(0);
  72. stackedMixerArea = new QStackedWidget(this);
  73. stackedMixerArea->setObjectName("stackedMixerArea");
  74. // Horizontal Widgets
  75. hMixerScrollArea = new QScrollArea(this);
  76. hMixerScrollArea->setObjectName("hMixerScrollArea");
  77. hMixerScrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  78. hMixerScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
  79. hMixerScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  80. hMixerScrollArea->setWidgetResizable(true);
  81. hMixerScrollArea->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
  82. hVolumeWidgets = new QWidget(this);
  83. hVolumeWidgets->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
  84. hVolumeWidgets->setObjectName("hVolumeWidgets");
  85. hVolumeControlLayout = new QVBoxLayout(hVolumeWidgets);
  86. hVolumeWidgets->setLayout(hVolumeControlLayout);
  87. hVolumeControlLayout->setContentsMargins(0, 0, 0, 0);
  88. hVolumeControlLayout->setSpacing(0);
  89. hMixerScrollArea->setWidget(hVolumeWidgets);
  90. // Vertical Widgets
  91. vMixerScrollArea = new QScrollArea(this);
  92. vMixerScrollArea->setObjectName("vMixerScrollArea");
  93. vMixerScrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  94. vMixerScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  95. vMixerScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
  96. vMixerScrollArea->setWidgetResizable(true);
  97. vMixerScrollArea->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
  98. vVolumeWidgets = new QWidget(this);
  99. vVolumeWidgets->setObjectName("vVolumeWidgets");
  100. vVolumeControlLayout = new QHBoxLayout(vVolumeWidgets);
  101. vVolumeWidgets->setLayout(vVolumeControlLayout);
  102. vVolumeControlLayout->setContentsMargins(0, 0, 0, 0);
  103. vVolumeControlLayout->setSpacing(0);
  104. vVolumeControlLayout->setAlignment(Qt::AlignLeft);
  105. vMixerScrollArea->setWidget(vVolumeWidgets);
  106. stackedMixerArea->addWidget(hMixerScrollArea);
  107. stackedMixerArea->addWidget(vMixerScrollArea);
  108. mixerToolbar = new QToolBar(this);
  109. mixerToolbar->setIconSize(QSize(16, 16));
  110. mixerToolbar->setFloatable(false);
  111. mainLayout->addWidget(stackedMixerArea);
  112. mainLayout->addWidget(mixerToolbar);
  113. advAudio = new QAction(this);
  114. advAudio->setText(QTStr("Basic.AdvAudio"));
  115. advAudio->setToolTip(QTStr("Basic.AdvAudio"));
  116. QIcon advIcon;
  117. advIcon.addFile(QString::fromUtf8(":/settings/images/settings/advanced.svg"), QSize(16, 16),
  118. QIcon::Mode::Normal, QIcon::State::Off);
  119. advAudio->setIcon(advIcon);
  120. advAudio->setObjectName("actionMixerToolbarAdvAudio");
  121. layoutButton = new QAction(this);
  122. layoutButton->setText("");
  123. layoutButton->setToolTip(QTStr("Basic.AudioMixer.Layout.Vertical"));
  124. QIcon layoutIcon;
  125. layoutIcon.addFile(QString::fromUtf8(":/res/images/layout-vertical.svg"), QSize(16, 16), QIcon::Mode::Normal,
  126. QIcon::State::Off);
  127. layoutButton->setIcon(layoutIcon);
  128. layoutButton->setObjectName("actionMixerToolbarToggleLayout");
  129. QWidget *spacer = new QWidget(mixerToolbar);
  130. spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
  131. optionsButton = new QPushButton(mixerToolbar);
  132. optionsButton->setText(QTStr("Basic.AudioMixer.Options"));
  133. idian::Utils::addClass(optionsButton, "toolbar-button");
  134. idian::Utils::addClass(optionsButton, "text-bold");
  135. createMixerContextMenu();
  136. optionsButton->setMenu(mixerMenu);
  137. toggleHiddenButton = new QPushButton(mixerToolbar);
  138. toggleHiddenButton->setCheckable(true);
  139. toggleHiddenButton->setChecked(showHidden);
  140. toggleHiddenButton->setText(QTStr("Basic.AudioMixer.HiddenTotal").arg(0));
  141. toggleHiddenButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
  142. QIcon hiddenIcon;
  143. hiddenIcon.addFile(QString::fromUtf8(":/res/images/hidden.svg"), QSize(16, 16), QIcon::Mode::Normal,
  144. QIcon::State::Off);
  145. toggleHiddenButton->setIcon(hiddenIcon);
  146. idian::Utils::addClass(toggleHiddenButton, "toolbar-button");
  147. idian::Utils::addClass(toggleHiddenButton, "toggle-hidden");
  148. mixerToolbar->addWidget(toggleHiddenButton);
  149. mixerToolbar->addSeparator();
  150. mixerToolbar->addWidget(spacer);
  151. mixerToolbar->addSeparator();
  152. mixerToolbar->addAction(layoutButton);
  153. mixerToolbar->addSeparator();
  154. mixerToolbar->addAction(advAudio);
  155. mixerToolbar->addSeparator();
  156. mixerToolbar->addWidget(optionsButton);
  157. // Setting this property on the QAction itself does not seem to work despite
  158. // the UI files doing exactly that, so we set it on the action widget directly
  159. QWidget *advAudioWidget = mixerToolbar->widgetForAction(advAudio);
  160. idian::Utils::addClass(advAudioWidget, "icon-cogs");
  161. // Connect to OBS signals
  162. signalHandlers.reserve(signalHandlers.size() + 8);
  163. signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", AudioMixer::obsSourceCreate, this);
  164. signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", AudioMixer::obsSourceRemove, this);
  165. signalHandlers.emplace_back(obs_get_signal_handler(), "source_destroy", AudioMixer::obsSourceRemove, this);
  166. signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", AudioMixer::obsSourceRename, this);
  167. signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", AudioMixer::obsSourceActivated, this);
  168. signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", AudioMixer::obsSourceDeactivated,
  169. this);
  170. signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate",
  171. AudioMixer::obsSourceAudioActivated, this);
  172. signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate",
  173. AudioMixer::obsSourceAudioDeactivated, this);
  174. obs_frontend_add_event_callback(AudioMixer::onFrontendEvent, this);
  175. // Connect to Qt signals
  176. connect(hMixerScrollArea, &QScrollArea::customContextMenuRequested, this,
  177. &AudioMixer::mixerContextMenuRequested);
  178. connect(vMixerScrollArea, &QScrollArea::customContextMenuRequested, this,
  179. &AudioMixer::mixerContextMenuRequested);
  180. connect(&updateTimer, &QTimer::timeout, this, &AudioMixer::updateVolumeLayouts);
  181. updateTimer.setSingleShot(true);
  182. OBSBasic *main = OBSBasic::Get();
  183. if (main) {
  184. connect(main, &OBSBasic::userSettingChanged, this,
  185. [this](const std::string &category, const std::string &name) {
  186. if (category == "BasicWindow" && name == "VerticalVolumeControl") {
  187. updateLayout();
  188. } else if (category == "BasicWindow" && name == "MixerShowInactive") {
  189. updateShowInactive();
  190. } else if (category == "BasicWindow" && name == "MixerKeepInactiveLast") {
  191. updateKeepInactiveLast();
  192. } else if (category == "BasicWindow" && name == "MixerShowHidden") {
  193. updateShowHidden();
  194. } else if (category == "BasicWindow" && name == "MixerKeepHiddenLast") {
  195. updateKeepHiddenLast();
  196. } else if (category == "BasicWindow" && name == "ShowListboxToolbars") {
  197. updateShowToolbar();
  198. } else if (category == "Accessibility" && name == "SettingsChanged") {
  199. refreshVolumeColors();
  200. }
  201. });
  202. connect(main, &OBSBasic::mixerStatusChanged, this, &AudioMixer::queueLayoutUpdate);
  203. connect(advAudio, &QAction::triggered, main, &OBSBasic::on_actionAdvAudioProperties_triggered,
  204. Qt::DirectConnection);
  205. connect(toggleHiddenButton, &QPushButton::clicked, this, &AudioMixer::toggleShowHidden);
  206. connect(layoutButton, &QAction::triggered, main, &OBSBasic::toggleMixerLayout);
  207. }
  208. updateShowToolbar();
  209. updatePreviewSources();
  210. updateGlobalSources();
  211. reloadVolumeControls();
  212. }
  213. AudioMixer::~AudioMixer()
  214. {
  215. signalHandlers.clear();
  216. previewSources.clear();
  217. globalSources.clear();
  218. clearVolumeControls();
  219. obs_frontend_remove_event_callback(AudioMixer::onFrontendEvent, this);
  220. }
  221. void AudioMixer::updateLayout()
  222. {
  223. bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolumeControl");
  224. setMixerLayoutVertical(vertical);
  225. updateVolumeLayouts();
  226. }
  227. void AudioMixer::toggleShowInactive(bool checked)
  228. {
  229. config_set_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowInactive", checked);
  230. OBSBasic *main = OBSBasic::Get();
  231. if (main) {
  232. emit main->userSettingChanged("BasicWindow", "MixerShowInactive");
  233. }
  234. }
  235. void AudioMixer::toggleKeepInactiveLast(bool checked)
  236. {
  237. config_set_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepInactiveLast", checked);
  238. OBSBasic *main = OBSBasic::Get();
  239. if (main) {
  240. emit main->userSettingChanged("BasicWindow", "MixerKeepInactiveLast");
  241. }
  242. }
  243. void AudioMixer::toggleShowHidden(bool checked)
  244. {
  245. config_set_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowHidden", checked);
  246. OBSBasic *main = OBSBasic::Get();
  247. if (main) {
  248. emit main->userSettingChanged("BasicWindow", "MixerShowHidden");
  249. }
  250. }
  251. void AudioMixer::toggleKeepHiddenLast(bool checked)
  252. {
  253. config_set_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepHiddenLast", checked);
  254. OBSBasic *main = OBSBasic::Get();
  255. if (main) {
  256. emit main->userSettingChanged("BasicWindow", "MixerKeepHiddenLast");
  257. }
  258. }
  259. VolumeControl *AudioMixer::createVolumeControl(obs_source_t *source)
  260. {
  261. bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolumeControl");
  262. VolumeControl *control = new VolumeControl(source, this, vertical);
  263. control->setGlobalInMixer(isSourceGlobal(source));
  264. control->setHiddenInMixer(isHiddenInMixer(source));
  265. control->setPinnedInMixer(isPinnedInMixer(source));
  266. control->enableSlider(!isVolumeLocked(source));
  267. OBSBasic *main = OBSBasic::Get();
  268. double meterDecayRate = config_get_double(main->Config(), "Audio", "MeterDecayRate");
  269. control->setMeterDecayRate(meterDecayRate);
  270. uint64_t peakMeterTypeIdx = config_get_uint(main->Config(), "Audio", "PeakMeterType");
  271. obs_peak_meter_type peakMeterType;
  272. switch (peakMeterTypeIdx) {
  273. case 0:
  274. peakMeterType = SAMPLE_PEAK_METER;
  275. break;
  276. case 1:
  277. peakMeterType = TRUE_PEAK_METER;
  278. break;
  279. default:
  280. peakMeterType = SAMPLE_PEAK_METER;
  281. break;
  282. }
  283. control->setPeakMeterType(peakMeterType);
  284. connect(control, &VolumeControl::unhideAll, this, &AudioMixer::unhideAllAudioControls);
  285. return control;
  286. }
  287. void AudioMixer::updateControlVisibility(QString uuid)
  288. {
  289. auto item = volumeList.find(uuid);
  290. if (item == volumeList.end()) {
  291. return;
  292. }
  293. VolumeControl *control = item->second;
  294. bool show = getMixerVisibilityForControl(control);
  295. if (show) {
  296. control->show();
  297. } else {
  298. control->hide();
  299. }
  300. queueLayoutUpdate();
  301. }
  302. void AudioMixer::sourceCreated(QString uuid)
  303. {
  304. addControlForUuid(uuid);
  305. updateGlobalSources();
  306. }
  307. void AudioMixer::sourceRemoved(QString uuid)
  308. {
  309. removeControlForUuid(uuid);
  310. updateGlobalSources();
  311. }
  312. void AudioMixer::updatePreviewSources()
  313. {
  314. bool isStudioMode = obs_frontend_preview_program_mode_active();
  315. clearPreviewSources();
  316. if (isStudioMode) {
  317. OBSSourceAutoRelease previewSource = obs_frontend_get_current_preview_scene();
  318. if (!previewSource) {
  319. return;
  320. }
  321. obs_scene_t *previewScene = obs_scene_from_source(previewSource);
  322. if (!previewScene) {
  323. return;
  324. }
  325. if (!previewScene) {
  326. return;
  327. }
  328. auto getPreviewSources = [this](obs_scene_t *, obs_sceneitem_t *item) {
  329. obs_source_t *source = obs_sceneitem_get_source(item);
  330. if (!source) {
  331. return true;
  332. }
  333. uint32_t flags = obs_source_get_output_flags(source);
  334. if ((flags & OBS_SOURCE_AUDIO) == 0) {
  335. return true;
  336. }
  337. auto uuidPointer = obs_source_get_uuid(source);
  338. if (uuidPointer && *uuidPointer) {
  339. previewSources.insert(QString::fromUtf8(uuidPointer));
  340. }
  341. return true;
  342. };
  343. using getPreviewSources_t = decltype(getPreviewSources);
  344. auto previewEnum = [](obs_scene_t *scene, obs_sceneitem_t *item, void *data) -> bool {
  345. return (*static_cast<getPreviewSources_t *>(data))(scene, item);
  346. };
  347. obs_scene_enum_items(previewScene, previewEnum, &getPreviewSources);
  348. }
  349. }
  350. void AudioMixer::updateGlobalSources()
  351. {
  352. globalSources.clear();
  353. for (int i = 1; i <= GLOBAL_SOURCE_TOTAL; i++) {
  354. OBSSourceAutoRelease source = obs_get_output_source(i);
  355. if (source) {
  356. auto uuidPointer = obs_source_get_uuid(source);
  357. if (uuidPointer && *uuidPointer) {
  358. globalSources.insert(QString::fromUtf8(uuidPointer));
  359. }
  360. }
  361. }
  362. queueLayoutUpdate();
  363. }
  364. QBoxLayout *AudioMixer::activeLayout() const
  365. {
  366. bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolumeControl");
  367. QBoxLayout *layout = vertical ? static_cast<QBoxLayout *>(vVolumeControlLayout)
  368. : static_cast<QBoxLayout *>(hVolumeControlLayout);
  369. return layout;
  370. }
  371. void AudioMixer::reloadVolumeControls()
  372. {
  373. clearVolumeControls();
  374. auto createMixerControls = [](void *param, obs_source_t *source) -> bool {
  375. AudioMixer *mixer = static_cast<AudioMixer *>(param);
  376. uint32_t flags = obs_source_get_output_flags(source);
  377. if ((flags & OBS_SOURCE_AUDIO) == 0) {
  378. return true;
  379. }
  380. auto uuidPointer = obs_source_get_uuid(source);
  381. if (!uuidPointer || !*uuidPointer) {
  382. return true;
  383. }
  384. mixer->addControlForUuid(QString::fromUtf8(uuidPointer));
  385. return true;
  386. };
  387. obs_enum_sources(createMixerControls, this);
  388. queueLayoutUpdate();
  389. }
  390. bool AudioMixer::getMixerVisibilityForControl(VolumeControl *control)
  391. {
  392. bool isPinned = control->mixerStatus().has(VolumeControl::MixerStatus::Pinned);
  393. bool isPreviewed = control->mixerStatus().has(VolumeControl::MixerStatus::Preview);
  394. bool isHidden = control->mixerStatus().has(VolumeControl::MixerStatus::Hidden);
  395. bool isAudioActive = control->mixerStatus().has(VolumeControl::MixerStatus::Active);
  396. if (isPinned) {
  397. return true;
  398. }
  399. if (isHidden && showHidden) {
  400. return true;
  401. }
  402. if (!isAudioActive && showInactive) {
  403. return !isHidden;
  404. }
  405. if (isAudioActive) {
  406. return !isHidden;
  407. }
  408. if (isPreviewed) {
  409. return !isHidden;
  410. }
  411. return false;
  412. }
  413. void AudioMixer::clearPreviewSources()
  414. {
  415. previewSources.clear();
  416. }
  417. bool AudioMixer::isSourcePreviewed(obs_source_t *source)
  418. {
  419. if (!source) {
  420. return false;
  421. }
  422. auto uuidPointer = obs_source_get_uuid(source);
  423. if (!uuidPointer || !*uuidPointer) {
  424. return false;
  425. }
  426. if (previewSources.find(QString::fromUtf8(uuidPointer)) != previewSources.end()) {
  427. return true;
  428. }
  429. return false;
  430. }
  431. bool AudioMixer::isSourceGlobal(obs_source_t *source)
  432. {
  433. if (!source) {
  434. return false;
  435. }
  436. auto uuidPointer = obs_source_get_uuid(source);
  437. if (!uuidPointer || !*uuidPointer) {
  438. return false;
  439. }
  440. if (globalSources.find(QString::fromUtf8(uuidPointer)) != globalSources.end()) {
  441. return true;
  442. }
  443. return false;
  444. }
  445. void AudioMixer::clearVolumeControls()
  446. {
  447. for (const auto &[uuid, control] : volumeList) {
  448. if (control) {
  449. control->deleteLater();
  450. }
  451. }
  452. volumeList.clear();
  453. }
  454. void AudioMixer::refreshVolumeColors()
  455. {
  456. for (const auto &[uuid, control] : volumeList) {
  457. control->refreshColors();
  458. }
  459. }
  460. void AudioMixer::unhideAllAudioControls()
  461. {
  462. for (const auto &[uuid, control] : volumeList) {
  463. control->setHiddenInMixer(false);
  464. }
  465. queueLayoutUpdate();
  466. }
  467. void AudioMixer::queueLayoutUpdate()
  468. {
  469. if (!updateTimer.isActive()) {
  470. updateTimer.start(0);
  471. }
  472. }
  473. void AudioMixer::updateVolumeLayouts()
  474. {
  475. setUpdatesEnabled(false);
  476. hiddenCount = 0;
  477. bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolumeControl");
  478. std::vector<RankedVolume> rankedVolumes;
  479. rankedVolumes.reserve(volumeList.size());
  480. for (const auto &entry : volumeList) {
  481. VolumeControl *control = entry.second;
  482. if (control) {
  483. int sortingWeight = 0;
  484. OBSSource source = OBSGetStrongRef(control->weakSource());
  485. if (!source) {
  486. const char *cachedName = control->getCachedName().toUtf8().constData();
  487. blog(LOG_INFO, "Tried to sort VolumeControl for '%s' but source is null", cachedName);
  488. continue;
  489. }
  490. bool isPreviewed = isSourcePreviewed(source);
  491. bool isGlobal = isSourceGlobal(source);
  492. bool isPinned = isPinnedInMixer(source);
  493. bool isHidden = isHiddenInMixer(source);
  494. bool isAudioActive = isSourceAudioActive(source);
  495. bool isLocked = isVolumeLocked(source);
  496. control->mixerStatus().set(VolumeControl::MixerStatus::Preview, isPreviewed);
  497. control->mixerStatus().set(VolumeControl::MixerStatus::Global, isGlobal);
  498. control->mixerStatus().set(VolumeControl::MixerStatus::Pinned, isPinned);
  499. control->mixerStatus().set(VolumeControl::MixerStatus::Hidden, isHidden);
  500. control->mixerStatus().set(VolumeControl::MixerStatus::Active, isAudioActive);
  501. control->mixerStatus().set(VolumeControl::MixerStatus::Locked, isLocked);
  502. if (isHidden) {
  503. hiddenCount += 1;
  504. }
  505. if (!isGlobal) {
  506. sortingWeight += 20;
  507. }
  508. if (!isPinned) {
  509. sortingWeight += 20;
  510. }
  511. if (isHidden && keepHiddenLast) {
  512. sortingWeight += 20;
  513. if (isPreviewed) {
  514. sortingWeight -= 10;
  515. }
  516. }
  517. if (!isAudioActive && keepInactiveLast) {
  518. sortingWeight += 50;
  519. if (isPreviewed) {
  520. sortingWeight -= 10;
  521. }
  522. }
  523. rankedVolumes.push_back({control, sortingWeight});
  524. }
  525. }
  526. std::sort(rankedVolumes.begin(), rankedVolumes.end(), [](const RankedVolume &a, const RankedVolume &b) {
  527. const QString &nameA = a.control->getCachedName();
  528. const QString &nameB = b.control->getCachedName();
  529. if (a.sortingWeight == b.sortingWeight) {
  530. return nameA.toLower() < nameB.toLower();
  531. }
  532. return a.sortingWeight < b.sortingWeight;
  533. });
  534. VolumeControl *prevControl = nullptr;
  535. int index = 0;
  536. QBoxLayout *layout = activeLayout();
  537. vMixerScrollArea->setWidgetResizable(false);
  538. hMixerScrollArea->setWidgetResizable(false);
  539. for (const auto &entry : rankedVolumes) {
  540. VolumeControl *volControl = entry.control;
  541. if (!volControl) {
  542. continue;
  543. }
  544. layout->insertWidget(index, volControl);
  545. volControl->setVertical(vertical);
  546. volControl->updateName();
  547. volControl->updateMixerState();
  548. bool showControl = getMixerVisibilityForControl(volControl);
  549. if (showControl) {
  550. volControl->show();
  551. } else {
  552. volControl->hide();
  553. }
  554. if (prevControl == nullptr) {
  555. setTabOrder(previousInFocusChain(), volControl->firstWidget());
  556. } else {
  557. setTabOrder(prevControl->lastWidget(), volControl->firstWidget());
  558. }
  559. volControl->updateTabOrder();
  560. prevControl = volControl;
  561. ++index;
  562. }
  563. toggleHiddenButton->setText(QTStr("Basic.AudioMixer.HiddenTotal").arg(hiddenCount));
  564. if (hiddenCount == 0) {
  565. toggleHiddenButton->setDisabled(true);
  566. idian::Utils::toggleClass(toggleHiddenButton, "text-muted", true);
  567. } else {
  568. toggleHiddenButton->setDisabled(false);
  569. idian::Utils::toggleClass(toggleHiddenButton, "text-muted", false);
  570. }
  571. vMixerScrollArea->setWidgetResizable(true);
  572. hMixerScrollArea->setWidgetResizable(true);
  573. setUpdatesEnabled(true);
  574. }
  575. void AudioMixer::mixerContextMenuRequested()
  576. {
  577. showMixerContextMenu();
  578. }
  579. void AudioMixer::setMixerLayoutVertical(bool vertical)
  580. {
  581. mixerVertical = vertical;
  582. if (vertical) {
  583. stackedMixerArea->setMinimumSize(180, 220);
  584. stackedMixerArea->setCurrentIndex(1);
  585. QIcon layoutIcon;
  586. layoutIcon.addFile(QString::fromUtf8(":/res/images/layout-horizontal.svg"), QSize(16, 16),
  587. QIcon::Mode::Normal, QIcon::State::Off);
  588. layoutButton->setIcon(layoutIcon);
  589. layoutButton->setToolTip(QTStr("Basic.AudioMixer.Layout.Horizontal"));
  590. } else {
  591. stackedMixerArea->setMinimumSize(220, 0);
  592. stackedMixerArea->setCurrentIndex(0);
  593. QIcon layoutIcon;
  594. layoutIcon.addFile(QString::fromUtf8(":/res/images/layout-vertical.svg"), QSize(16, 16),
  595. QIcon::Mode::Normal, QIcon::State::Off);
  596. layoutButton->setIcon(layoutIcon);
  597. layoutButton->setToolTip(QTStr("Basic.AudioMixer.Layout.Vertical"));
  598. }
  599. // Qt caches the size of QWidgetActions so this is the simplest way to update the text
  600. // of the checkboxes in the menu and make sure the menu size is recalculated.
  601. createMixerContextMenu();
  602. QWidget *buttonWidget = mixerToolbar->widgetForAction(layoutButton);
  603. if (buttonWidget) {
  604. idian::Utils::toggleClass(buttonWidget, "icon-layout-horizontal", vertical);
  605. idian::Utils::toggleClass(buttonWidget, "icon-layout-vertical", !vertical);
  606. }
  607. }
  608. void AudioMixer::createMixerContextMenu()
  609. {
  610. if (mixerMenu) {
  611. mixerMenu->deleteLater();
  612. }
  613. mixerMenu = new QMenu(this);
  614. // Create menu actions
  615. QAction *unhideAllAction = new QAction(QTStr("UnhideAll"), mixerMenu);
  616. showHiddenCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowHidden"), mixerMenu);
  617. QWidgetAction *showHiddenAction = new QWidgetAction(mixerMenu);
  618. showHiddenCheckBox->setAction(showHiddenAction);
  619. showHiddenCheckBox->setChecked(showHidden);
  620. showHiddenAction->setDefaultWidget(showHiddenCheckBox);
  621. QWidgetAction *showInactiveAction = new QWidgetAction(mixerMenu);
  622. MenuCheckBox *showInactiveCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowInactive"), mixerMenu);
  623. showInactiveCheckBox->setAction(showInactiveAction);
  624. showInactiveCheckBox->setChecked(showInactive);
  625. showInactiveAction->setDefaultWidget(showInactiveCheckBox);
  626. QWidgetAction *hiddenLastAction = new QWidgetAction(mixerMenu);
  627. const char *hiddenLastString = mixerVertical ? "Basic.AudioMixer.KeepHiddenRight"
  628. : "Basic.AudioMixer.KeepHiddenBottom";
  629. MenuCheckBox *hiddenLastCheckBox = new MenuCheckBox(QTStr(hiddenLastString), mixerMenu);
  630. hiddenLastCheckBox->setAction(hiddenLastAction);
  631. hiddenLastCheckBox->setChecked(keepHiddenLast);
  632. hiddenLastAction->setDefaultWidget(hiddenLastCheckBox);
  633. QWidgetAction *inactiveLastAction = new QWidgetAction(mixerMenu);
  634. const char *inactiveLastString = mixerVertical ? "Basic.AudioMixer.KeepInactiveRight"
  635. : "Basic.AudioMixer.KeepInactiveBottom";
  636. MenuCheckBox *inactiveLastCheckBox = new MenuCheckBox(QTStr(inactiveLastString), mixerMenu);
  637. inactiveLastCheckBox->setAction(inactiveLastAction);
  638. inactiveLastCheckBox->setChecked(keepInactiveLast);
  639. inactiveLastAction->setDefaultWidget(inactiveLastCheckBox);
  640. // Connect menu actions
  641. connect(unhideAllAction, &QAction::triggered, this, &AudioMixer::unhideAllAudioControls, Qt::DirectConnection);
  642. connect(showHiddenCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleShowHidden, Qt::DirectConnection);
  643. connect(hiddenLastCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleKeepHiddenLast, Qt::DirectConnection);
  644. connect(showInactiveCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleShowInactive, Qt::DirectConnection);
  645. connect(inactiveLastCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleKeepInactiveLast,
  646. Qt::DirectConnection);
  647. // Build menu and show
  648. mixerMenu->addAction(unhideAllAction);
  649. mixerMenu->addSeparator();
  650. mixerMenu->addAction(showHiddenAction);
  651. mixerMenu->addAction(showInactiveAction);
  652. mixerMenu->addAction(hiddenLastAction);
  653. mixerMenu->addAction(inactiveLastAction);
  654. optionsButton->setMenu(mixerMenu);
  655. }
  656. void AudioMixer::showMixerContextMenu()
  657. {
  658. createMixerContextMenu();
  659. mixerMenu->popup(QCursor::pos());
  660. }
  661. void AudioMixer::addControlForUuid(QString uuid)
  662. {
  663. OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.toUtf8().constData());
  664. QPointer<VolumeControl> newControl = createVolumeControl(source);
  665. volumeList.insert({uuid, newControl});
  666. queueLayoutUpdate();
  667. }
  668. void AudioMixer::removeControlForUuid(QString uuid)
  669. {
  670. auto item = volumeList.find(uuid);
  671. if (item != volumeList.end()) {
  672. VolumeControl *widget = item->second;
  673. if (widget) {
  674. activeLayout()->removeWidget(widget);
  675. widget->deleteLater();
  676. }
  677. volumeList.erase(item);
  678. }
  679. previewSources.erase(uuid);
  680. globalSources.erase(uuid);
  681. }
  682. void AudioMixer::onFrontendEvent(obs_frontend_event event, void *data)
  683. {
  684. AudioMixer *mixer = static_cast<AudioMixer *>(data);
  685. mixer->handleFrontendEvent(event);
  686. }
  687. void AudioMixer::handleFrontendEvent(obs_frontend_event event)
  688. {
  689. switch (event) {
  690. case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
  691. updatePreviewSources();
  692. queueLayoutUpdate();
  693. break;
  694. case OBS_FRONTEND_EVENT_EXIT:
  695. obs_frontend_remove_event_callback(AudioMixer::onFrontendEvent, this);
  696. break;
  697. default:
  698. break;
  699. }
  700. }
  701. void AudioMixer::updateShowInactive()
  702. {
  703. bool settingShowInactive = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowInactive");
  704. if (showInactive == settingShowInactive) {
  705. return;
  706. }
  707. showInactive = settingShowInactive;
  708. queueLayoutUpdate();
  709. }
  710. void AudioMixer::updateKeepInactiveLast()
  711. {
  712. bool settingKeepInactiveLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepInactiveLast");
  713. if (keepInactiveLast == settingKeepInactiveLast) {
  714. return;
  715. }
  716. keepInactiveLast = settingKeepInactiveLast;
  717. queueLayoutUpdate();
  718. }
  719. void AudioMixer::updateShowHidden()
  720. {
  721. bool settingShowHidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowHidden");
  722. if (showHidden == settingShowHidden) {
  723. return;
  724. }
  725. showHidden = settingShowHidden;
  726. toggleHiddenButton->setText(QTStr("Basic.AudioMixer.HiddenTotal").arg(hiddenCount));
  727. toggleHiddenButton->setChecked(showHidden);
  728. showHiddenCheckBox->setChecked(showHidden);
  729. queueLayoutUpdate();
  730. }
  731. void AudioMixer::updateKeepHiddenLast()
  732. {
  733. bool settingKeepHiddenLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepHiddenLast");
  734. if (keepHiddenLast == settingKeepHiddenLast) {
  735. return;
  736. }
  737. keepHiddenLast = settingKeepHiddenLast;
  738. queueLayoutUpdate();
  739. }
  740. void AudioMixer::updateShowToolbar()
  741. {
  742. bool settingShowToolbar = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars");
  743. if (showToolbar == settingShowToolbar) {
  744. return;
  745. }
  746. showToolbar = settingShowToolbar;
  747. showToolbar ? mixerToolbar->show() : mixerToolbar->hide();
  748. }
  749. void AudioMixer::obsSourceActivated(void *data, calldata_t *params)
  750. {
  751. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  752. uint32_t flags = obs_source_get_output_flags(source);
  753. if (flags & OBS_SOURCE_AUDIO) {
  754. auto uuidPointer = obs_source_get_uuid(source);
  755. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  756. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  757. }
  758. }
  759. void AudioMixer::obsSourceDeactivated(void *data, calldata_t *params)
  760. {
  761. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  762. uint32_t flags = obs_source_get_output_flags(source);
  763. if (flags & OBS_SOURCE_AUDIO) {
  764. auto uuidPointer = obs_source_get_uuid(source);
  765. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  766. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  767. }
  768. }
  769. void AudioMixer::obsSourceAudioActivated(void *data, calldata_t *params)
  770. {
  771. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  772. if (obs_source_active(source)) {
  773. auto uuidPointer = obs_source_get_uuid(source);
  774. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  775. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  776. }
  777. }
  778. void AudioMixer::obsSourceAudioDeactivated(void *data, calldata_t *params)
  779. {
  780. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  781. auto uuidPointer = obs_source_get_uuid(source);
  782. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility", Qt::QueuedConnection,
  783. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  784. }
  785. void AudioMixer::obsSourceCreate(void *data, calldata_t *params)
  786. {
  787. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  788. uint32_t flags = obs_source_get_output_flags(source);
  789. if (flags & OBS_SOURCE_AUDIO) {
  790. auto uuidPointer = obs_source_get_uuid(source);
  791. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "sourceCreated", Qt::QueuedConnection,
  792. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  793. }
  794. }
  795. void AudioMixer::obsSourceRemove(void *data, calldata_t *params)
  796. {
  797. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  798. uint32_t flags = obs_source_get_output_flags(source);
  799. if (flags & OBS_SOURCE_AUDIO) {
  800. auto uuidPointer = obs_source_get_uuid(source);
  801. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "sourceRemoved", Qt::QueuedConnection,
  802. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  803. }
  804. }
  805. void AudioMixer::obsSourceRename(void *data, calldata_t *)
  806. {
  807. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "queueLayoutUpdate", Qt::QueuedConnection);
  808. }