AudioMixer.cpp 30 KB

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