1
0

AudioMixer.cpp 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  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("AdvAudioProps"));
  115. advAudio->setToolTip(QTStr("AdvAudioProps"));
  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("");
  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("%1 hidden").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. } else {
  590. stackedMixerArea->setMinimumSize(220, 0);
  591. stackedMixerArea->setCurrentIndex(0);
  592. QIcon layoutIcon;
  593. layoutIcon.addFile(QString::fromUtf8(":/res/images/layout-vertical.svg"), QSize(16, 16),
  594. QIcon::Mode::Normal, QIcon::State::Off);
  595. layoutButton->setIcon(layoutIcon);
  596. }
  597. QWidget *buttonWidget = mixerToolbar->widgetForAction(layoutButton);
  598. if (buttonWidget) {
  599. idian::Utils::toggleClass(buttonWidget, "icon-layout-horizontal", vertical);
  600. idian::Utils::toggleClass(buttonWidget, "icon-layout-vertical", !vertical);
  601. }
  602. }
  603. void AudioMixer::createMixerContextMenu()
  604. {
  605. if (mixerMenu) {
  606. mixerMenu->deleteLater();
  607. }
  608. mixerMenu = new QMenu(this);
  609. // Create menu actions
  610. QAction *unhideAllAction = new QAction(QTStr("UnhideAll"), mixerMenu);
  611. showHiddenCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowHidden"), mixerMenu);
  612. showHiddenAction = new QWidgetAction(mixerMenu);
  613. showHiddenCheckBox->setAction(showHiddenAction);
  614. showHiddenCheckBox->setChecked(showHidden);
  615. showHiddenAction->setDefaultWidget(showHiddenCheckBox);
  616. QWidgetAction *showInactiveAction = new QWidgetAction(mixerMenu);
  617. MenuCheckBox *showInactiveCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowInactive"), mixerMenu);
  618. showInactiveCheckBox->setAction(showInactiveAction);
  619. showInactiveCheckBox->setChecked(showInactive);
  620. showInactiveAction->setDefaultWidget(showInactiveCheckBox);
  621. QWidgetAction *hiddenLastAction = new QWidgetAction(mixerMenu);
  622. const char *hiddenShifted = mixerVertical ? "Basic.AudioMixer.KeepHiddenRight"
  623. : "Basic.AudioMixer.KeepHiddenBottom";
  624. MenuCheckBox *hiddenLastCheckBox = new MenuCheckBox(QTStr(hiddenShifted), mixerMenu);
  625. hiddenLastCheckBox->setAction(hiddenLastAction);
  626. hiddenLastCheckBox->setChecked(keepHiddenLast);
  627. hiddenLastAction->setDefaultWidget(hiddenLastCheckBox);
  628. QWidgetAction *inactiveLastAction = new QWidgetAction(mixerMenu);
  629. const char *inactiveShifted = mixerVertical ? "Basic.AudioMixer.KeepInactiveRight"
  630. : "Basic.AudioMixer.KeepInactiveBottom";
  631. MenuCheckBox *inactiveLastCheckBox = new MenuCheckBox(QTStr(inactiveShifted), mixerMenu);
  632. inactiveLastCheckBox->setAction(inactiveLastAction);
  633. inactiveLastCheckBox->setChecked(keepInactiveLast);
  634. inactiveLastAction->setDefaultWidget(inactiveLastCheckBox);
  635. // Connect menu actions
  636. connect(unhideAllAction, &QAction::triggered, this, &AudioMixer::unhideAllAudioControls, Qt::DirectConnection);
  637. connect(showHiddenCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleShowHidden, Qt::DirectConnection);
  638. connect(hiddenLastCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleKeepHiddenLast, Qt::DirectConnection);
  639. connect(showInactiveCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleShowInactive, Qt::DirectConnection);
  640. connect(inactiveLastCheckBox, &QCheckBox::toggled, this, &AudioMixer::toggleKeepInactiveLast,
  641. Qt::DirectConnection);
  642. // Build menu and show
  643. mixerMenu->addAction(unhideAllAction);
  644. mixerMenu->addSeparator();
  645. mixerMenu->addAction(showHiddenAction);
  646. mixerMenu->addAction(showInactiveAction);
  647. mixerMenu->addAction(hiddenLastAction);
  648. mixerMenu->addAction(inactiveLastAction);
  649. optionsButton->setMenu(mixerMenu);
  650. }
  651. void AudioMixer::showMixerContextMenu()
  652. {
  653. createMixerContextMenu();
  654. mixerMenu->popup(QCursor::pos());
  655. }
  656. void AudioMixer::addControlForUuid(QString uuid)
  657. {
  658. OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.toUtf8().constData());
  659. QPointer<VolumeControl> newControl = createVolumeControl(source);
  660. volumeList.insert({uuid, newControl});
  661. queueLayoutUpdate();
  662. }
  663. void AudioMixer::removeControlForUuid(QString uuid)
  664. {
  665. auto item = volumeList.find(uuid);
  666. if (item != volumeList.end()) {
  667. VolumeControl *widget = item->second;
  668. if (widget) {
  669. activeLayout()->removeWidget(widget);
  670. widget->deleteLater();
  671. }
  672. volumeList.erase(item);
  673. }
  674. previewSources.erase(uuid);
  675. globalSources.erase(uuid);
  676. }
  677. void AudioMixer::onFrontendEvent(obs_frontend_event event, void *data)
  678. {
  679. AudioMixer *mixer = static_cast<AudioMixer *>(data);
  680. mixer->handleFrontendEvent(event);
  681. }
  682. void AudioMixer::handleFrontendEvent(obs_frontend_event event)
  683. {
  684. switch (event) {
  685. case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
  686. updatePreviewSources();
  687. queueLayoutUpdate();
  688. break;
  689. case OBS_FRONTEND_EVENT_EXIT:
  690. obs_frontend_remove_event_callback(AudioMixer::onFrontendEvent, this);
  691. break;
  692. default:
  693. break;
  694. }
  695. }
  696. void AudioMixer::updateShowInactive()
  697. {
  698. bool settingShowInactive = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowInactive");
  699. if (showInactive == settingShowInactive) {
  700. return;
  701. }
  702. showInactive = settingShowInactive;
  703. queueLayoutUpdate();
  704. }
  705. void AudioMixer::updateKeepInactiveLast()
  706. {
  707. bool settingKeepInactiveLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepInactiveLast");
  708. if (keepInactiveLast == settingKeepInactiveLast) {
  709. return;
  710. }
  711. keepInactiveLast = settingKeepInactiveLast;
  712. queueLayoutUpdate();
  713. }
  714. void AudioMixer::updateShowHidden()
  715. {
  716. bool settingShowHidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerShowHidden");
  717. if (showHidden == settingShowHidden) {
  718. return;
  719. }
  720. showHidden = settingShowHidden;
  721. toggleHiddenButton->setText(QTStr("Basic.AudioMixer.HiddenTotal").arg(hiddenCount));
  722. toggleHiddenButton->setChecked(showHidden);
  723. showHiddenCheckBox->setChecked(showHidden);
  724. queueLayoutUpdate();
  725. }
  726. void AudioMixer::updateKeepHiddenLast()
  727. {
  728. bool settingKeepHiddenLast = config_get_bool(App()->GetUserConfig(), "BasicWindow", "MixerKeepHiddenLast");
  729. if (keepHiddenLast == settingKeepHiddenLast) {
  730. return;
  731. }
  732. keepHiddenLast = settingKeepHiddenLast;
  733. queueLayoutUpdate();
  734. }
  735. void AudioMixer::updateShowToolbar()
  736. {
  737. bool settingShowToolbar = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars");
  738. if (showToolbar == settingShowToolbar) {
  739. return;
  740. }
  741. showToolbar = settingShowToolbar;
  742. showToolbar ? mixerToolbar->show() : mixerToolbar->hide();
  743. }
  744. void AudioMixer::obsSourceActivated(void *data, calldata_t *params)
  745. {
  746. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  747. uint32_t flags = obs_source_get_output_flags(source);
  748. if (flags & OBS_SOURCE_AUDIO) {
  749. auto uuidPointer = obs_source_get_uuid(source);
  750. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  751. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  752. }
  753. }
  754. void AudioMixer::obsSourceDeactivated(void *data, calldata_t *params)
  755. {
  756. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  757. uint32_t flags = obs_source_get_output_flags(source);
  758. if (flags & OBS_SOURCE_AUDIO) {
  759. auto uuidPointer = obs_source_get_uuid(source);
  760. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  761. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  762. }
  763. }
  764. void AudioMixer::obsSourceAudioActivated(void *data, calldata_t *params)
  765. {
  766. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  767. if (obs_source_active(source)) {
  768. auto uuidPointer = obs_source_get_uuid(source);
  769. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility",
  770. Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  771. }
  772. }
  773. void AudioMixer::obsSourceAudioDeactivated(void *data, calldata_t *params)
  774. {
  775. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  776. auto uuidPointer = obs_source_get_uuid(source);
  777. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "updateControlVisibility", Qt::QueuedConnection,
  778. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  779. }
  780. void AudioMixer::obsSourceCreate(void *data, calldata_t *params)
  781. {
  782. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  783. uint32_t flags = obs_source_get_output_flags(source);
  784. if (flags & OBS_SOURCE_AUDIO) {
  785. auto uuidPointer = obs_source_get_uuid(source);
  786. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "sourceCreated", Qt::QueuedConnection,
  787. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  788. }
  789. }
  790. void AudioMixer::obsSourceRemove(void *data, calldata_t *params)
  791. {
  792. obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
  793. uint32_t flags = obs_source_get_output_flags(source);
  794. if (flags & OBS_SOURCE_AUDIO) {
  795. auto uuidPointer = obs_source_get_uuid(source);
  796. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "sourceRemoved", Qt::QueuedConnection,
  797. Q_ARG(QString, QString::fromUtf8(uuidPointer)));
  798. }
  799. }
  800. void AudioMixer::obsSourceRename(void *data, calldata_t *)
  801. {
  802. QMetaObject::invokeMethod(static_cast<AudioMixer *>(data), "queueLayoutUpdate", Qt::QueuedConnection);
  803. }