#include "macro-condition-audio.hpp" #include "audio-helpers.hpp" #include "layout-helpers.hpp" #include "macro-helpers.hpp" #include "selection-helpers.hpp" namespace advss { constexpr int64_t nsPerMs = 1000000; const std::string MacroConditionAudio::id = "audio"; bool MacroConditionAudio::_registered = MacroConditionFactory::Register( MacroConditionAudio::id, {MacroConditionAudio::Create, MacroConditionAudioEdit::Create, "AdvSceneSwitcher.condition.audio"}); const static std::map checkTypes = { {MacroConditionAudio::Type::OUTPUT_VOLUME, "AdvSceneSwitcher.condition.audio.type.output"}, {MacroConditionAudio::Type::CONFIGURED_VOLUME, "AdvSceneSwitcher.condition.audio.type.volume"}, {MacroConditionAudio::Type::SYNC_OFFSET, "AdvSceneSwitcher.condition.audio.type.syncOffset"}, {MacroConditionAudio::Type::MONITOR, "AdvSceneSwitcher.condition.audio.type.monitor"}, {MacroConditionAudio::Type::BALANCE, "AdvSceneSwitcher.condition.audio.type.balance"}, }; const static std::map audioOutputConditionTypes = { {MacroConditionAudio::OutputCondition::ABOVE, "AdvSceneSwitcher.condition.audio.state.above"}, {MacroConditionAudio::OutputCondition::BELOW, "AdvSceneSwitcher.condition.audio.state.below"}, }; const static std::map audioVolumeConditionTypes = { {MacroConditionAudio::VolumeCondition::ABOVE, "AdvSceneSwitcher.condition.audio.state.above"}, {MacroConditionAudio::VolumeCondition::EXACT, "AdvSceneSwitcher.condition.audio.state.exact"}, {MacroConditionAudio::VolumeCondition::BELOW, "AdvSceneSwitcher.condition.audio.state.below"}, {MacroConditionAudio::VolumeCondition::MUTE, "AdvSceneSwitcher.condition.audio.state.mute"}, {MacroConditionAudio::VolumeCondition::UNMUTE, "AdvSceneSwitcher.condition.audio.state.unmute"}, }; void MacroConditionAudio::SetType(const Type &type) { _checkType = type; SetupTempVars(); } MacroConditionAudio::~MacroConditionAudio() { obs_volmeter_remove_callback(_volmeter, SetVolumeLevel, this); obs_volmeter_destroy(_volmeter); } float MacroConditionAudio::GetVolumePeak() { using namespace std::chrono_literals; using namespace std::chrono; static constexpr std::chrono::milliseconds timeout = 250ms; // OBS might rarely not update _peak quickly enough when very low // intervals are configured on the General tab. // In that case _peak might be set to negative infinity still, which // will result in unexpected behavior, so we use the previously valid // peak value instead. // // If no volume update was received within a timeout window, however, it // is assumed, that the source no longer produces any audio output and // thus a peak volume value of negative infinity is used. float peak; std::lock_guard lock(_peakMutex); auto msPassedSinceLastUpdate = duration_cast( high_resolution_clock::now() - _lastPeakUpdate); if (_lastPeakUpdate.time_since_epoch().count() != 0 && msPassedSinceLastUpdate > timeout) { peak = -std::numeric_limits::infinity(); } else { peak = _peakUpdated ? _peak : _previousPeak; } _previousPeak = peak; _peak = -std::numeric_limits::infinity(); _peakUpdated = false; return peak; } bool MacroConditionAudio::CheckOutputCondition() { bool ret = false; OBSSourceAutoRelease source = obs_weak_source_get_source(_audioSource.GetSource()); float peak = GetVolumePeak(); double curVolume = _useDb ? peak : DecibelToPercent(peak) * 100; switch (_outputCondition) { case OutputCondition::ABOVE: if (_useDb) { ret = curVolume > _volumeDB; } else { ret = curVolume > _volumePercent; } break; case OutputCondition::BELOW: if (_useDb) { ret = curVolume < _volumeDB; } else { ret = curVolume < _volumePercent; } break; default: break; } SetVariableValue(std::to_string(curVolume)); SetTempVarValue("output_volume", std::to_string(curVolume)); // Reset for next check if (_audioSource.GetType() == SourceSelection::Type::VARIABLE) { ResetVolmeter(); } return ret && source; } bool MacroConditionAudio::CheckVolumeCondition() { bool ret = false; OBSSourceAutoRelease source = obs_weak_source_get_source(_audioSource.GetSource()); float curVolume = _useDb ? PercentToDecibel(obs_source_get_volume(source)) : obs_source_get_volume(source); bool muted = obs_source_muted(source); switch (_volumeCondition) { case VolumeCondition::ABOVE: if (_useDb) { ret = curVolume > _volumeDB; } else { ret = curVolume > _volumePercent / 100.f; } SetVariableValue(std::to_string(curVolume)); break; case VolumeCondition::EXACT: if (_useDb) { ret = curVolume == _volumeDB; } else { ret = curVolume == _volumePercent / 100.f; } SetVariableValue(std::to_string(curVolume)); break; case VolumeCondition::BELOW: if (_useDb) { ret = curVolume < _volumeDB; } else { ret = curVolume < _volumePercent / 100.f; } SetVariableValue(std::to_string(curVolume)); break; case VolumeCondition::MUTE: ret = muted; SetVariableValue(""); break; case VolumeCondition::UNMUTE: ret = !muted; SetVariableValue(""); break; default: break; } SetTempVarValue("configured_volume", std::to_string(curVolume)); SetTempVarValue("muted", (source && muted) ? "true" : "false"); return ret && source; } bool MacroConditionAudio::CheckSyncOffset() { if (!_audioSource.GetSource()) { return false; } bool ret = false; OBSSourceAutoRelease source = obs_weak_source_get_source(_audioSource.GetSource()); auto curOffset = obs_source_get_sync_offset(source) / nsPerMs; if (_outputCondition == OutputCondition::ABOVE) { ret = curOffset > _syncOffset; } else { ret = curOffset < _syncOffset; } SetVariableValue(std::to_string(curOffset)); SetTempVarValue("sync_offset", std::to_string(curOffset)); return ret && source; } bool MacroConditionAudio::CheckMonitor() { if (!_audioSource.GetSource()) { return false; } bool ret = false; OBSSourceAutoRelease source = obs_weak_source_get_source(_audioSource.GetSource()); ret = obs_source_get_monitoring_type(source) == _monitorType; SetVariableValue(""); SetTempVarValue("monitor", std::to_string(obs_source_get_monitoring_type(source))); return ret && source; } bool MacroConditionAudio::CheckBalance() { if (!_audioSource.GetSource()) { return false; } bool ret = false; OBSSourceAutoRelease source = obs_weak_source_get_source(_audioSource.GetSource()); auto curBalance = obs_source_get_balance_value(source); if (_outputCondition == OutputCondition::ABOVE) { ret = curBalance > _balance; } else { ret = curBalance < _balance; } SetVariableValue(std::to_string(curBalance)); SetTempVarValue("balance", std::to_string(curBalance)); return ret && source; } bool MacroConditionAudio::CheckCondition() { bool ret = false; switch (_checkType) { case Type::OUTPUT_VOLUME: ret = CheckOutputCondition(); break; case Type::CONFIGURED_VOLUME: ret = CheckVolumeCondition(); break; case Type::SYNC_OFFSET: ret = CheckSyncOffset(); break; case Type::MONITOR: ret = CheckMonitor(); break; case Type::BALANCE: ret = CheckBalance(); break; } if (GetVariableValue().empty()) { SetVariableValue(ret ? "true" : "false"); } return ret; } bool MacroConditionAudio::Save(obs_data_t *obj) const { MacroCondition::Save(obj); _audioSource.Save(obj, "audioSource"); obs_data_set_int(obj, "monitor", _monitorType); _volumePercent.Save(obj, "volume"); _syncOffset.Save(obj, "syncOffset"); _balance.Save(obj, "balance"); obs_data_set_int(obj, "checkType", static_cast(_checkType)); obs_data_set_int(obj, "outputCondition", static_cast(_outputCondition)); obs_data_set_int(obj, "volumeCondition", static_cast(_volumeCondition)); obs_data_set_bool(obj, "useDb", _useDb); _volumeDB.Save(obj, "volumeDB"); obs_data_set_int(obj, "version", 3); return true; } obs_volmeter_t *AddVolmeterToSource(MacroConditionAudio *entry, obs_weak_source *source) { obs_volmeter_t *volmeter = obs_volmeter_create(OBS_FADER_LOG); obs_volmeter_add_callback(volmeter, MacroConditionAudio::SetVolumeLevel, entry); OBSSourceAutoRelease audioSource = obs_weak_source_get_source(source); if (!obs_volmeter_attach_source(volmeter, audioSource)) { const char *name = obs_source_get_name(audioSource); blog(LOG_WARNING, "failed to attach volmeter to source %s", name); } return volmeter; } bool MacroConditionAudio::Load(obs_data_t *obj) { MacroCondition::Load(obj); _audioSource.Load(obj, "audioSource"); _monitorType = static_cast( obs_data_get_int(obj, "monitor")); if (!obs_data_has_user_value(obj, "version")) { _volumePercent = obs_data_get_int(obj, "volume"); _syncOffset = obs_data_get_int(obj, "syncOffset"); _balance = obs_data_get_double(obj, "balance"); } else { _volumePercent.Load(obj, "volume"); _syncOffset.Load(obj, "syncOffset"); _balance.Load(obj, "balance"); } _checkType = static_cast(obs_data_get_int(obj, "checkType")); _outputCondition = static_cast( obs_data_get_int(obj, "outputCondition")); _volumeCondition = static_cast( obs_data_get_int(obj, "volumeCondition")); _volmeter = AddVolmeterToSource(this, _audioSource.GetSource()); if (obs_data_get_int(obj, "version") < 2) { // Set default values for dB handling _useDb = false; _volumeDB = 0.0; // In previous versions the scaling happened in CheckCondition() // so these values must now be converted to enable compatibility // with the decibel value based checks. OBSDataAutoRelease data = obs_data_get_obj(obj, "volume"); auto oldFixedValue = obs_data_get_int(data, "value"); _volumeDB = ((double)oldFixedValue / 1.7) - 60; _volumePercent = DecibelToPercent(_volumeDB) * 100; } else { _useDb = obs_data_get_bool(obj, "useDb"); _volumeDB.Load(obj, "volumeDB"); } return true; } std::string MacroConditionAudio::GetShortDesc() const { return _audioSource.ToString(); } void MacroConditionAudio::SetVolumeLevel(void *data, const float *, const float peak[MAX_AUDIO_CHANNELS], const float *) { auto c = static_cast(data); const auto macro = c->GetMacro(); if (MacroIsPaused(macro)) { return; } std::lock_guard lock(c->_peakMutex); for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { if (peak[i] > c->_peak) { c->_peak = peak[i]; } } c->_peakUpdated = true; c->_lastPeakUpdate = std::chrono::high_resolution_clock::now(); } void MacroConditionAudio::ResetVolmeter() { obs_volmeter_remove_callback(_volmeter, SetVolumeLevel, this); obs_volmeter_destroy(_volmeter); _volmeter = AddVolmeterToSource(this, _audioSource.GetSource()); } void MacroConditionAudio::SetupTempVars() { MacroCondition::SetupTempVars(); switch (_checkType) { case Type::OUTPUT_VOLUME: AddTempvar( "output_volume", obs_module_text( "AdvSceneSwitcher.tempVar.audio.output_volume"), obs_module_text( "AdvSceneSwitcher.tempVar.audio.output_volume.description")); break; case Type::CONFIGURED_VOLUME: AddTempvar( "configured_volume", obs_module_text( "AdvSceneSwitcher.tempVar.audio.configured_volume"), obs_module_text( "AdvSceneSwitcher.tempVar.audio.configured_volume.description")); AddTempvar("muted", obs_module_text( "AdvSceneSwitcher.tempVar.audio.muted")); break; case Type::SYNC_OFFSET: AddTempvar( "sync_offset", obs_module_text( "AdvSceneSwitcher.tempVar.audio.sync_offset")); break; case Type::MONITOR: AddTempvar("monitor", obs_module_text( "AdvSceneSwitcher.tempVar.audio.monitor")); break; case Type::BALANCE: AddTempvar("balance", obs_module_text( "AdvSceneSwitcher.tempVar.audio.balance")); break; default: break; } } static inline void populateCheckTypes(QComboBox *list) { list->clear(); for (const auto &[type, name] : checkTypes) { if (type == MacroConditionAudio::Type::MONITOR) { if (obs_audio_monitoring_available()) { list->addItem(obs_module_text(name.c_str()), static_cast(type)); } } else { list->addItem(obs_module_text(name.c_str()), static_cast(type)); } } } static inline void populateOutputConditionSelection(QComboBox *list) { list->clear(); for (const auto &[_, name] : audioOutputConditionTypes) { list->addItem(obs_module_text(name.c_str())); } } static inline void populateVolumeConditionSelection(QComboBox *list) { list->clear(); for (const auto &[_, name] : audioVolumeConditionTypes) { list->addItem(obs_module_text(name.c_str())); } } MacroConditionAudioEdit::MacroConditionAudioEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _checkTypes(new QComboBox()), _sources(new SourceSelectionWidget(this, QStringList(), true)), _condition(new QComboBox()), _volumePercent(new VariableDoubleSpinBox()), _volumeDB(new VariableDoubleSpinBox), _percentDBToggle(new QPushButton), _syncOffset(new VariableSpinBox()), _monitorTypes(new QComboBox), _balance(new SliderSpinBox(0., 1., "")) { _volumePercent->setSuffix("%"); _volumePercent->setMaximum(100); _volumePercent->setMinimum(0); _volumeDB->setMinimum(-100); _volumeDB->setMaximum(0); _volumeDB->setSuffix("dB"); _volumeDB->specialValueText("-inf"); _syncOffset->setMinimum(-950); _syncOffset->setMaximum(20000); _syncOffset->setSuffix("ms"); auto sources = GetAudioSourceNames(); sources.sort(); _sources->SetSourceNameList(sources); QWidget::connect(_checkTypes, SIGNAL(currentIndexChanged(int)), this, SLOT(CheckTypeChanged(int))); QWidget::connect( _volumePercent, SIGNAL(NumberVariableChanged(const NumberVariable &)), this, SLOT(VolumePercentChanged(const NumberVariable &))); QWidget::connect( _syncOffset, SIGNAL(NumberVariableChanged(const NumberVariable &)), this, SLOT(SyncOffsetChanged(const NumberVariable &))); QWidget::connect(_monitorTypes, SIGNAL(currentIndexChanged(int)), this, SLOT(MonitorTypeChanged(int))); QWidget::connect( _balance, SIGNAL(DoubleValueChanged(const NumberVariable &)), this, SLOT(BalanceChanged(const NumberVariable &))); QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, SLOT(ConditionChanged(int))); QWidget::connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this, SLOT(SourceChanged(const SourceSelection &))); QWidget::connect( _volumeDB, SIGNAL(NumberVariableChanged(const NumberVariable &)), this, SLOT(VolumeDBChanged(const NumberVariable &))); QWidget::connect(_percentDBToggle, SIGNAL(clicked()), this, SLOT(PercentDBClicked())); populateCheckTypes(_checkTypes); PopulateMonitorTypeSelection(_monitorTypes); QHBoxLayout *switchLayout = new QHBoxLayout; std::unordered_map widgetPlaceholders = { {"{{checkType}}", _checkTypes}, {"{{audioSources}}", _sources}, {"{{volume}}", _volumePercent}, {"{{syncOffset}}", _syncOffset}, {"{{monitorTypes}}", _monitorTypes}, {"{{balance}}", _balance}, {"{{condition}}", _condition}, {"{{volumeDB}}", _volumeDB}, {"{{percentDBToggle}}", _percentDBToggle}, }; PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.audio.entry"), switchLayout, widgetPlaceholders); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(switchLayout); mainLayout->addWidget(_balance); setLayout(mainLayout); _entryData = entryData; UpdateEntryData(); _loading = false; } void MacroConditionAudioEdit::UpdateVolmeterSource() { delete _volMeter; OBSSourceAutoRelease soruce = obs_weak_source_get_source( _entryData->_audioSource.GetSource()); _volMeter = new VolControl(soruce.Get()); auto layout = this->layout(); layout->addWidget(_volMeter); QWidget::connect(_volMeter->GetSlider(), &DoubleSlider::DoubleValChanged, [=](double) { SyncSliderAndValueSelection(true); }); // Slider will default to 0 so set it manually once SyncSliderAndValueSelection(false); } void MacroConditionAudioEdit::SourceChanged(const SourceSelection &source) { if (_loading || !_entryData) { return; } { auto lock = LockContext(); _entryData->_audioSource = source; _entryData->ResetVolmeter(); } UpdateVolmeterSource(); SetWidgetVisibility(); emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); } void MacroConditionAudioEdit::VolumePercentChanged( const NumberVariable &vol) { if (_loading || !_entryData) { return; } { auto lock = LockContext(); _entryData->_volumePercent = vol; } SyncSliderAndValueSelection(false); } void MacroConditionAudioEdit::SyncOffsetChanged(const NumberVariable &value) { if (_loading || !_entryData) { return; } auto lock = LockContext(); _entryData->_syncOffset = value; } void MacroConditionAudioEdit::MonitorTypeChanged(int value) { if (_loading || !_entryData) { return; } auto lock = LockContext(); _entryData->_monitorType = static_cast(value); } void MacroConditionAudioEdit::BalanceChanged(const NumberVariable &value) { if (_loading || !_entryData) { return; } auto lock = LockContext(); _entryData->_balance = value; } void MacroConditionAudioEdit::VolumeDBChanged( const NumberVariable &value) { if (_loading || !_entryData) { return; } { auto lock = LockContext(); _entryData->_volumeDB = value; } SyncSliderAndValueSelection(false); } void MacroConditionAudioEdit::PercentDBClicked() { if (_loading || !_entryData) { return; } auto lock = LockContext(); _entryData->_useDb = !_entryData->_useDb; SetWidgetVisibility(); } void MacroConditionAudioEdit::SyncSliderAndValueSelection(bool sliderMoved) { if (!_entryData) { return; } if (sliderMoved) { auto sliderPosition = _volMeter->GetSlider()->DoubleValue(); // Adjust to the dB scale on the volume meter widget auto dBScaleValue = ((sliderPosition * 3.0) / 5.0) - 60.0; if (_entryData->_useDb) { _volumeDB->SetFixedValue(dBScaleValue); const QSignalBlocker blocker(this); _volumePercent->SetFixedValue( DecibelToPercent(dBScaleValue) * 100); } else { _volumePercent->SetFixedValue( DecibelToPercent(dBScaleValue) * 100); const QSignalBlocker blocker(this); _volumeDB->SetFixedValue(dBScaleValue); } } else { const QSignalBlocker blocker(this); double dBValue = _entryData->_useDb ? _entryData->_volumeDB.GetFixedValue() : PercentToDecibel(_entryData->_volumePercent / 100.0); auto sliderPosition = (dBValue + 60.0) * 5 / 3.0; _volMeter->GetSlider()->SetDoubleVal(sliderPosition); } } void MacroConditionAudioEdit::ConditionChanged(int cond) { if (_loading || !_entryData) { return; } auto lock = LockContext(); if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::BALANCE || _entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) { _entryData->_outputCondition = static_cast(cond); } else { _entryData->_volumeCondition = static_cast(cond); } SetWidgetVisibility(); } void MacroConditionAudioEdit::CheckTypeChanged(int idx) { if (_loading || !_entryData) { return; } auto lock = LockContext(); _entryData->SetType(static_cast( _checkTypes->itemData(idx).toInt())); const QSignalBlocker b(_condition); if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::BALANCE || _entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) { populateOutputConditionSelection(_condition); } else if (_entryData->GetType() == MacroConditionAudio::Type::CONFIGURED_VOLUME) { populateVolumeConditionSelection(_condition); } SetWidgetVisibility(); } void MacroConditionAudioEdit::UpdateEntryData() { if (!_entryData) { return; } _sources->SetSource(_entryData->_audioSource); _volumePercent->SetValue(_entryData->_volumePercent); _volumeDB->SetValue(_entryData->_volumeDB); _syncOffset->SetValue(_entryData->_syncOffset); _monitorTypes->setCurrentIndex(_entryData->_monitorType); _balance->SetDoubleValue(_entryData->_balance); _checkTypes->setCurrentIndex( _checkTypes->findData(static_cast(_entryData->GetType()))); if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::BALANCE || _entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) { populateOutputConditionSelection(_condition); _condition->setCurrentIndex( static_cast(_entryData->_outputCondition)); } else if (_entryData->GetType() == MacroConditionAudio::Type::CONFIGURED_VOLUME) { populateVolumeConditionSelection(_condition); _condition->setCurrentIndex( static_cast(_entryData->_volumeCondition)); } UpdateVolmeterSource(); SetWidgetVisibility(); } bool MacroConditionAudioEdit::HasVolumeControl() const { return _entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME || (_entryData->GetType() == MacroConditionAudio::Type::CONFIGURED_VOLUME && (_entryData->_volumeCondition == MacroConditionAudio::VolumeCondition::ABOVE || _entryData->_volumeCondition == MacroConditionAudio::VolumeCondition::EXACT || _entryData->_volumeCondition == MacroConditionAudio::VolumeCondition::BELOW)); } void MacroConditionAudioEdit::SetWidgetVisibility() { if (!_entryData) { return; } _condition->setVisible( _entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::CONFIGURED_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::BALANCE || _entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET); _syncOffset->setVisible(_entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET); _monitorTypes->setVisible(_entryData->GetType() == MacroConditionAudio::Type::MONITOR); _balance->setVisible(_entryData->GetType() == MacroConditionAudio::Type::BALANCE); _volMeter->setVisible(_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME); _volumePercent->setVisible(HasVolumeControl() && !_entryData->_useDb); _volumeDB->setVisible(HasVolumeControl() && _entryData->_useDb); _percentDBToggle->setText(_entryData->_useDb ? "dB" : "%"); _percentDBToggle->setVisible(HasVolumeControl()); adjustSize(); } } // namespace advss