From 8d023ee3858b74710dc63d30ac128530f9433b77 Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Mon, 21 Sep 2020 00:52:42 +0200 Subject: [PATCH] Add audio tab (#36) * add volumemeter widget * add audio tab UI * add checkAudioSwitch functionality * use constexpr instead of preprocessing --- CMakeLists.txt | 3 + forms/advanced-scene-switcher.ui | 253 +++++- src/advanced-scene-switcher.cpp | 30 +- src/audio-switch.cpp | 327 ++++++++ src/general.cpp | 68 +- src/headers/advanced-scene-switcher.hpp | 20 +- src/headers/switcher-data-structs.hpp | 167 +++- src/headers/utility.hpp | 13 + src/headers/volume-control.hpp | 245 ++++++ src/hotkey.cpp | 6 +- src/idle-switch.cpp | 10 +- src/media-switch.cpp | 8 +- src/priority.cpp | 2 +- src/scene-round-trip.cpp | 14 +- src/time-switch.cpp | 11 +- src/volume-control.cpp | 1002 +++++++++++++++++++++++ 16 files changed, 2087 insertions(+), 92 deletions(-) create mode 100644 src/audio-switch.cpp create mode 100644 src/headers/volume-control.hpp create mode 100644 src/volume-control.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6477be90..6ac49323 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,6 +169,7 @@ else () src/headers/switcher-data-structs.hpp src/headers/utility.hpp src/headers/curl-helper.hpp + src/headers/volume-control.hpp ) set(advanced-scene-switcher_SOURCES @@ -189,7 +190,9 @@ else () src/pause-switch.cpp src/random.cpp src/time-switch.cpp + src/audio-switch.cpp src/curl-helper.cpp + src/volume-control.cpp ) set(advanced-scene-switcher_UI diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui index 4512cfbd..102544ad 100644 --- a/forms/advanced-scene-switcher.ui +++ b/forms/advanced-scene-switcher.ui @@ -2404,7 +2404,7 @@ File - + @@ -3657,6 +3657,257 @@ + + + Audio + + + + + + + + + + When the volume of + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + is above + + + + + + + % + + + 100 + + + + + + + switch to + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + using + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + 0 + 0 + + + + false + + + + + + + + + + 22 + 22 + + + + true + + + addIconSmall + + + + + + + + 22 + 22 + + + + true + + + removeIconSmall + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 22 + 22 + + + + + + + + ../../../forms/images/up.svg../../../forms/images/up.svg + + + true + + + upArrowIconSmall + + + + + + + + 22 + 22 + + + + + + + + ../../../forms/images/down.svg../../../forms/images/down.svg + + + true + + + downArrowIconSmall + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + diff --git a/src/advanced-scene-switcher.cpp b/src/advanced-scene-switcher.cpp index 5c334d8e..404ae893 100644 --- a/src/advanced-scene-switcher.cpp +++ b/src/advanced-scene-switcher.cpp @@ -45,7 +45,7 @@ void SceneSwitcher::populateSceneSelection(QComboBox *sel, bool addPrevious) } if (addPrevious) - sel->addItem(PREVIOUS_SCENE_NAME); + sel->addItem(previous_scene_name); } void SceneSwitcher::populateTransitionSelection(QComboBox *sel) @@ -90,6 +90,7 @@ void SceneSwitcher::loadUI() setupMediaTab(); setupFileTab(); setupTimeTab(); + setupAudioTab(); setTabOrder(); @@ -119,6 +120,7 @@ static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *) switcher->saveFileSwitches(obj); switcher->saveMediaSwitches(obj); switcher->saveTimeSwitches(obj); + switcher->saveAudioSwitches(obj); switcher->saveGeneralSettings(obj); obs_data_set_obj(save_data, "advanced-scene-switcher", obj); @@ -144,6 +146,7 @@ static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *) switcher->loadFileSwitches(obj); switcher->loadMediaSwitches(obj); switcher->loadTimeSwitches(obj); + switcher->loadAudioSwitches(obj); switcher->loadGeneralSettings(obj); obs_data_release(obj); @@ -213,27 +216,27 @@ void SwitcherData::Thread() for (int switchFuncName : functionNamesByPriority) { switch (switchFuncName) { - case READ_FILE_FUNC: + case read_file_func: checkSwitchInfoFromFile(match, scene, transition); checkFileContent(match, scene, transition); break; - case IDLE_FUNC: + case idle_func: checkIdleSwitch(match, scene, transition); break; - case EXE_FUNC: + case exe_func: checkExeSwitch(match, scene, transition); break; - case SCREEN_REGION_FUNC: + case screen_region_func: checkScreenRegionSwitch(match, scene, transition); break; - case WINDOW_TITLE_FUNC: + case window_title_func: checkWindowTitleSwitch(match, scene, transition); break; - case ROUND_TRIP_FUNC: + case round_trip_func: checkSceneRoundTrip(match, scene, transition, lock); if (sceneChangedDuringWait()) //scene might have changed during the sleep @@ -241,12 +244,15 @@ void SwitcherData::Thread() goto startLoop; } break; - case MEDIA_FUNC: + case media_func: checkMediaSwitch(match, scene, transition); break; - case TIME_FUNC: + case time_func: checkTimeSwitch(match, scene, transition); break; + case audio_func: + checkAudioSwitch(match, scene, transition); + break; } if (switcher->stop) { @@ -442,13 +448,13 @@ extern "C" void InitSceneSwitcher() "startStopToggleSwitcherHotkey", "Toggle Start/Stop for the Advanced Scene Switcher", startStopToggleHotkeyFunc, NULL); - loadKeybinding(toggleHotkeyId, TOGGLE_HOTKEY_PATH); + loadKeybinding(toggleHotkeyId, toggle_hotkey_path); obs_hotkey_id startHotkeyId = obs_hotkey_register_frontend( "startSwitcherHotkey", "Start the Advanced Scene Switcher", startHotkeyFunc, NULL); - loadKeybinding(startHotkeyId, START_HOTKEY_PATH); + loadKeybinding(startHotkeyId, start_hotkey_path); obs_hotkey_id stopHotkeyId = obs_hotkey_register_frontend( "stopSwitcherHotkey", "Stop the Advanced Scene Switcher", stopHotkeyFunc, NULL); - loadKeybinding(stopHotkeyId, STOP_HOTKEY_PATH); + loadKeybinding(stopHotkeyId, stop_hotkey_path); } diff --git a/src/audio-switch.cpp b/src/audio-switch.cpp new file mode 100644 index 00000000..387a9b94 --- /dev/null +++ b/src/audio-switch.cpp @@ -0,0 +1,327 @@ +#include "headers/advanced-scene-switcher.hpp" + +#include "headers/volume-control.hpp" + +void SceneSwitcher::on_audioSwitches_currentRowChanged(int idx) +{ + if (loading) + return; + if (idx == -1) + return; + + QListWidgetItem *item = ui->audioSwitches->item(idx); + + QString audioScenestr = item->data(Qt::UserRole).toString(); + + std::lock_guard lock(switcher->m); + for (auto &s : switcher->audioSwitches) { + if (audioScenestr.compare(s.audioSwitchStr.c_str()) == 0) { + QString sceneName = GetWeakSourceName(s.scene).c_str(); + QString audioSrouceName = + GetWeakSourceName(s.audioSource).c_str(); + QString transitionName = + GetWeakSourceName(s.transition).c_str(); + ui->audioScenes->setCurrentText(sceneName); + ui->audioTransitions->setCurrentText(transitionName); + ui->audioSources->setCurrentText(audioSrouceName); + ui->audioVolumeThreshold->setValue(s.volumeThreshold); + volMeter->GetSlider()->setValue(s.volumeThreshold); + break; + } + } +} + +void SceneSwitcher::on_audioSources_currentTextChanged(const QString &text) +{ + SetAudioVolumeMeter(text); +} + +void SceneSwitcher::SetAudioVolumeMeter(const QString &name) +{ + obs_source_t *soruce = + obs_get_source_by_name(name.toUtf8().constData()); + if (!soruce) { + return; + } + + if (volMeter) { + ui->audioControlLayout->removeWidget(volMeter); + delete volMeter; + } + + volMeter = new VolControl(soruce); + ui->audioControlLayout->addWidget(volMeter); + obs_source_release(soruce); + + QWidget::connect(volMeter->GetSlider(), SIGNAL(valueChanged(int)), + ui->audioVolumeThreshold, SLOT(setValue(int))); + QWidget::connect(ui->audioVolumeThreshold, SIGNAL(valueChanged(int)), + volMeter->GetSlider(), SLOT(setValue(int))); +} + +int SceneSwitcher::audioFindByData(const QString &source, const int &volume) +{ + QRegExp rx(MakeAudioSwitchName(QStringLiteral(".*"), + QStringLiteral(".*"), source, volume)); + int count = ui->audioSwitches->count(); + + for (int i = 0; i < count; i++) { + QListWidgetItem *item = ui->audioSwitches->item(i); + QString str = item->data(Qt::UserRole).toString(); + + if (rx.exactMatch(str)) + return i; + } + + return -1; +} + +void SceneSwitcher::on_audioAdd_clicked() +{ + QString sceneName = ui->audioScenes->currentText(); + QString transitionName = ui->audioTransitions->currentText(); + QString audioSourceName = ui->audioSources->currentText(); + int vol = ui->audioVolumeThreshold->value(); + + if (sceneName.isEmpty() || audioSourceName.isEmpty()) + return; + + OBSWeakSource source = GetWeakSourceByQString(sceneName); + OBSWeakSource audioSource = GetWeakSourceByQString(audioSourceName); + OBSWeakSource transition = GetWeakTransitionByQString(transitionName); + + QString text = MakeAudioSwitchName(sceneName, transitionName, + audioSourceName, vol); + QVariant v = QVariant::fromValue(text); + + int idx = audioFindByData(audioSourceName, vol); + + if (idx == -1) { + std::lock_guard lock(switcher->m); + switcher->audioSwitches.emplace_back( + source, transition, audioSource, vol, + (sceneName == QString(previous_scene_name)), + text.toUtf8().constData()); + QListWidgetItem *item = + new QListWidgetItem(text, ui->audioSwitches); + item->setData(Qt::UserRole, v); + } else { + QListWidgetItem *item = ui->audioSwitches->item(idx); + item->setText(text); + item->setData(Qt::UserRole, v); + + { + std::lock_guard lock(switcher->m); + for (auto &s : switcher->audioSwitches) { + if (s.audioSource == audioSource && + s.volumeThreshold == vol) { + s.scene = source; + s.transition = transition; + s.usePreviousScene = + (sceneName == + QString(previous_scene_name)); + s.audioSwitchStr = + text.toUtf8().constData(); + break; + } + } + } + } +} + +void SceneSwitcher::on_audioRemove_clicked() +{ + QListWidgetItem *item = ui->audioSwitches->currentItem(); + if (!item) + return; + + std::string text = + item->data(Qt::UserRole).toString().toUtf8().constData(); + + { + std::lock_guard lock(switcher->m); + auto &switches = switcher->audioSwitches; + + for (auto it = switches.begin(); it != switches.end(); ++it) { + auto &s = *it; + + if (s.audioSwitchStr == text) { + switches.erase(it); + break; + } + } + } + + delete item; +} + +void SceneSwitcher::on_audioUp_clicked() +{ + int index = ui->audioSwitches->currentRow(); + if (index != -1 && index != 0) { + ui->audioSwitches->insertItem( + index - 1, ui->audioSwitches->takeItem(index)); + ui->audioSwitches->setCurrentRow(index - 1); + + std::lock_guard lock(switcher->m); + + iter_swap(switcher->audioSwitches.begin() + index, + switcher->audioSwitches.begin() + index - 1); + } +} + +void SceneSwitcher::on_audioDown_clicked() +{ + int index = ui->audioSwitches->currentRow(); + if (index != -1 && index != ui->audioSwitches->count() - 1) { + ui->audioSwitches->insertItem( + index + 1, ui->audioSwitches->takeItem(index)); + ui->audioSwitches->setCurrentRow(index + 1); + + std::lock_guard lock(switcher->m); + + iter_swap(switcher->audioSwitches.begin() + index, + switcher->audioSwitches.begin() + index + 1); + } +} + +void SwitcherData::checkAudioSwitch(bool &match, OBSWeakSource &scene, + OBSWeakSource &transition) +{ + if (audioSwitches.size() == 0) + return; + + for (AudioSwitch &s : audioSwitches) { + obs_source_t *as = obs_weak_source_get_source(s.audioSource); + const char *name = obs_source_get_name(as); + bool audioActive = obs_source_active(as); + obs_source_release(as); + + // peak will have a value from -60 db to 0 db + bool volumeThresholdreached = (s.peak + 60) * 1.7 > + s.volumeThreshold; + + if (volumeThresholdreached && audioActive) { + scene = (s.usePreviousScene) ? previousScene : s.scene; + transition = s.transition; + match = true; + + if (verbose) + blog(LOG_INFO, + "Advanced Scene Switcher audio match for %s", + name); + + break; + } + } +} + +void SwitcherData::saveAudioSwitches(obs_data_t *obj) +{ + obs_data_array_t *audioArray = obs_data_array_create(); + for (AudioSwitch &s : switcher->audioSwitches) { + obs_data_t *array_obj = obs_data_create(); + + obs_source_t *sceneSource = obs_weak_source_get_source(s.scene); + obs_source_t *transition = + obs_weak_source_get_source(s.transition); + obs_source_t *audioSrouce = + obs_weak_source_get_source(s.audioSource); + if ((s.usePreviousScene || sceneSource) && transition) { + const char *sceneName = + obs_source_get_name(sceneSource); + const char *transitionName = + obs_source_get_name(transition); + const char *audioSourceName = + obs_source_get_name(audioSrouce); + obs_data_set_string(array_obj, "scene", + s.usePreviousScene + ? previous_scene_name + : sceneName); + obs_data_set_string(array_obj, "transition", + transitionName); + obs_data_set_string(array_obj, "audioSource", + audioSourceName); + obs_data_set_int(array_obj, "volume", + s.volumeThreshold); + obs_data_array_push_back(audioArray, array_obj); + } + obs_source_release(sceneSource); + obs_source_release(transition); + obs_source_release(audioSrouce); + + obs_data_release(array_obj); + } + obs_data_set_array(obj, "audioSwitches", audioArray); + obs_data_array_release(audioArray); +} + +void SwitcherData::loadAudioSwitches(obs_data_t *obj) +{ + switcher->audioSwitches.clear(); + + obs_data_array_t *audioArray = obs_data_get_array(obj, "audioSwitches"); + size_t count = obs_data_array_count(audioArray); + + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(audioArray, i); + + const char *scene = obs_data_get_string(array_obj, "scene"); + const char *transition = + obs_data_get_string(array_obj, "transition"); + const char *audioSource = + obs_data_get_string(array_obj, "audioSource"); + int vol = obs_data_get_int(array_obj, "volume"); + + std::string audioSwitchStr = + MakeAudioSwitchName(scene, transition, audioSource, vol) + .toUtf8() + .constData(); + + switcher->audioSwitches.emplace_back( + GetWeakSourceByName(scene), + GetWeakTransitionByName(transition), + GetWeakSourceByName(audioSource), vol, + (strcmp(scene, previous_scene_name) == 0), + audioSwitchStr); + + obs_data_release(array_obj); + } + obs_data_array_release(audioArray); +} + +void SceneSwitcher::setupAudioTab() +{ + populateSceneSelection(ui->audioScenes, true); + populateTransitionSelection(ui->audioTransitions); + + auto sourceEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + QComboBox *combo = reinterpret_cast(data); + uint32_t flags = obs_source_get_output_flags(source); + + if ((flags & OBS_SOURCE_AUDIO) != 0) { + const char *name = obs_source_get_name(source); + combo->addItem(name); + } + return true; + }; + + obs_enum_sources(sourceEnum, ui->audioSources); + + for (auto &s : switcher->audioSwitches) { + std::string sceneName = (s.usePreviousScene) + ? previous_scene_name + : GetWeakSourceName(s.scene); + std::string transitionName = GetWeakSourceName(s.transition); + std::string audioSourceName = GetWeakSourceName(s.audioSource); + QString listText = MakeAudioSwitchName(sceneName.c_str(), + transitionName.c_str(), + audioSourceName.c_str(), + s.volumeThreshold); + + QListWidgetItem *item = + new QListWidgetItem(listText, ui->audioSwitches); + item->setData(Qt::UserRole, listText); + } +} diff --git a/src/general.cpp b/src/general.cpp index 85e372ca..76399740 100644 --- a/src/general.cpp +++ b/src/general.cpp @@ -265,6 +265,7 @@ void SceneSwitcher::on_exportSettings_clicked() switcher->saveFileSwitches(obj); switcher->saveMediaSwitches(obj); switcher->saveTimeSwitches(obj); + switcher->saveAudioSwitches(obj); switcher->saveGeneralSettings(obj); obs_data_save_json(obj, file.fileName().toUtf8().constData()); @@ -309,6 +310,7 @@ void SceneSwitcher::on_importSettings_clicked() switcher->loadFileSwitches(obj); switcher->loadMediaSwitches(obj); switcher->loadTimeSwitches(obj); + switcher->loadAudioSwitches(obj); switcher->loadGeneralSettings(obj); obs_data_release(obj); @@ -362,6 +364,9 @@ int findTabIndex(QTabBar *bar, int pos) case 11: tabName = "Sequence"; break; + case 12: + tabName = "Audio"; + break; } for (int i = 0; i < bar->count(); ++i) { @@ -442,6 +447,8 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj) switcher->functionNamesByPriority[6]); obs_data_set_int(obj, "priority7", switcher->functionNamesByPriority[7]); + obs_data_set_int(obj, "priority8", + switcher->functionNamesByPriority[8]); obs_data_set_int(obj, "threadPriority", switcher->threadPriority); @@ -457,11 +464,12 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj) obs_data_set_int(obj, "timeTabPos", switcher->tabOrder[9]); obs_data_set_int(obj, "idleTabPos", switcher->tabOrder[10]); obs_data_set_int(obj, "sequenceTabPos", switcher->tabOrder[11]); + obs_data_set_int(obj, "audioTabPos", switcher->tabOrder[12]); } void SwitcherData::loadGeneralSettings(obs_data_t *obj) { - obs_data_set_default_int(obj, "interval", DEFAULT_INTERVAL); + obs_data_set_default_int(obj, "interval", default_interval); switcher->interval = obs_data_get_int(obj, "interval"); obs_data_set_default_int(obj, "switch_if_not_matching", NO_SWITCH); @@ -494,14 +502,15 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj) switcher->verbose = obs_data_get_bool(obj, "verbose"); - obs_data_set_default_int(obj, "priority0", DEFAULT_PRIORITY_0); - obs_data_set_default_int(obj, "priority1", DEFAULT_PRIORITY_1); - obs_data_set_default_int(obj, "priority2", DEFAULT_PRIORITY_2); - obs_data_set_default_int(obj, "priority3", DEFAULT_PRIORITY_3); - obs_data_set_default_int(obj, "priority4", DEFAULT_PRIORITY_4); - obs_data_set_default_int(obj, "priority5", DEFAULT_PRIORITY_5); - obs_data_set_default_int(obj, "priority6", DEFAULT_PRIORITY_6); - obs_data_set_default_int(obj, "priority7", DEFAULT_PRIORITY_7); + obs_data_set_default_int(obj, "priority0", default_priority_0); + obs_data_set_default_int(obj, "priority1", default_priority_1); + obs_data_set_default_int(obj, "priority2", default_priority_2); + obs_data_set_default_int(obj, "priority3", default_priority_3); + obs_data_set_default_int(obj, "priority4", default_priority_4); + obs_data_set_default_int(obj, "priority5", default_priority_5); + obs_data_set_default_int(obj, "priority6", default_priority_6); + obs_data_set_default_int(obj, "priority7", default_priority_7); + obs_data_set_default_int(obj, "priority8", default_priority_8); switcher->functionNamesByPriority[0] = (obs_data_get_int(obj, "priority0")); @@ -519,15 +528,18 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj) (obs_data_get_int(obj, "priority6")); switcher->functionNamesByPriority[7] = (obs_data_get_int(obj, "priority7")); + switcher->functionNamesByPriority[8] = + (obs_data_get_int(obj, "priority8")); if (!switcher->prioFuncsValid()) { - switcher->functionNamesByPriority[0] = (DEFAULT_PRIORITY_0); - switcher->functionNamesByPriority[1] = (DEFAULT_PRIORITY_1); - switcher->functionNamesByPriority[2] = (DEFAULT_PRIORITY_2); - switcher->functionNamesByPriority[3] = (DEFAULT_PRIORITY_3); - switcher->functionNamesByPriority[4] = (DEFAULT_PRIORITY_4); - switcher->functionNamesByPriority[5] = (DEFAULT_PRIORITY_5); - switcher->functionNamesByPriority[6] = (DEFAULT_PRIORITY_6); - switcher->functionNamesByPriority[7] = (DEFAULT_PRIORITY_7); + switcher->functionNamesByPriority[0] = (default_priority_0); + switcher->functionNamesByPriority[1] = (default_priority_1); + switcher->functionNamesByPriority[2] = (default_priority_2); + switcher->functionNamesByPriority[3] = (default_priority_3); + switcher->functionNamesByPriority[4] = (default_priority_4); + switcher->functionNamesByPriority[5] = (default_priority_5); + switcher->functionNamesByPriority[6] = (default_priority_6); + switcher->functionNamesByPriority[7] = (default_priority_7); + switcher->functionNamesByPriority[8] = (default_priority_8); } obs_data_set_default_int(obj, "threadPriority", @@ -546,6 +558,7 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj) obs_data_set_default_int(obj, "timeTabPos", 9); obs_data_set_default_int(obj, "idleTabPos", 10); obs_data_set_default_int(obj, "sequenceTabPos", 11); + obs_data_set_default_int(obj, "audioTabPos", 12); switcher->tabOrder.emplace_back( (int)(obs_data_get_int(obj, "generalTabPos"))); @@ -571,6 +584,8 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj) (int)(obs_data_get_int(obj, "idleTabPos"))); switcher->tabOrder.emplace_back( (int)(obs_data_get_int(obj, "sequenceTabPos"))); + switcher->tabOrder.emplace_back( + (int)(obs_data_get_int(obj, "audioTabPos"))); } void SceneSwitcher::setupGeneralTab() @@ -625,30 +640,33 @@ void SceneSwitcher::setupGeneralTab() for (int p : switcher->functionNamesByPriority) { std::string s = ""; switch (p) { - case READ_FILE_FUNC: + case read_file_func: s = "File Content"; break; - case ROUND_TRIP_FUNC: + case round_trip_func: s = "Scene Sequence"; break; - case IDLE_FUNC: + case idle_func: s = "Idle Detection"; break; - case EXE_FUNC: + case exe_func: s = "Executable"; break; - case SCREEN_REGION_FUNC: + case screen_region_func: s = "Screen Region"; break; - case WINDOW_TITLE_FUNC: + case window_title_func: s = "Window Title"; break; - case MEDIA_FUNC: + case media_func: s = "Media"; break; - case TIME_FUNC: + case time_func: s = "Time"; break; + case audio_func: + s = "Audio"; + break; } QString text(s.c_str()); QListWidgetItem *item = diff --git a/src/headers/advanced-scene-switcher.hpp b/src/headers/advanced-scene-switcher.hpp index d76facf8..0f5e04a4 100644 --- a/src/headers/advanced-scene-switcher.hpp +++ b/src/headers/advanced-scene-switcher.hpp @@ -10,6 +10,7 @@ #include "ui_advanced-scene-switcher.h" #endif #include "switcher-data-structs.hpp" +#include "volume-control.hpp" class QCloseEvent; @@ -21,6 +22,7 @@ class SceneSwitcher : public QDialog { public: std::unique_ptr ui; + VolControl *volMeter = nullptr; bool loading = true; SceneSwitcher(QWidget *parent); @@ -43,12 +45,14 @@ public: int IgnoreIdleWindowsFindByData(const QString &window); int randomFindByData(const QString &scene); int timeFindByData(const timeTrigger &trigger, const QTime &time); + int audioFindByData(const QString &source, const int &volume); void UpdateNonMatchingScene(const QString &name); void UpdateAutoStopScene(const QString &name); void UpdateAutoStartScene(const QString &name); void UpdateIdleDataTransition(const QString &name); void UpdateIdleDataScene(const QString &name); + void SetAudioVolumeMeter(const QString &name); void loadUI(); void populateSceneSelection(QComboBox *sel, bool addPrevious); @@ -66,6 +70,7 @@ public: void setupMediaTab(); void setupFileTab(); void setupTimeTab(); + void setupAudioTab(); void setTabOrder(); public slots: @@ -175,6 +180,13 @@ public slots: void on_timeUp_clicked(); void on_timeDown_clicked(); + void on_audioAdd_clicked(); + void on_audioRemove_clicked(); + void on_audioUp_clicked(); + void on_audioDown_clicked(); + void on_audioSwitches_currentRowChanged(int idx); + void on_audioSources_currentTextChanged(const QString &text); + void on_priorityUp_clicked(); void on_priorityDown_clicked(); void on_threadPriority_currentTextChanged(const QString &text); @@ -182,6 +194,8 @@ public slots: void updateScreenRegionCursorPos(); void on_close_clicked(); + +private: }; /******************************************************************************** @@ -234,9 +248,9 @@ void switchScene(OBSWeakSource &scene, OBSWeakSource &transition, * Hotkey helper ********************************************************************************/ -#define TOGGLE_HOTKEY_PATH "hotkey_toggle.txt" -#define STOP_HOTKEY_PATH "hotkey_stop.txt" -#define START_HOTKEY_PATH "hotkey_start.txt" +constexpr auto toggle_hotkey_path = "hotkey_toggle.txt"; +constexpr auto stop_hotkey_path = "hotkey_stop.txt"; +constexpr auto start_hotkey_path = "hotkey_start.txt"; void stopHotkeyFunc(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed); diff --git a/src/headers/switcher-data-structs.hpp b/src/headers/switcher-data-structs.hpp index 7732d6f3..3cca3761 100644 --- a/src/headers/switcher-data-structs.hpp +++ b/src/headers/switcher-data-structs.hpp @@ -11,29 +11,31 @@ #include #include "utility.hpp" -#define DEFAULT_INTERVAL 300 +constexpr auto default_interval = 300; -#define DEFAULT_IDLE_TIME 60 +constexpr auto default_idle_time = 60; -#define PREVIOUS_SCENE_NAME "Previous Scene" +constexpr auto previous_scene_name = "Previous Scene"; -#define READ_FILE_FUNC 0 -#define ROUND_TRIP_FUNC 1 -#define IDLE_FUNC 2 -#define EXE_FUNC 3 -#define SCREEN_REGION_FUNC 4 -#define WINDOW_TITLE_FUNC 5 -#define MEDIA_FUNC 6 -#define TIME_FUNC 7 +constexpr auto read_file_func = 0; +constexpr auto round_trip_func = 1; +constexpr auto idle_func = 2; +constexpr auto exe_func = 3; +constexpr auto screen_region_func = 4; +constexpr auto window_title_func = 5; +constexpr auto media_func = 6; +constexpr auto time_func = 7; +constexpr auto audio_func = 8; -#define DEFAULT_PRIORITY_0 READ_FILE_FUNC -#define DEFAULT_PRIORITY_1 ROUND_TRIP_FUNC -#define DEFAULT_PRIORITY_2 IDLE_FUNC -#define DEFAULT_PRIORITY_3 EXE_FUNC -#define DEFAULT_PRIORITY_4 SCREEN_REGION_FUNC -#define DEFAULT_PRIORITY_5 WINDOW_TITLE_FUNC -#define DEFAULT_PRIORITY_6 MEDIA_FUNC -#define DEFAULT_PRIORITY_7 TIME_FUNC +constexpr auto default_priority_0 = read_file_func; +constexpr auto default_priority_1 = round_trip_func; +constexpr auto default_priority_2 = idle_func; +constexpr auto default_priority_3 = exe_func; +constexpr auto default_priority_4 = screen_region_func; +constexpr auto default_priority_5 = window_title_func; +constexpr auto default_priority_6 = media_func; +constexpr auto default_priority_7 = time_func; +constexpr auto default_priority_8 = audio_func; /******************************************************************************** * Data structs for each scene switching method @@ -199,7 +201,7 @@ struct FileIOData { struct IdleData { bool idleEnable = false; - int time = DEFAULT_IDLE_TIME; + int time = default_idle_time; OBSWeakSource scene; OBSWeakSource transition; bool usePreviousScene; @@ -238,7 +240,6 @@ struct TimeSwitch { OBSWeakSource transition; timeTrigger trigger; QTime time; - bool matched; bool usePreviousScene; std::string timeSwitchStr; @@ -255,6 +256,107 @@ struct TimeSwitch { } }; +struct AudioSwitch { + OBSWeakSource scene; + OBSWeakSource audioSource; + OBSWeakSource transition; + int volumeThreshold; + float peak; + bool usePreviousScene; + obs_volmeter_t *volmeter; + std::string audioSwitchStr; + + static void setVolumeLevel(void *data, + const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) + { + UNUSED_PARAMETER(magnitude); + UNUSED_PARAMETER(inputPeak); + AudioSwitch *s = static_cast(data); + + s->peak = peak[0]; + for (int i = 1; i < MAX_AUDIO_CHANNELS; i++) + if (peak[i] > s->peak) + s->peak = peak[i]; + } + + inline AudioSwitch(OBSWeakSource scene_, OBSWeakSource transition_, + OBSWeakSource audioSource_, int volumeThreshold_, + bool usePreviousScene_, std::string audioSwitchStr_) + : scene(scene_), + transition(transition_), + audioSource(audioSource_), + volumeThreshold(volumeThreshold_), + usePreviousScene(usePreviousScene_), + audioSwitchStr(audioSwitchStr_) + { + volmeter = obs_volmeter_create(OBS_FADER_LOG); + obs_volmeter_add_callback(volmeter, setVolumeLevel, this); + obs_source_t *as = obs_weak_source_get_source(audioSource); + if (!obs_volmeter_attach_source(volmeter, as)) { + const char *name = obs_source_get_name(as); + blog(LOG_WARNING, + "failed to attach volmeter to source %s", name); + } + obs_source_release(as); + } + + AudioSwitch(const AudioSwitch &other) + : scene(other.scene), + transition(other.transition), + audioSource(other.audioSource), + volumeThreshold(other.volumeThreshold), + usePreviousScene(other.usePreviousScene), + audioSwitchStr(other.audioSwitchStr) + { + volmeter = obs_volmeter_create(OBS_FADER_LOG); + obs_volmeter_add_callback(volmeter, setVolumeLevel, this); + obs_source_t *as = + obs_weak_source_get_source(other.audioSource); + if (!obs_volmeter_attach_source(volmeter, as)) { + const char *name = obs_source_get_name(as); + blog(LOG_WARNING, + "failed to attach volmeter to source %s", name); + } + obs_source_release(as); + } + + AudioSwitch(AudioSwitch &&other) + : scene(other.scene), + transition(other.transition), + audioSource(other.audioSource), + volumeThreshold(other.volumeThreshold), + usePreviousScene(other.usePreviousScene), + audioSwitchStr(other.audioSwitchStr), + volmeter(other.volmeter) + { + other.volmeter = nullptr; + } + + inline ~AudioSwitch() + { + obs_volmeter_remove_callback(volmeter, setVolumeLevel, this); + obs_volmeter_destroy(volmeter); + } + + AudioSwitch &operator=(const AudioSwitch &other) + { + return *this = AudioSwitch(other); + } + + AudioSwitch &operator=(AudioSwitch &&other) noexcept + { + if (this == &other) { + return *this; + } + obs_volmeter_destroy(volmeter); + volmeter = other.volmeter; + other.volmeter = nullptr; + return *this; + } +}; + typedef enum { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 } NoMatch; typedef enum { PERSIST = 0, START = 1, STOP = 2 } StartupBehavior; typedef enum { @@ -280,7 +382,7 @@ struct SwitcherData { bool verbose = false; bool tansitionOverrideOverride = false; - int interval = DEFAULT_INTERVAL; + int interval = default_interval; obs_source_t *waitScene = NULL; //scene during which wait started OBSWeakSource previousScene = NULL; @@ -330,10 +432,12 @@ struct SwitcherData { std::vector timeSwitches; QDateTime liveTime; + std::vector audioSwitches; + std::vector functionNamesByPriority = std::vector{ - DEFAULT_PRIORITY_0, DEFAULT_PRIORITY_1, DEFAULT_PRIORITY_2, - DEFAULT_PRIORITY_3, DEFAULT_PRIORITY_4, DEFAULT_PRIORITY_5, - DEFAULT_PRIORITY_6, DEFAULT_PRIORITY_7}; + default_priority_0, default_priority_1, default_priority_2, + default_priority_3, default_priority_4, default_priority_5, + default_priority_6, default_priority_7, default_priority_8}; struct ThreadPrio { std::string name; @@ -396,6 +500,8 @@ struct SwitcherData { OBSWeakSource &transition); void checkTimeSwitch(bool &match, OBSWeakSource &scene, OBSWeakSource &transition); + void checkAudioSwitch(bool &match, OBSWeakSource &scene, + OBSWeakSource &transition); void saveWindowTitleSwitches(obs_data_t *obj); void saveScreenRegionSwitches(obs_data_t *obj); @@ -408,6 +514,7 @@ struct SwitcherData { void saveFileSwitches(obs_data_t *obj); void saveMediaSwitches(obs_data_t *obj); void saveTimeSwitches(obs_data_t *obj); + void saveAudioSwitches(obs_data_t *obj); void saveGeneralSettings(obs_data_t *obj); void loadWindowTitleSwitches(obs_data_t *obj); @@ -421,6 +528,7 @@ struct SwitcherData { void loadFileSwitches(obs_data_t *obj); void loadMediaSwitches(obs_data_t *obj); void loadTimeSwitches(obs_data_t *obj); + void loadAudioSwitches(obs_data_t *obj); void loadGeneralSettings(obs_data_t *obj); void Prune() @@ -531,6 +639,15 @@ struct SwitcherData { mediaSwitches.erase(mediaSwitches.begin() + i--); } + + for (size_t i = 0; i < audioSwitches.size(); i++) { + AudioSwitch &s = audioSwitches[i]; + if ((!s.usePreviousScene && + !WeakSourceValid(s.scene)) || + !WeakSourceValid(s.transition)) + audioSwitches.erase(audioSwitches.begin() + + i--); + } } inline ~SwitcherData() { Stop(); } }; diff --git a/src/headers/utility.hpp b/src/headers/utility.hpp index 0ca9d980..6857fd07 100644 --- a/src/headers/utility.hpp +++ b/src/headers/utility.hpp @@ -224,6 +224,19 @@ static inline QString MakeTimeSwitchName(const QString &scene, return switchName; } +static inline QString MakeAudioSwitchName(const QString &scene, + const QString &transition, + const QString &audioSrouce, + const int &volume) +{ + QString switchName = QStringLiteral("When volume of ") + audioSrouce + + QStringLiteral(" is above ") + + QString::number(volume) + + QStringLiteral("% switch to ") + scene + + QStringLiteral(" using ") + transition; + return switchName; +} + static inline std::string GetWeakSourceName(obs_weak_source_t *weak_source) { std::string name; diff --git a/src/headers/volume-control.hpp b/src/headers/volume-control.hpp new file mode 100644 index 00000000..7ef783d1 --- /dev/null +++ b/src/headers/volume-control.hpp @@ -0,0 +1,245 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QPushButton; +class VolumeMeterTimer; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor + WRITE setBackgroundNominalColor DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor + WRITE setBackgroundWarningColor DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColor READ getBackgroundErrorColor + WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor + WRITE setForegroundNominalColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor + WRITE setForegroundWarningColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColor READ getForegroundErrorColor + WRITE setForegroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor + DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE + setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE + setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE + setMinorTickColor DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel + DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel + DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel + DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel + DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE + setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE + setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY( + qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime + WRITE setMagnitudeIntegrationTime DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE + setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration + WRITE setInputPeakHoldDuration DESIGNABLE true) + +private slots: + void ClipEnding(); + +private: + obs_volmeter_t *obs_volmeter; + static QWeakPointer updateTimer; + QSharedPointer updateTimerRef; + + inline void resetLevels(); + inline void handleChannelCofigurationChange(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, + qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, + qreal timeSinceLastRedraw); + + void paintInputMeter(QPainter &painter, int x, int y, int width, + int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, + float magnitude, float peak, float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width, + int height); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, + float magnitude, float peak, float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + QPixmap *tickPaintCache = nullptr; + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, + obs_volmeter_t *obs_volmeter = nullptr, + bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class QSlider; +class MuteCheckBox; + +class VolControl : public QWidget { + Q_OBJECT + +private: + OBSSource source; + QLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + QSlider *slider; + float levelTotal; + float levelCount; + obs_fader_t *obs_fader; + obs_volmeter_t *obs_volmeter; + bool vertical; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, + const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + +private slots: + void SliderChanged(int vol); + void updateText(); + +public: + explicit VolControl(OBSSource source, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + QString GetName() const; + void SetName(const QString &newName); + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + + QSlider *GetSlider() const; +}; diff --git a/src/hotkey.cpp b/src/hotkey.cpp index 5bb419c7..4e4cae7a 100644 --- a/src/hotkey.cpp +++ b/src/hotkey.cpp @@ -17,7 +17,7 @@ void startHotkeyFunc(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey, if (hotkeyData != NULL) { char *path = obs_module_config_path(""); std::ofstream file; - file.open(std::string(path).append(START_HOTKEY_PATH), + file.open(std::string(path).append(start_hotkey_path), std::ofstream::trunc); if (file.is_open()) { size_t num = obs_data_array_count(hotkeyData); @@ -51,7 +51,7 @@ void stopHotkeyFunc(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey, if (hotkeyData != NULL) { char *path = obs_module_config_path(""); std::ofstream file; - file.open(std::string(path).append(STOP_HOTKEY_PATH), + file.open(std::string(path).append(stop_hotkey_path), std::ofstream::trunc); if (file.is_open()) { size_t num = obs_data_array_count(hotkeyData); @@ -87,7 +87,7 @@ void startStopToggleHotkeyFunc(void *data, obs_hotkey_id id, if (hotkeyData != NULL) { char *path = obs_module_config_path(""); std::ofstream file; - file.open(std::string(path).append(TOGGLE_HOTKEY_PATH), + file.open(std::string(path).append(toggle_hotkey_path), std::ofstream::trunc); if (file.is_open()) { size_t num = obs_data_array_count(hotkeyData); diff --git a/src/idle-switch.cpp b/src/idle-switch.cpp index 7e92a5f3..5dee86c4 100644 --- a/src/idle-switch.cpp +++ b/src/idle-switch.cpp @@ -80,7 +80,7 @@ void SceneSwitcher::UpdateIdleDataTransition(const QString &name) void SceneSwitcher::UpdateIdleDataScene(const QString &name) { - switcher->idleData.usePreviousScene = (name == PREVIOUS_SCENE_NAME); + switcher->idleData.usePreviousScene = (name == previous_scene_name); obs_source_t *scene = obs_get_source_by_name(name.toUtf8().constData()); obs_weak_source_t *ws = obs_source_get_weak_source(scene); @@ -221,7 +221,7 @@ void SwitcherData::saveIdleSwitches(obs_data_t *obj) obs_data_set_bool(obj, "idleEnable", switcher->idleData.idleEnable); obs_data_set_string(obj, "idleSceneName", switcher->idleData.usePreviousScene - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : idleSceneName.c_str()); obs_data_set_string(obj, "idleTransitionName", idleTransitionName.c_str()); @@ -256,10 +256,10 @@ void SwitcherData::loadIdleSwitches(obs_data_t *obj) GetWeakTransitionByName(idleTransitionName.c_str()); obs_data_set_default_bool(obj, "idleEnable", false); switcher->idleData.idleEnable = obs_data_get_bool(obj, "idleEnable"); - obs_data_set_default_int(obj, "idleTime", DEFAULT_IDLE_TIME); + obs_data_set_default_int(obj, "idleTime", default_idle_time); switcher->idleData.time = obs_data_get_int(obj, "idleTime"); switcher->idleData.usePreviousScene = - (idleSceneName == PREVIOUS_SCENE_NAME); + (idleSceneName == previous_scene_name); } void SceneSwitcher::setupIdleTab() @@ -279,7 +279,7 @@ void SceneSwitcher::setupIdleTab() ui->idleCheckBox->setChecked(switcher->idleData.idleEnable); ui->idleScenes->setCurrentText( switcher->idleData.usePreviousScene - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : GetWeakSourceName(switcher->idleData.scene).c_str()); ui->idleTransitions->setCurrentText( GetWeakSourceName(switcher->idleData.transition).c_str()); diff --git a/src/media-switch.cpp b/src/media-switch.cpp index 28a311f3..8de9cd54 100644 --- a/src/media-switch.cpp +++ b/src/media-switch.cpp @@ -63,7 +63,7 @@ void SceneSwitcher::on_mediaAdd_clicked() std::lock_guard lock(switcher->m); switcher->mediaSwitches.emplace_back( scene, source, transition, state, restriction, time, - (sceneName == QString(PREVIOUS_SCENE_NAME)), + (sceneName == QString(previous_scene_name)), switchText.toUtf8().constData()); } @@ -195,7 +195,7 @@ void SwitcherData::saveMediaSwitches(obs_data_t *obj) obs_data_set_string(array_obj, "source", sourceName); obs_data_set_string(array_obj, "scene", s.usePreviousScene - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : sceneName); obs_data_set_string(array_obj, "transition", transitionName); @@ -244,7 +244,7 @@ void SwitcherData::loadMediaSwitches(obs_data_t *obj) switcher->mediaSwitches.emplace_back( GetWeakSourceByName(scene), GetWeakSourceByName(source), GetWeakTransitionByName(transition), state, restriction, - time, (strcmp(scene, PREVIOUS_SCENE_NAME) == 0), + time, (strcmp(scene, previous_scene_name) == 0), mediaStr); obs_data_release(array_obj); @@ -287,7 +287,7 @@ void SceneSwitcher::setupMediaTab() for (auto &s : switcher->mediaSwitches) { std::string sourceName = GetWeakSourceName(s.source); std::string sceneName = (s.usePreviousScene) - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : GetWeakSourceName(s.scene); std::string transitionName = GetWeakSourceName(s.transition); QString listText = MakeMediaSwitchName( diff --git a/src/priority.cpp b/src/priority.cpp index 01c47055..f4619975 100644 --- a/src/priority.cpp +++ b/src/priority.cpp @@ -61,7 +61,7 @@ bool SwitcherData::prioFuncsValid() return false; for (int p : functionNamesByPriority) { - if (p < 0 || p > 7) + if (p < 0 || p > 8) return false; } return true; diff --git a/src/scene-round-trip.cpp b/src/scene-round-trip.cpp index 149ba195..22a144fc 100644 --- a/src/scene-round-trip.cpp +++ b/src/scene-round-trip.cpp @@ -78,7 +78,7 @@ void SceneSwitcher::on_sceneRoundTripAdd_clicked() std::lock_guard lock(switcher->m); switcher->sceneRoundTripSwitches.emplace_back( source1, source2, transition, delay, - (scene2Name == QString(PREVIOUS_SCENE_NAME)), + (scene2Name == QString(previous_scene_name)), text.toUtf8().constData()); } else { QListWidgetItem *item = ui->sceneRoundTrips->item(idx); @@ -93,7 +93,7 @@ void SceneSwitcher::on_sceneRoundTripAdd_clicked() s.transition = transition; s.usePreviousScene = (scene2Name == - QString(PREVIOUS_SCENE_NAME)); + QString(previous_scene_name)); s.sceneRoundTripStr = text.toUtf8().constData(); break; @@ -164,7 +164,7 @@ bool oldSceneRoundTripLoad(QFile *file) GetWeakTransitionByQString(lines[4]); if (WeakSourceValid(scene1) && - ((lines[1] == QString(PREVIOUS_SCENE_NAME)) || + ((lines[1] == QString(previous_scene_name)) || (WeakSourceValid(scene2))) && WeakSourceValid(transition)) { newSceneRoundTripSwitch.emplace_back( @@ -175,7 +175,7 @@ bool oldSceneRoundTripLoad(QFile *file) lines[4]), lines[2].toDouble() / 1000., (lines[1] == - QString(PREVIOUS_SCENE_NAME)), + QString(previous_scene_name)), lines[3].toStdString())); } lines.clear(); @@ -380,7 +380,7 @@ void SwitcherData::saveSceneRoundTripSwitches(obs_data_t *obj) sceneName1); obs_data_set_string(array_obj, "sceneRoundTripScene2", s.usePreviousScene - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : sceneName2); obs_data_set_string(array_obj, "transition", transitionName); @@ -444,7 +444,7 @@ void SwitcherData::loadSceneRoundTripSwitches(obs_data_t *obj) GetWeakSourceByName(scene1), GetWeakSourceByName(scene2), GetWeakTransitionByName(transition), delay, - (strcmp(scene2, PREVIOUS_SCENE_NAME) == 0), + (strcmp(scene2, previous_scene_name) == 0), sceneRoundTripStr); obs_data_release(array_obj); @@ -462,7 +462,7 @@ void SceneSwitcher::setupSequenceTab() for (auto &s : switcher->sceneRoundTripSwitches) { std::string sceneName1 = GetWeakSourceName(s.scene1); std::string sceneName2 = (s.usePreviousScene) - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : GetWeakSourceName(s.scene2); std::string transitionName = GetWeakSourceName(s.transition); QString text = MakeSceneRoundTripSwitchName( diff --git a/src/time-switch.cpp b/src/time-switch.cpp index f23198c3..356d9078 100644 --- a/src/time-switch.cpp +++ b/src/time-switch.cpp @@ -85,7 +85,7 @@ void SceneSwitcher::on_timeAdd_clicked() std::lock_guard lock(switcher->m); switcher->timeSwitches.emplace_back( source, transition, trigger, time, - (sceneName == QString(PREVIOUS_SCENE_NAME)), + (sceneName == QString(previous_scene_name)), text.toUtf8().constData()); QListWidgetItem *item = @@ -104,7 +104,7 @@ void SceneSwitcher::on_timeAdd_clicked() s.transition = transition; s.usePreviousScene = (sceneName == - QString(PREVIOUS_SCENE_NAME)); + QString(previous_scene_name)); s.timeSwitchStr = text.toUtf8().constData(); break; @@ -196,7 +196,6 @@ bool checkLiveTime(TimeSwitch &s, QDateTime &start, int &interval) bool checkRegularTime(TimeSwitch &s, int &interval) { - bool match = false; if (s.trigger != ANY_DAY && s.trigger != QDate::currentDate().dayOfWeek()) return false; @@ -248,7 +247,7 @@ void SwitcherData::saveTimeSwitches(obs_data_t *obj) obs_source_get_name(transition); obs_data_set_string(array_obj, "scene", s.usePreviousScene - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : sceneName); obs_data_set_string(array_obj, "transition", transitionName); @@ -293,7 +292,7 @@ void SwitcherData::loadTimeSwitches(obs_data_t *obj) switcher->timeSwitches.emplace_back( GetWeakSourceByName(scene), GetWeakTransitionByName(transition), trigger, time, - (strcmp(scene, PREVIOUS_SCENE_NAME) == 0), + (strcmp(scene, previous_scene_name) == 0), timeSwitchStr); obs_data_release(array_obj); @@ -318,7 +317,7 @@ void SceneSwitcher::setupTimeTab() for (auto &s : switcher->timeSwitches) { std::string sceneName = (s.usePreviousScene) - ? PREVIOUS_SCENE_NAME + ? previous_scene_name : GetWeakSourceName(s.scene); std::string transitionName = GetWeakSourceName(s.transition); QString listText = MakeTimeSwitchName(sceneName.c_str(), diff --git a/src/volume-control.cpp b/src/volume-control.cpp new file mode 100644 index 00000000..8ec51e64 --- /dev/null +++ b/src/volume-control.cpp @@ -0,0 +1,1002 @@ +#include "headers/volume-control.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; + +#define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x))) +#define FADER_PRECISION 4096.0 + +QWeakPointer VolumeMeter::updateTimer; + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); +} + +void VolControl::OBSVolumeLevel(void *data, + const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::SliderChanged(int vol) +{ + updateText(); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); +} + +QString VolControl::GetName() const +{ + return nameLabel->text(); +} + +void VolControl::SetName(const QString &newName) +{ + nameLabel->setText(newName); +} +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical) +{ + nameLabel = new QLabel(); + volLabel = new QLabel(); + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(4, 4, 4, 4); + mainLayout->setSpacing(2); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new QSlider(Qt::Vertical); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + controlLayout->addItem(new QSpacerItem(3, 0)); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addItem(meterLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + setMaximumWidth(110); + } else { + QHBoxLayout *volLayout = new QHBoxLayout; + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *botLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + slider = new QSlider(Qt::Horizontal); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + volLayout->addWidget(slider); + volLayout->setSpacing(5); + + botLayout->setContentsMargins(0, 0, 0, 0); + botLayout->setSpacing(0); + botLayout->addLayout(volLayout); + + mainLayout->addItem(textLayout); + mainLayout->addWidget(volMeter); + mainLayout->addItem(botLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + QFont font = nameLabel->font(); + font.setPointSize(font.pointSize() - 1); + + nameLabel->setText(sourceName); + nameLabel->setFont(font); + volLabel->setFont(font); + + slider->setMinimum(0); + slider->setMaximum(100); + + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + QWidget::connect(slider, SIGNAL(valueChanged(int)), this, + SLOT(SliderChanged(int))); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +QSlider *VolControl::GetSlider() const +{ + return slider; +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + obs_fader_destroy(obs_fader); + obs_volmeter_destroy(obs_volmeter); +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return backgroundNominalColor; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + backgroundNominalColor = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return backgroundWarningColor; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + backgroundWarningColor = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return backgroundErrorColor; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + backgroundErrorColor = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return foregroundNominalColor; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + foregroundNominalColor = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return foregroundWarningColor; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + foregroundWarningColor = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return foregroundErrorColor; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + foregroundErrorColor = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, + bool vertical) + : QWidget(parent), obs_volmeter(obs_volmeter), vertical(vertical) +{ + // Use a font that can be rendered small. + tickFont = QFont("Arial"); + tickFont.setPixelSize(7); + // Default meter color settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0xff, 0xff, 0xff); // Black + minorTickColor.setRgb(0xcc, 0xcc, 0xcc); // Black + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + + channels = (int)audio_output_get_channels(obs_get_audio()); + + handleChannelCofigurationChange(); + updateTimerRef = updateTimer.toStrongRef(); + if (!updateTimerRef) { + updateTimerRef = QSharedPointer::create(); + updateTimerRef->start(34); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); + delete tickPaintCache; +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +inline void VolumeMeter::handleChannelCofigurationChange() +{ + QMutexLocker locker(&dataMutex); + + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + + // Make room for 3 pixels meter, with one pixel between each. + // Then 9/13 pixels for ticks and numbers. + if (vertical) + setMinimumSize(displayNrAudioChannels * 4 + 14, 130); + else + setMinimumSize(130, displayNrAudioChannels * 4 + 8); + + resetLevels(); + } +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void +VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, + qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || + isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = CLAMP(displayPeak[channelNr] - decay, + currentPeak[channelNr], 0); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || + !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = + (uint64_t)(ts - + displayPeakHoldLastUpdateTime[channelNr]) * + 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = + (uint64_t)( + ts - + displayInputPeakHoldLastUpdateTime[channelNr]) * + 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = + currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = + float((currentMagnitude[channelNr] - + displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * + 0.99); + displayMagnitude[channelNr] = + CLAMP(displayMagnitude[channelNr] + attack, + (float)minimumLevel, 0); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, + qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, + timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, + int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width, + int height) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + if (i == 0 || i == -5) + painter.drawText(position - 3, height, str); + else + painter.drawText(position - 5, height, str); + painter.drawLine(position, y, position, y + 2); + } + + // Draw minor tick lines. + painter.setPen(minorTickColor); + for (int i = 0; i >= minimumLevel; i--) { + int position = int(x + width - (i * scale) - 1); + if (i % 5 != 0) + painter.drawLine(position, y, position, y + 1); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int((i * scale) - 1); + QString str = QString::number(i); + + if (i == 0) + painter.drawText(x + 5, position + 4, str); + else if (i == -60) + painter.drawText(x + 4, position, str); + else + painter.drawText(x + 4, position + 2, str); + painter.drawLine(x, position, x + 2, position); + } + + // Draw minor tick lines. + painter.setPen(minorTickColor); + for (int i = 0; i >= minimumLevel; i--) { + int position = y + int((i * scale) - 1); + if (i % 5 != 0) + painter.drawLine(x, position, x + 1, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +void VolumeMeter::ClipEnding() +{ + clipping = false; +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, + int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = int(x + width - (magnitude * scale)); + int peakPosition = int(x + width - (peak * scale)); + int peakHoldPosition = int(x + width - (peakHold * scale)); + int warningPosition = int(x + width - (warningLevel * scale)); + int errorPosition = int(x + width - (errorLevel * scale)); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, + peakPosition - minimumPosition, height, + foregroundNominalColor); + painter.fillRect(peakPosition, y, + warningPosition - peakPosition, height, + backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + foregroundNominalColor); + painter.fillRect(warningPosition, y, + peakPosition - warningPosition, height, + foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, + height, backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, + height, foregroundErrorColor); + painter.fillRect(peakPosition, y, + maximumPosition - peakPosition, height, + backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, + SLOT(ClipEnding())); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, + magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, + int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = int(y + height - (magnitude * scale)); + int peakPosition = int(y + height - (peak * scale)); + int peakHoldPosition = int(y + height - (peakHold * scale)); + int warningPosition = int(y + height - (warningLevel * scale)); + int errorPosition = int(y + height - (errorLevel * scale)); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, + peakPosition - minimumPosition, + foregroundNominalColor); + painter.fillRect(x, peakPosition, width, + warningPosition - peakPosition, + backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + foregroundNominalColor); + painter.fillRect(x, warningPosition, width, + peakPosition - warningPosition, + foregroundWarningColor); + painter.fillRect(x, peakPosition, width, + errorPosition - peakPosition, + backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + foregroundWarningColor); + painter.fillRect(x, errorPosition, width, + peakPosition - errorPosition, + foregroundErrorColor); + painter.fillRect(x, peakPosition, width, + maximumPosition - peakPosition, + backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, + SLOT(ClipEnding())); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, + magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + + const QRect rect = event->region().boundingRect(); + int width = rect.width(); + int height = rect.height(); + + handleChannelCofigurationChange(); + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + // Draw the ticks in a off-screen buffer when the widget changes size. + QSize tickPaintCacheSize; + if (vertical) + tickPaintCacheSize = QSize(14, height); + else + tickPaintCacheSize = QSize(width, 9); + if (tickPaintCache == nullptr || + tickPaintCache->size() != tickPaintCacheSize) { + delete tickPaintCache; + tickPaintCache = new QPixmap(tickPaintCacheSize); + + QColor clearColor(0, 0, 0, 0); + tickPaintCache->fill(clearColor); + + QPainter tickPainter(tickPaintCache); + if (vertical) { + tickPainter.translate(0, height); + tickPainter.scale(1, -1); + paintVTicks(tickPainter, 0, 11, + tickPaintCacheSize.height() - 11); + } else { + paintHTicks(tickPainter, 6, 0, + tickPaintCacheSize.width() - 6, + tickPaintCacheSize.height()); + } + tickPainter.end(); + } + + // Actual painting of the widget starts here. + QPainter painter(this); + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height); + painter.scale(1, -1); + painter.drawPixmap(displayNrAudioChannels * 4 - 1, 7, + *tickPaintCache); + } else { + painter.drawPixmap(0, height - 9, *tickPaintCache); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; + channelNr++) { + + int channelNrFixed = + (displayNrAudioChannels == 1 && channels > 2) + ? 2 + : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * 4, 8, 3, height - 10, + displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], + displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, 5, channelNr * 4, width - 5, 3, + displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], + displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * 4, 3, 3, 3, + displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * 4, 3, 3, + displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) + meter->update(); +}