Files
obs-studio/frontend/components/VolumeControl.cpp
2026-01-13 16:34:33 -05:00

973 lines
30 KiB
C++

#include "VolumeControl.hpp"
#include <components/MuteCheckBox.hpp>
#include <components/VolumeMeter.hpp>
#include <components/VolumeName.hpp>
#include <components/VolumeSlider.hpp>
#include <dialogs/NameDialog.hpp>
#include <widgets/OBSBasic.hpp>
#include <QMessageBox>
#include <QObjectCleanupHandler>
#include "moc_VolumeControl.cpp"
namespace {
bool isSourceUnassigned(obs_source_t *source)
{
uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1));
obs_monitoring_type mt = obs_source_get_monitoring_type(source);
return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY;
}
void showUnassignedWarning(const char *name)
{
auto msgBox = [=]() {
QMessageBox msgbox(App()->GetMainWindow());
msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title"));
msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name));
msgbox.setIcon(QMessageBox::Icon::Information);
msgbox.addButton(QMessageBox::Ok);
QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain"));
msgbox.setCheckBox(cb);
msgbox.exec();
if (cb->isChecked()) {
config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true);
config_save_safe(App()->GetUserConfig(), "tmp", nullptr);
}
};
QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox));
}
} // namespace
VolumeControl::VolumeControl(obs_source_t *source, QWidget *parent, bool vertical)
: weakSource_(OBSGetWeakRef(source)),
obs_fader(obs_fader_create(OBS_FADER_LOG)),
vertical(vertical),
contextMenu(nullptr),
QFrame(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent, true);
utils = std::make_unique<idian::Utils>(this);
uuid = obs_source_get_uuid(source);
mainLayout = new QBoxLayout(QBoxLayout::LeftToRight, this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
setLayout(mainLayout);
categoryLabel = new QLabel("Active");
categoryLabel->setAlignment(Qt::AlignCenter);
utils->addClass(categoryLabel, "mixer-category");
utils->addClass(categoryLabel, "text-tiny");
nameButton = new VolumeName(source, this);
nameButton->setMaximumWidth(140);
utils->addClass(nameButton, "text-small");
utils->addClass(nameButton, "mixer-name");
muteButton = new QPushButton(this);
muteButton->setCheckable(true);
utils->addClass(muteButton, "btn-mute");
monitorButton = new QPushButton(this);
monitorButton->setCheckable(true);
utils->addClass(monitorButton, "btn-monitor");
volumeLabel = new QLabel(this);
volumeLabel->setObjectName("volLabel");
slider = new VolumeSlider(obs_fader, Qt::Horizontal, this);
slider->setMinimum(0);
slider->setMaximum(int(FADER_PRECISION));
sourceName = obs_source_get_name(source);
setObjectName(sourceName);
utils->applyStateStylingEventFilter(muteButton);
utils->applyStateStylingEventFilter(monitorButton);
volumeMeter = new VolumeMeter(this, source);
bool muted = obs_source_muted(source);
bool unassigned = isSourceUnassigned(source);
volumeMeter->setMuted(muted || unassigned);
setLayoutVertical(vertical);
setName(sourceName);
obs_fader_add_callback(obs_fader, obsVolumeChanged, this);
obsSignals.reserve(9);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "mute", obsVolumeMuted, this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", obsMixersOrMonitoringChanged,
this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", obsMixersOrMonitoringChanged,
this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "activate", VolumeControl::obsSourceActivated,
this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "deactivate",
VolumeControl::obsSourceDeactivated, this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "audio_activate",
VolumeControl::obsSourceActivated, this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "audio_deactivate",
VolumeControl::obsSourceDeactivated, this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "remove", VolumeControl::obsSourceDestroy, this);
obsSignals.emplace_back(obs_source_get_signal_handler(source), "destroy", VolumeControl::obsSourceDestroy,
this);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, &VolumeControl::showVolumeControlMenu);
connect(nameButton, &VolumeName::renamed, this, &VolumeControl::setName);
connect(nameButton, &VolumeName::clicked, this, [&]() { showVolumeControlMenu(); });
connect(slider, &VolumeSlider::valueChanged, this, &VolumeControl::sliderChanged);
connect(muteButton, &QPushButton::clicked, this, &VolumeControl::handleMuteButton);
connect(monitorButton, &QPushButton::clicked, this, &VolumeControl::handleMonitorButton);
OBSBasic *main = OBSBasic::Get();
if (main) {
connect(main, &OBSBasic::profileSettingChanged, this,
[this](const std::string &category, const std::string &name) {
if (category == "Audio" && name == "MeterDecayRate") {
updateDecayRate();
} else if (category == "Audio" && name == "PeakMeterType") {
updatePeakMeterType();
}
});
}
obs_fader_attach_source(obs_fader, source);
// Call volume changed once to init the slider position and label
changeVolume();
updateMixerState();
}
VolumeControl::~VolumeControl()
{
obs_fader_remove_callback(obs_fader, obsVolumeChanged, this);
obsSignals.clear();
if (contextMenu) {
contextMenu->close();
}
}
void VolumeControl::obsVolumeChanged(void *data, float)
{
VolumeControl *volControl = static_cast<VolumeControl *>(data);
QMetaObject::invokeMethod(volControl, "changeVolume", Qt::QueuedConnection);
}
void VolumeControl::obsVolumeMuted(void *data, calldata_t *)
{
VolumeControl *volControl = static_cast<VolumeControl *>(data);
QMetaObject::invokeMethod(volControl, "updateMixerState", Qt::QueuedConnection);
}
void VolumeControl::obsMixersOrMonitoringChanged(void *data, calldata_t *)
{
VolumeControl *volControl = static_cast<VolumeControl *>(data);
QMetaObject::invokeMethod(volControl, "updateMixerState", Qt::QueuedConnection);
}
void VolumeControl::obsSourceActivated(void *data, calldata_t *)
{
QMetaObject::invokeMethod(static_cast<VolumeControl *>(data), "sourceActiveChanged", Qt::QueuedConnection,
Q_ARG(bool, true));
}
void VolumeControl::obsSourceDeactivated(void *data, calldata_t *)
{
QMetaObject::invokeMethod(static_cast<VolumeControl *>(data), "sourceActiveChanged", Qt::QueuedConnection,
Q_ARG(bool, false));
}
void VolumeControl::obsSourceDestroy(void *data, calldata_t *)
{
QMetaObject::invokeMethod(static_cast<VolumeControl *>(data), "handleSourceDestroyed", Qt::QueuedConnection);
}
void VolumeControl::setLayoutVertical(bool vertical)
{
QBoxLayout *newLayout = new QBoxLayout(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
newLayout->setContentsMargins(0, 0, 0, 0);
newLayout->setSpacing(0);
if (vertical) {
setMaximumWidth(110);
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
QHBoxLayout *categoryLayout = new QHBoxLayout;
QHBoxLayout *nameLayout = new QHBoxLayout;
QHBoxLayout *controlLayout = new QHBoxLayout;
QHBoxLayout *volLayout = new QHBoxLayout;
QFrame *meterFrame = new QFrame;
QHBoxLayout *meterLayout = new QHBoxLayout;
volumeMeter->setVertical(true);
volumeMeter->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
slider->setOrientation(Qt::Vertical);
slider->setLayoutDirection(Qt::LeftToRight);
slider->setDisplayTicks(true);
nameButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
categoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
volumeLabel->setAlignment(Qt::AlignLeft);
categoryLayout->setAlignment(Qt::AlignCenter);
nameLayout->setAlignment(Qt::AlignCenter);
meterLayout->setAlignment(Qt::AlignCenter);
controlLayout->setAlignment(Qt::AlignCenter);
volLayout->setAlignment(Qt::AlignCenter);
meterFrame->setObjectName("volMeterFrame");
categoryLayout->setContentsMargins(0, 0, 0, 0);
categoryLayout->setSpacing(0);
categoryLayout->addWidget(categoryLabel);
nameLayout->setContentsMargins(0, 0, 0, 0);
nameLayout->setSpacing(0);
nameLayout->addWidget(nameButton);
controlLayout->setContentsMargins(0, 0, 0, 0);
controlLayout->setSpacing(0);
// Add Headphone (audio monitoring) widget here
controlLayout->addWidget(muteButton);
controlLayout->addWidget(monitorButton);
meterLayout->setContentsMargins(0, 0, 0, 0);
meterLayout->setSpacing(0);
meterLayout->addWidget(slider);
meterLayout->addWidget(volumeMeter);
meterFrame->setLayout(meterLayout);
volLayout->setContentsMargins(0, 0, 0, 0);
volLayout->setSpacing(0);
volLayout->addWidget(volumeLabel);
volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Maximum));
newLayout->addItem(categoryLayout);
newLayout->addItem(nameLayout);
newLayout->addItem(volLayout);
newLayout->addWidget(meterFrame);
newLayout->addItem(controlLayout);
newLayout->setStretch(0, 0);
newLayout->setStretch(1, 0);
newLayout->setStretch(2, 0);
newLayout->setStretch(3, 1);
newLayout->setStretch(4, 0);
volumeMeter->setFocusProxy(slider);
} else {
setMaximumWidth(QWIDGETSIZE_MAX);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
QVBoxLayout *textLayout = new QVBoxLayout;
QHBoxLayout *controlLayout = new QHBoxLayout;
QFrame *meterFrame = new QFrame;
QVBoxLayout *meterLayout = new QVBoxLayout;
QVBoxLayout *buttonLayout = new QVBoxLayout;
volumeMeter->setVertical(false);
volumeMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
slider->setOrientation(Qt::Horizontal);
slider->setLayoutDirection(Qt::LeftToRight);
slider->setDisplayTicks(true);
nameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
categoryLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
volumeLabel->setAlignment(Qt::AlignRight);
QHBoxLayout *textTopLayout = new QHBoxLayout;
textTopLayout->setContentsMargins(0, 0, 0, 0);
textTopLayout->addWidget(categoryLabel);
textTopLayout->addWidget(volumeLabel);
textLayout->setContentsMargins(0, 0, 0, 0);
textLayout->addItem(textTopLayout);
textLayout->addWidget(nameButton);
meterFrame->setObjectName("volMeterFrame");
meterFrame->setLayout(meterLayout);
meterLayout->setContentsMargins(0, 0, 0, 0);
meterLayout->setSpacing(0);
meterLayout->addWidget(slider);
meterLayout->addWidget(volumeMeter);
meterLayout->setStretch(0, 2);
meterLayout->setStretch(1, 2);
buttonLayout->setContentsMargins(0, 0, 0, 0);
buttonLayout->setSpacing(0);
buttonLayout->addWidget(muteButton);
buttonLayout->addWidget(monitorButton);
controlLayout->addItem(buttonLayout);
controlLayout->addWidget(meterFrame);
newLayout->addItem(textLayout);
newLayout->addItem(controlLayout);
newLayout->setStretch(0, 3);
newLayout->setStretch(1, 6);
volumeMeter->setFocusProxy(slider);
}
QWidget().setLayout(mainLayout);
setLayout(newLayout);
mainLayout = newLayout;
updateTabOrder();
adjustSize();
}
void VolumeControl::showVolumeControlMenu(QPoint pos)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
QMenu *popup = new QMenu(this);
// Create menu QActions
QAction *lockAction = new QAction(QTStr("LockVolume"), popup);
lockAction->setCheckable(true);
lockAction->setChecked(mixerStatus().has(VolumeControl::MixerStatus::Locked));
bool isGlobal = mixerStatus().has(VolumeControl::MixerStatus::Global);
QAction *pinAction = new QAction(QTStr("Basic.AudioMixer.Pin"), popup);
bool isPinned = mixerStatus().has(VolumeControl::MixerStatus::Pinned);
if (isPinned) {
pinAction->setText(QTStr("Basic.AudioMixer.Unpin"));
}
QAction *hideAction = new QAction(QTStr("Basic.AudioMixer.Hide"), popup);
bool isHidden = mixerStatus().has(VolumeControl::MixerStatus::Hidden);
if (isHidden && !isGlobal) {
hideAction->setText(QTStr("Basic.AudioMixer.Unhide"));
}
QAction *unhideAllAction = new QAction(QTStr("UnhideAll"), popup);
QAction *mixerRenameAction = new QAction(QTStr("Rename"), popup);
QAction *copyFiltersAction = new QAction(QTStr("Copy.Filters"), popup);
QAction *pasteFiltersAction = new QAction(QTStr("Paste.Filters"), popup);
QAction *filtersAction = new QAction(QTStr("Filters"), popup);
QAction *propertiesAction = new QAction(QTStr("Properties"), popup);
// Set properties on actions that require source reference
hideAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
pinAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
mixerRenameAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
copyFiltersAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
pasteFiltersAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
filtersAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
propertiesAction->setProperty("source", QVariant::fromValue<OBSSource>(source));
// Connect actions to signals
OBSBasic *main = OBSBasic::Get();
connect(unhideAllAction, &QAction::triggered, this, [this]() { emit unhideAll(); });
connect(hideAction, &QAction::triggered, this, [this, isHidden]() { setHiddenInMixer(!isHidden); });
connect(
pinAction, &QAction::triggered, this, [this, isPinned]() { setPinnedInMixer(!isPinned); },
Qt::DirectConnection);
connect(lockAction, &QAction::toggled, this, &VolumeControl::setLocked);
connect(copyFiltersAction, &QAction::triggered, main, &OBSBasic::actionCopyFilters);
connect(pasteFiltersAction, &QAction::triggered, main, &OBSBasic::actionPasteFilters);
connect(mixerRenameAction, &QAction::triggered, this, &VolumeControl::renameSource);
connect(filtersAction, &QAction::triggered, main, &OBSBasic::actionOpenSourceFilters);
connect(propertiesAction, &QAction::triggered, main, &OBSBasic::actionOpenSourceProperties);
// Enable/disable actions
copyFiltersAction->setEnabled(obs_source_filter_count(source) > 0);
pasteFiltersAction->setEnabled(!obs_weak_source_expired(main->copyFiltersSource()));
if (isGlobal) {
pinAction->setDisabled(true);
hideAction->setDisabled(true);
}
if (isPinned) {
hideAction->setDisabled(true);
}
// Build menu
popup->addAction(unhideAllAction);
popup->addSeparator();
popup->addAction(pinAction);
popup->addAction(hideAction);
popup->addAction(lockAction);
popup->addSeparator();
popup->addAction(copyFiltersAction);
popup->addAction(pasteFiltersAction);
popup->addSeparator();
popup->addAction(mixerRenameAction);
popup->addSeparator();
popup->addAction(filtersAction);
popup->addAction(propertiesAction);
// Calculate menu position
QPoint popupPos = mapToGlobal(pos);
if (pos.isNull()) {
QPoint menuPos = nameButton->mapToGlobal(nameButton->rect().bottomLeft());
QSize menuSize = popup->sizeHint();
QRect available = QGuiApplication::screenAt(menuPos)->availableGeometry();
int spaceBelow = available.bottom() - menuPos.y();
int spaceAbove = menuPos.y() - available.top();
if (menuSize.height() > spaceBelow && spaceAbove > spaceBelow) {
menuPos = nameButton->mapToGlobal(nameButton->rect().topLeft());
menuPos.ry() -= menuSize.height();
}
if (menuPos.x() + menuSize.width() > available.right()) {
menuPos.rx() = available.right() - menuSize.width();
}
popupPos = menuPos;
}
popup->popup(popupPos);
connect(popup, &QMenu::aboutToHide, popup, &QMenu::deleteLater);
}
void VolumeControl::renameSource()
{
QAction *action = reinterpret_cast<QAction *>(sender());
obs_source_t *source = action->property("source").value<OBSSource>();
const char *prevName = obs_source_get_name(source);
for (;;) {
std::string name;
bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"),
QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName));
if (!accepted) {
return;
}
if (name.empty()) {
OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text"));
continue;
}
OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str());
if (sourceTest) {
OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text"));
continue;
}
std::string prevName(obs_source_get_name(source));
auto undo = [prevName](const std::string &data) {
OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str());
obs_source_set_name(source, prevName.c_str());
};
std::string editedName = name;
auto redo = [editedName](const std::string &data) {
OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str());
obs_source_set_name(source, editedName.c_str());
};
OBSBasic *main = OBSBasic::Get();
const char *uuid = obs_source_get_uuid(source);
main->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, uuid, uuid);
obs_source_set_name(source, name.c_str());
break;
}
}
void VolumeControl::changeVolume()
{
QSignalBlocker blocker(slider);
slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION));
updateText();
}
void VolumeControl::setLocked(bool locked)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
obs_data_set_bool(priv_settings, "volume_locked", locked);
enableSlider(!locked);
mixerStatus().set(VolumeControl::MixerStatus::Locked, locked);
OBSBasic *main = OBSBasic::Get();
emit main->mixerStatusChanged(uuid);
}
void VolumeControl::updateCategoryLabel()
{
QString labelText = QTStr("Basic.AudioMixer.Category.Active");
if (mixerStatus().has(VolumeControl::MixerStatus::Unassigned)) {
labelText = QTStr("Basic.AudioMixer.Category.Unassigned");
} else if (mixerStatus().has(VolumeControl::MixerStatus::Global)) {
labelText = QTStr("Basic.AudioMixer.Category.Global");
} else if (mixerStatus().has(VolumeControl::MixerStatus::Pinned)) {
labelText = QTStr("Basic.AudioMixer.Category.Pinned");
} else if (mixerStatus().has(VolumeControl::MixerStatus::Hidden)) {
labelText = QTStr("Basic.AudioMixer.Category.Hidden");
} else if (!mixerStatus().has(VolumeControl::MixerStatus::Active)) {
labelText = QTStr("Basic.AudioMixer.Category.Inactive");
if (mixerStatus().has(VolumeControl::MixerStatus::Preview)) {
labelText = QTStr("Basic.AudioMixer.Category.Preview");
}
}
bool stylePinned = mixerStatus().has(VolumeControl::MixerStatus::Global) ||
mixerStatus().has(VolumeControl::MixerStatus::Pinned);
bool styleInactive = mixerStatus().has(VolumeControl::MixerStatus::Active) != true;
bool styleHidden = mixerStatus().has(VolumeControl::MixerStatus::Hidden);
bool styleUnassigned = mixerStatus().has(VolumeControl::MixerStatus::Unassigned);
bool stylePreviewed = mixerStatus().has(VolumeControl::MixerStatus::Preview);
utils->toggleClass("volume-pinned", stylePinned);
utils->toggleClass("volume-inactive", styleInactive);
utils->toggleClass("volume-preview", styleInactive && stylePreviewed);
utils->toggleClass("volume-hidden", styleHidden && !stylePinned);
utils->toggleClass("volume-unassigned", styleUnassigned);
categoryLabel->setText(labelText);
utils->polishChildren();
volumeMeter->updateBackgroundCache();
}
void VolumeControl::updateDecayRate()
{
OBSBasic *main = OBSBasic::Get();
double meterDecayRate = config_get_double(main->Config(), "Audio", "MeterDecayRate");
setMeterDecayRate(meterDecayRate);
}
void VolumeControl::updatePeakMeterType()
{
OBSBasic *main = OBSBasic::Get();
uint32_t peakMeterTypeIdx = config_get_uint(main->Config(), "Audio", "PeakMeterType");
enum obs_peak_meter_type peakMeterType;
switch (peakMeterTypeIdx) {
case 0:
peakMeterType = SAMPLE_PEAK_METER;
break;
case 1:
peakMeterType = TRUE_PEAK_METER;
break;
default:
peakMeterType = SAMPLE_PEAK_METER;
break;
}
setPeakMeterType(peakMeterType);
}
void VolumeControl::setMuted(bool mute)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
bool prev = obs_source_muted(source);
bool unassigned = isSourceUnassigned(source);
obs_source_set_muted(source, mute);
if (!mute && unassigned) {
// Show notice about the source no being assigned to any tracks
bool has_shown_warning =
config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources");
if (!has_shown_warning) {
showUnassignedWarning(obs_source_get_name(source));
}
}
auto undo_redo = [](const std::string &uuid, bool val) {
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
obs_source_set_muted(source, val);
};
QString text = QTStr(mute ? "Undo.Volume.Mute" : "Undo.Volume.Unmute");
const char *name = obs_source_get_name(source);
OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev),
std::bind(undo_redo, std::placeholders::_1, mute), uuid, uuid);
}
void VolumeControl::setMonitoring(obs_monitoring_type type)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
obs_monitoring_type prevMonitoringType = obs_source_get_monitoring_type(source);
obs_source_set_monitoring_type(source, type);
auto undo_redo = [](const std::string &uuid, obs_monitoring_type val) {
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
obs_source_set_monitoring_type(source, val);
};
QString text = QTStr("Undo.MonitoringType.Change");
const char *name = obs_source_get_name(source);
OBSBasic::Get()->undo_s.add_action(text.arg(name),
std::bind(undo_redo, std::placeholders::_1, prevMonitoringType),
std::bind(undo_redo, std::placeholders::_1, type), uuid, uuid);
}
void VolumeControl::sourceActiveChanged(bool active)
{
setUseDisabledColors(!active);
mixerStatus().set(VolumeControl::MixerStatus::Active, active);
OBSBasic *main = OBSBasic::Get();
emit main->mixerStatusChanged(uuid);
}
void VolumeControl::updateMixerState()
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
deleteLater();
return;
}
bool muted = obs_source_muted(source);
bool unassigned = isSourceUnassigned(source);
obs_monitoring_type monitoringType = obs_source_get_monitoring_type(source);
bool isActive = obs_source_active(source) && obs_source_audio_active(source);
mixerStatus().set(VolumeControl::MixerStatus::Active, isActive);
setUseDisabledColors(!isActive);
mixerStatus().set(VolumeControl::MixerStatus::Unassigned, unassigned);
QSignalBlocker blockMute(muteButton);
QSignalBlocker blockMonitor(monitorButton);
bool showAsMuted = muted || monitoringType == OBS_MONITORING_TYPE_MONITOR_ONLY;
bool showAsMonitored = monitoringType != OBS_MONITORING_TYPE_NONE;
bool showAsUnassigned = !muted && unassigned;
volumeMeter->setMuted((showAsMuted || showAsUnassigned) && !showAsMonitored);
// Qt doesn't support overriding the QPushButton icon using pseudo state selectors like :checked
// in QSS so we set a checked class selector on the button to be used instead.
utils->toggleClass(muteButton, "checked", showAsMuted);
utils->toggleClass(monitorButton, "checked", showAsMonitored);
utils->toggleClass(muteButton, "mute-unassigned", showAsUnassigned);
muteButton->setChecked(showAsMuted);
monitorButton->setChecked(showAsMonitored);
if (showAsUnassigned) {
QIcon unassignedIcon;
unassignedIcon.addFile(QString::fromUtf8(":/res/images/unassigned.svg"), QSize(16, 16),
QIcon::Mode::Normal, QIcon::State::Off);
muteButton->setIcon(unassignedIcon);
} else if (showAsMuted) {
QIcon mutedIcon;
mutedIcon.addFile(QString::fromUtf8(":/res/images/mute.svg"), QSize(16, 16), QIcon::Mode::Normal,
QIcon::State::Off);
muteButton->setIcon(mutedIcon);
} else {
QIcon unmutedIcon;
unmutedIcon.addFile(QString::fromUtf8(":/settings/images/settings/audio.svg"), QSize(16, 16),
QIcon::Mode::Normal, QIcon::State::Off);
muteButton->setIcon(unmutedIcon);
}
if (showAsMonitored) {
QIcon monitorOnIcon;
monitorOnIcon.addFile(QString::fromUtf8(":/res/images/headphones.svg"), QSize(16, 16),
QIcon::Mode::Normal, QIcon::State::Off);
monitorButton->setIcon(monitorOnIcon);
} else {
QIcon monitorOffIcon;
monitorOffIcon.addFile(QString::fromUtf8(":/res/images/headphones-off.svg"), QSize(16, 16),
QIcon::Mode::Normal, QIcon::State::Off);
monitorButton->setIcon(monitorOffIcon);
}
utils->repolish(muteButton);
utils->repolish(monitorButton);
updateCategoryLabel();
}
void VolumeControl::handleMuteButton(bool mute)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
// The Mute and Monitor buttons in the volume mixer work as a pseudo quad-state toggle.
// Both buttons must be in their "off" state in order to actually process it as a mute.
// Otherwise, clicking "Mute" with monitoring enabled will toggle the monitoring type.
obs_monitoring_type monitoringType = obs_source_get_monitoring_type(source);
if (mute && monitoringType == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT) {
setMonitoring(OBS_MONITORING_TYPE_MONITOR_ONLY);
} else if (!mute && monitoringType == OBS_MONITORING_TYPE_MONITOR_ONLY) {
setMonitoring(OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT);
} else {
setMuted(mute);
}
}
void VolumeControl::handleMonitorButton(bool enableMonitoring)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
// The Mute and Monitor buttons in the volume mixer work as a pseudo quad-state toggle.
// The source is only ever actually "Muted" if Monitoring is set to None.
obs_monitoring_type monitoringType = obs_source_get_monitoring_type(source);
bool muted = obs_source_muted(source);
if (!enableMonitoring) {
setMonitoring(OBS_MONITORING_TYPE_NONE);
if (monitoringType == OBS_MONITORING_TYPE_MONITOR_ONLY) {
setMuted(true);
}
} else if (enableMonitoring && muted) {
setMonitoring(OBS_MONITORING_TYPE_MONITOR_ONLY);
setMuted(false);
} else if (enableMonitoring && !muted) {
setMonitoring(OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT);
}
}
void VolumeControl::sliderChanged(int vol)
{
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
float prev = obs_source_get_volume(source);
obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION);
updateText();
auto undo_redo = [](const std::string &uuid, float val) {
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
obs_source_set_volume(source, val);
};
float val = obs_source_get_volume(source);
const char *name = obs_source_get_name(source);
OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name),
std::bind(undo_redo, std::placeholders::_1, prev),
std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true);
}
void VolumeControl::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");
}
volumeLabel->setText(text);
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
bool muted = obs_source_muted(source);
const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted";
QString sourceName = obs_source_get_name(source);
QString accText = QTStr(accTextLookup).arg(sourceName);
slider->setAccessibleName(accText);
}
void VolumeControl::setVertical(bool vertical_)
{
if (vertical == vertical_) {
return;
}
vertical = vertical_;
setLayoutVertical(vertical);
}
void VolumeControl::updateTabOrder()
{
QWidget *prevFocus = firstWidget()->previousInFocusChain();
QWidget *lastFocus = lastWidget()->nextInFocusChain();
if (vertical) {
setTabOrder(prevFocus, nameButton);
setTabOrder(nameButton, slider);
setTabOrder(slider, muteButton);
setTabOrder(muteButton, monitorButton);
setTabOrder(monitorButton, lastFocus);
} else {
setTabOrder(prevFocus, nameButton);
setTabOrder(nameButton, muteButton);
setTabOrder(muteButton, monitorButton);
setTabOrder(monitorButton, slider);
setTabOrder(slider, lastFocus);
}
}
void VolumeControl::updateName()
{
setName(sourceName);
}
void VolumeControl::setName(QString name)
{
sourceName = name;
muteButton->setAccessibleName(QTStr("VolControl.Mute").arg(name));
}
void VolumeControl::setMeterDecayRate(qreal q)
{
volumeMeter->setPeakDecayRate(q);
}
void VolumeControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType)
{
volumeMeter->setPeakMeterType(peakMeterType);
}
void VolumeControl::enableSlider(bool enable)
{
slider->setEnabled(enable);
}
void VolumeControl::setUseDisabledColors(bool greyscale)
{
volumeMeter->setUseDisabledColors(greyscale);
}
void VolumeControl::setGlobalInMixer(bool global)
{
if (mixerStatus().has(VolumeControl::MixerStatus::Global) != global) {
mixerStatus().set(VolumeControl::MixerStatus::Global, global);
OBSBasic *main = OBSBasic::Get();
emit main->mixerStatusChanged(uuid);
}
}
void VolumeControl::setPinnedInMixer(bool pinned)
{
if (mixerStatus().has(VolumeControl::MixerStatus::Pinned) != pinned) {
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
obs_data_set_bool(priv_settings, "mixer_pinned", pinned);
mixerStatus().set(VolumeControl::MixerStatus::Pinned, pinned);
if (pinned) {
// Unset hidden state when pinning controls
setHiddenInMixer(false);
}
OBSBasic *main = OBSBasic::Get();
emit main->mixerStatusChanged(uuid);
}
}
void VolumeControl::setHiddenInMixer(bool hidden)
{
if (mixerStatus().has(VolumeControl::MixerStatus::Hidden) != hidden) {
OBSSource source = OBSGetStrongRef(weakSource());
if (!source) {
return;
}
OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
obs_data_set_bool(priv_settings, "mixer_hidden", hidden);
mixerStatus().set(VolumeControl::MixerStatus::Hidden, hidden);
OBSBasic *main = OBSBasic::Get();
emit main->mixerStatusChanged(uuid);
}
}
void VolumeControl::refreshColors()
{
volumeMeter->refreshColors();
}
void VolumeControl::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS],
const float inputPeak[MAX_AUDIO_CHANNELS])
{
if (volumeMeter) {
volumeMeter->setLevels(magnitude, peak, inputPeak);
}
}