diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0c606ffe..dfae3754 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -251,6 +251,7 @@ AdvSceneSwitcher.action.audio.type.mute="Mute" AdvSceneSwitcher.action.audio.type.unmute="Unmute" AdvSceneSwitcher.action.audio.type.sourceVolume="Set source volume" AdvSceneSwitcher.action.audio.type.masterVolume="Set master volume" +AdvSceneSwitcher.action.audio.fade="{{fade}}Fade over {{duration}} seconds" AdvSceneSwitcher.action.audio.entry="{{actions}} {{audioSources}} {{volume}}" AdvSceneSwitcher.action.recording="Recording" AdvSceneSwitcher.action.recording.type.stop="Stop recording" diff --git a/src/advanced-scene-switcher.cpp b/src/advanced-scene-switcher.cpp index 69485aea..5ccbde54 100644 --- a/src/advanced-scene-switcher.cpp +++ b/src/advanced-scene-switcher.cpp @@ -444,6 +444,13 @@ void SwitcherData::Stop() server.stop(); client.disconnect(); + for (auto &t : audioHelperThreads) { + if (t.joinable()) { + t.join(); + } + } + audioHelperThreads.clear(); + if (showSystemTrayNotifications) { DisplayTrayMessage( obs_module_text("AdvSceneSwitcher.pluginName"), diff --git a/src/headers/macro-action-audio.hpp b/src/headers/macro-action-audio.hpp index d1f0ec75..f4aab536 100644 --- a/src/headers/macro-action-audio.hpp +++ b/src/headers/macro-action-audio.hpp @@ -1,7 +1,10 @@ #pragma once -#include -#include #include "macro-action-edit.hpp" +#include "duration-control.hpp" + +#include +#include +#include enum class AudioAction { MUTE, @@ -26,6 +29,8 @@ public: OBSWeakSource _audioSource; AudioAction _action = AudioAction::MUTE; int _volume = 0; + bool _fade = false; + Duration _duration; private: static bool _registered; @@ -53,6 +58,8 @@ private slots: void SourceChanged(const QString &text); void ActionChanged(int value); void VolumeChanged(int value); + void FadeChanged(int value); + void DurationChanged(double seconds); signals: void HeaderInfoChanged(const QString &); @@ -60,9 +67,11 @@ protected: QComboBox *_audioSources; QComboBox *_actions; QSpinBox *_volumePercent; + QCheckBox *_fade; + DurationSelection *_duration; + QHBoxLayout *_fadeLayout; std::shared_ptr _entryData; private: - QHBoxLayout *_mainLayout; bool _loading = true; }; diff --git a/src/headers/switcher-data-structs.hpp b/src/headers/switcher-data-structs.hpp index 7fdf6270..7b98445e 100644 --- a/src/headers/switcher-data-structs.hpp +++ b/src/headers/switcher-data-structs.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "scene-group.hpp" #include "scene-trigger.hpp" @@ -92,6 +93,10 @@ struct SwitcherData { StartupBehavior startupBehavior = PERSIST; AutoStartEvent autoStartEvent = AutoStartEvent::NEVER; + std::vector audioHelperThreads; + std::atomic_bool masterAudioFadeActive = {false}; + std::unordered_map activeAudioFades; + Duration cooldown; std::chrono::high_resolution_clock::time_point lastMatchTime; diff --git a/src/macro-action-audio.cpp b/src/macro-action-audio.cpp index aa4fceb3..473a6208 100644 --- a/src/macro-action-audio.cpp +++ b/src/macro-action-audio.cpp @@ -18,6 +18,94 @@ const static std::map actionTypes = { "AdvSceneSwitcher.action.audio.type.masterVolume"}, }; +constexpr auto fadeInterval = std::chrono::milliseconds(100); +constexpr float minFade = 0.000001f; + +void fadeSourceVolume(Duration duration, float vol, OBSWeakSource audioSource) +{ + auto s = obs_weak_source_get_source(audioSource); + if (!s) { + return; + } + float curVol = obs_source_get_volume(s); + obs_source_release(s); + bool volIncrease = curVol <= vol; + int nrSteps = duration.seconds * 1000 / fadeInterval.count(); + float volDiff = (volIncrease) ? vol - curVol : curVol - vol; + float volStep = volDiff / nrSteps; + + if (volStep < minFade) { + switcher->activeAudioFades[GetWeakSourceName(audioSource)] = + false; + return; + } + + for (int step = 0; step < nrSteps && !switcher->stop; ++step) { + auto s = obs_weak_source_get_source(audioSource); + if (!s) { + return; + } + curVol = (volIncrease) ? curVol + volStep : curVol - volStep; + obs_source_set_volume(s, curVol); + std::this_thread::sleep_for(fadeInterval); + obs_source_release(s); + } + + switcher->activeAudioFades[GetWeakSourceName(audioSource)] = false; +} +void fadeMasterVolume(Duration duration, float vol) +{ + float curVol = obs_get_master_volume(); + bool volIncrease = curVol <= vol; + int nrSteps = duration.seconds * 1000 / fadeInterval.count(); + float volDiff = (volIncrease) ? vol - curVol : curVol - vol; + float volStep = volDiff / nrSteps; + + if (volStep < minFade) { + switcher->masterAudioFadeActive = false; + return; + } + + for (int step = 0; step < nrSteps && !switcher->stop; ++step) { + curVol = (volIncrease) ? curVol + volStep : curVol - volStep; + obs_set_master_volume(curVol); + std::this_thread::sleep_for(fadeInterval); + } + + switcher->masterAudioFadeActive = false; +} + +void startSourceFade(Duration &duration, float vol, OBSWeakSource audioSource) +{ + if (!audioSource) { + return; + } + auto it = + switcher->activeAudioFades.find(GetWeakSourceName(audioSource)); + if (it != switcher->activeAudioFades.end() && it->second == true) { + blog(LOG_WARNING, + "Audio fade for volume of %s already active! New fade request will be ignored!", + GetWeakSourceName(audioSource).c_str()); + return; + } + switcher->activeAudioFades[GetWeakSourceName(audioSource)] = true; + switcher->audioHelperThreads.emplace_back(fadeSourceVolume, duration, + vol, audioSource); +} + +void startMasterFade(Duration &duration, float vol) +{ + + if (switcher->masterAudioFadeActive) { + blog(LOG_WARNING, + "Audio fade for master volume already active! New fade request will be ignored!"); + return; + } + switcher->masterAudioFadeActive = true; + switcher->audioHelperThreads.emplace_back(fadeMasterVolume, duration, + vol); +} + bool MacroActionAudio::PerformAction() { auto s = obs_weak_source_get_source(_audioSource); @@ -29,10 +117,19 @@ bool MacroActionAudio::PerformAction() obs_source_set_muted(s, false); break; case AudioAction::SOURCE_VOLUME: - obs_source_set_volume(s, (float)_volume / 100.0f); + if (_fade) { + startSourceFade(_duration, (float)_volume / 100.0f, + _audioSource); + } else { + obs_source_set_volume(s, (float)_volume / 100.0f); + } break; case AudioAction::MASTER_VOLUME: - obs_set_master_volume((float)_volume / 100.0f); + if (_fade) { + startMasterFade(_duration, (float)_volume / 100.0f); + } else { + obs_set_master_volume((float)_volume / 100.0f); + } break; default: break; @@ -46,9 +143,10 @@ void MacroActionAudio::LogAction() auto it = actionTypes.find(_action); if (it != actionTypes.end()) { vblog(LOG_INFO, - "performed action \"%s\" for source \"%s\" with volume %d", + "performed action \"%s\" for source \"%s\" with volume %d with fade %d %f", it->second.c_str(), - GetWeakSourceName(_audioSource).c_str(), _volume); + GetWeakSourceName(_audioSource).c_str(), _volume, _fade, + _duration.seconds); } else { blog(LOG_WARNING, "ignored unknown audio action %d", static_cast(_action)); @@ -58,20 +156,24 @@ void MacroActionAudio::LogAction() bool MacroActionAudio::Save(obs_data_t *obj) { MacroAction::Save(obj); + _duration.Save(obj); obs_data_set_string(obj, "audioSource", GetWeakSourceName(_audioSource).c_str()); obs_data_set_int(obj, "action", static_cast(_action)); obs_data_set_int(obj, "volume", _volume); + obs_data_set_bool(obj, "fade", _fade); return true; } bool MacroActionAudio::Load(obs_data_t *obj) { MacroAction::Load(obj); + _duration.Load(obj); const char *audioSourceName = obs_data_get_string(obj, "audioSource"); _audioSource = GetWeakSourceByName(audioSourceName); _action = static_cast(obs_data_get_int(obj, "action")); _volume = obs_data_get_int(obj, "volume"); + _fade = obs_data_get_bool(obj, "fade"); return true; } @@ -100,6 +202,8 @@ MacroActionAudioEdit::MacroActionAudioEdit( _volumePercent->setMinimum(0); _volumePercent->setMaximum(2000); _volumePercent->setSuffix("%"); + _fade = new QCheckBox(); + _duration = new DurationSelection(parent, false); populateActionSelection(_actions); populateAudioSelection(_audioSources); @@ -111,15 +215,25 @@ MacroActionAudioEdit::MacroActionAudioEdit( SLOT(SourceChanged(const QString &))); QWidget::connect(_volumePercent, SIGNAL(valueChanged(int)), this, SLOT(VolumeChanged(int))); + QWidget::connect(_fade, SIGNAL(stateChanged(int)), this, + SLOT(FadeChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); - QHBoxLayout *mainLayout = new QHBoxLayout; std::unordered_map widgetPlaceholders = { - {"{{audioSources}}", _audioSources}, - {"{{actions}}", _actions}, - {"{{volume}}", _volumePercent}, + {"{{audioSources}}", _audioSources}, {"{{actions}}", _actions}, + {"{{volume}}", _volumePercent}, {"{{fade}}", _fade}, + {"{{duration}}", _duration}, }; + QHBoxLayout *entryLayout = new QHBoxLayout; placeWidgets(obs_module_text("AdvSceneSwitcher.action.audio.entry"), - mainLayout, widgetPlaceholders); + entryLayout, widgetPlaceholders); + _fadeLayout = new QHBoxLayout; + placeWidgets(obs_module_text("AdvSceneSwitcher.action.audio.fade"), + _fadeLayout, widgetPlaceholders); + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addLayout(entryLayout); + mainLayout->addLayout(_fadeLayout); setLayout(mainLayout); _entryData = entryData; @@ -140,19 +254,11 @@ bool hasSourceControl(AudioAction action) void MacroActionAudioEdit::SetWidgetVisibility() { - if (hasVolumeControl(_entryData->_action)) { - _volumePercent->show(); - } else { - _volumePercent->hide(); - } - - if (hasSourceControl(_entryData->_action)) { - _audioSources->show(); - } else { - _audioSources->hide(); - } + _volumePercent->setVisible(hasVolumeControl(_entryData->_action)); + _audioSources->setVisible(hasSourceControl(_entryData->_action)); + setLayoutVisible(_fadeLayout, hasVolumeControl(_entryData->_action)); + adjustSize(); } - void MacroActionAudioEdit::UpdateEntryData() { if (!_entryData) { @@ -163,7 +269,8 @@ void MacroActionAudioEdit::UpdateEntryData() GetWeakSourceName(_entryData->_audioSource).c_str()); _actions->setCurrentIndex(static_cast(_entryData->_action)); _volumePercent->setValue(_entryData->_volume); - + _fade->setChecked(_entryData->_fade); + _duration->SetDuration(_entryData->_duration); SetWidgetVisibility(); } @@ -199,3 +306,23 @@ void MacroActionAudioEdit::VolumeChanged(int value) std::lock_guard lock(switcher->m); _entryData->_volume = value; } + +void MacroActionAudioEdit::FadeChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_fade = value; +} + +void MacroActionAudioEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +}