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();
+}