Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/cmake/ui-components.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ target_sources(
components/FocusList.hpp
components/GameCaptureToolbar.cpp
components/GameCaptureToolbar.hpp
components/GainReductionMeter.cpp
components/GainReductionMeter.hpp
components/ImageSourceToolbar.cpp
components/ImageSourceToolbar.hpp
components/MediaControls.cpp
Expand Down
187 changes: 187 additions & 0 deletions frontend/components/GainReductionMeter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#include "GainReductionMeter.hpp"

#include <OBSApp.hpp>

#include <cmath>

#include <QHBoxLayout>
#include <QPainter>
#include <QTimer>
#include <QVBoxLayout>

#include <util/platform.h>

#include "moc_GainReductionMeter.cpp"

GainReductionMeter::GainReductionMeter(QWidget *parent, obs_source_t *source)
: QWidget(parent),
weakSource(OBSGetWeakRef(source))
{
setFocusPolicy(Qt::NoFocus);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);

// Header row: "Gain Reduction" on the left, live dB on the right
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 4, 0, 8);
layout->setSpacing(4);

auto *header = new QHBoxLayout();
header->setContentsMargins(0, 0, 0, 0);

auto *titleLabel = new QLabel(QTStr("Basic.Filters.GainReduction"), this);
valueLabel = new QLabel(QStringLiteral("0.0 dB"), this);
valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

header->addWidget(titleLabel);
header->addStretch();
header->addWidget(valueLabel);
layout->addLayout(header);
layout->addSpacing(14);

// Stop polling cleanly if the filter is deleted while the dialog is open
if (source) {
destroyedSignal = OBSSignal(obs_source_get_signal_handler(source), "destroy",
&GainReductionMeter::obsSourceDestroyed, this);
}

// ~30 Hz UI update; audio thread writes GR much faster via atomics
auto *timer = new QTimer(this);
timer->setTimerType(Qt::PreciseTimer);
connect(timer, &QTimer::timeout, this, &GainReductionMeter::tick);
timer->start(33);

tick();
}

GainReductionMeter::~GainReductionMeter() = default;

void GainReductionMeter::obsSourceDestroyed(void *data, calldata_t *)
{
auto *self = static_cast<GainReductionMeter *>(data);
// Bounce to the UI thread before touching Qt widgets
QMetaObject::invokeMethod(self, "onSourceDestroyed", Qt::QueuedConnection);
}

void GainReductionMeter::onSourceDestroyed()
{
weakSource = nullptr;
destroyedSignal.Disconnect();
currentGainReductionDb = 0.0f;
peakHoldDb = 0.0f;
valueLabel->setText(QStringLiteral("0.0 dB"));
update();
}

QSize GainReductionMeter::minimumSizeHint() const
{
return QSize(120, 40);
}

QSize GainReductionMeter::sizeHint() const
{
return QSize(200, 40);
}

float GainReductionMeter::dbToBarWidth(float gainReductionDb, int width) const
{
if (width <= 0) {
return 0.0f;
}

// 0 dB -> empty; kMinimumDb (-60) -> full width. gainReductionDb is <= 0.
float amount = qBound(0.0f, gainReductionDb / kMinimumDb, 1.0f);
return amount * (float)width;
}

void GainReductionMeter::pollGainReduction()
{
OBSSource source = OBSGetStrongRef(weakSource);
if (!source) {
currentGainReductionDb = 0.0f;
return;
}

// Compressor registers this in compressor_create
proc_handler_t *ph = obs_source_get_proc_handler(source);
if (!ph) {
currentGainReductionDb = 0.0f;
return;
}

calldata_t cd = {};
if (!proc_handler_call(ph, "get_gain_reduction", &cd)) {
calldata_free(&cd);
currentGainReductionDb = 0.0f;
return;
}

// Negative = reduction applied; 0 = idle / below threshold
currentGainReductionDb = (float)calldata_float(&cd, "db");
if (!std::isfinite(currentGainReductionDb) || currentGainReductionDb > 0.0f) {
currentGainReductionDb = 0.0f;
} else if (currentGainReductionDb < kMinimumDb) {
currentGainReductionDb = kMinimumDb;
}

calldata_free(&cd);
}

void GainReductionMeter::updatePeakHold(float gainReductionDb, uint64_t ts)
{
const uint64_t holdNs = (uint64_t)(kPeakHoldDurationSec * 1000000000.0);

// More reduction (more negative) always refreshes the peak
if (gainReductionDb < peakHoldDb) {
peakHoldDb = gainReductionDb;
peakHoldTimeNs = ts;
return;
}

// After the hold window, let the peak fall back to the live value
if (peakHoldTimeNs == 0 || (ts - peakHoldTimeNs) >= holdNs) {
peakHoldDb = gainReductionDb;
peakHoldTimeNs = ts;
}
}

void GainReductionMeter::tick()
{
pollGainReduction();
updatePeakHold(currentGainReductionDb, os_gettime_ns());

// Label shows live GR only; peak is visual-only on the bar
valueLabel->setText(QStringLiteral("%1 dB").arg(currentGainReductionDb, 0, 'f', 1));
update();
}

void GainReductionMeter::paintEvent(QPaintEvent *)
{
QPainter painter(this);

const int barHeight = 10;
const int barY = height() - barHeight - 4;
const int barWidth = width();

if (barWidth <= 0 || barY < 0) {
return;
}

const QColor background(0x2a, 0x2a, 0x2a);
const QColor fill(0xe6, 0xb8, 0x00); // amber fill = live GR
const QColor peakTick(0xff, 0xf0, 0xa0); // lighter tick = peak hold

painter.fillRect(0, barY, barWidth, barHeight, background);

// Live fill grows left -> right as reduction increases
const int fillWidth = (int)dbToBarWidth(currentGainReductionDb, barWidth);
if (fillWidth > 0) {
painter.fillRect(0, barY, fillWidth, barHeight, fill);
}

// Peak tick sits at the recent maximum reduction
const int peakX = (int)dbToBarWidth(peakHoldDb, barWidth);
if (peakHoldDb < -0.05f && peakX > 0) {
const int tickWidth = qMax(2, barWidth / 120);
painter.fillRect(qMin(peakX, barWidth - tickWidth), barY, tickWidth, barHeight, peakTick);
}
}
53 changes: 53 additions & 0 deletions frontend/components/GainReductionMeter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pragma once

#include <obs.hpp>

#include <QLabel>
#include <QWidget>

// Live gain-reduction meter shown above compressor filter properties.
// Polls the filter's get_gain_reduction proc handler on a timer.
class GainReductionMeter : public QWidget {
Q_OBJECT

public:
// source is the compressor filter instance to meter
explicit GainReductionMeter(QWidget *parent = nullptr, obs_source_t *source = nullptr);
~GainReductionMeter() override;

QSize minimumSizeHint() const override;
QSize sizeHint() const override;

protected:
// Draws the amber GR bar and peak-hold tick at the bottom of the widget
void paintEvent(QPaintEvent *event) override;

private slots:
// UI-thread cleanup when the filter source is destroyed
void onSourceDestroyed();
// Timer callback: poll GR, update peak hold + label, repaint
void tick();

private:
// OBS signal callback (may run off the UI thread) -> queues onSourceDestroyed
static void obsSourceDestroyed(void *data, calldata_t *);

// Reads current GR dB from the filter via proc_handler
void pollGainReduction();
// Keeps the most-reduced (most negative) value for kPeakHoldDurationSec
void updatePeakHold(float gainReductionDb, uint64_t ts);
// Maps GR dB (0 .. kMinimumDb) to a horizontal pixel width
float dbToBarWidth(float gainReductionDb, int width) const;

OBSWeakSource weakSource;
OBSSignal destroyedSignal;

QLabel *valueLabel = nullptr; // live numeric readout, e.g. "-6.2 dB"

float currentGainReductionDb = 0.0f; // live value (bar fill + label)
float peakHoldDb = 0.0f; // peak marker only (not shown in label)
uint64_t peakHoldTimeNs = 0; // when peakHoldDb was last set

static constexpr float kMinimumDb = -60.0f; // right edge of the bar scale
static constexpr float kPeakHoldDurationSec = 1.5f; // how long the peak tick sticks
};
1 change: 1 addition & 0 deletions frontend/data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ Basic.Filters.EffectFilters="Effect Filters"
Basic.Filters.Title="Filters for '%1'"
Basic.Filters.AddFilter.Title="Filter name"
Basic.Filters.AddFilter.Text="Please specify the name of the filter"
Basic.Filters.GainReduction="Gain Reduction"

# transform window
Basic.TransformWindow="Scene Item Transform"
Expand Down
18 changes: 18 additions & 0 deletions frontend/dialogs/OBSBasicFilters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "OBSBasicFilters.hpp"

#include <components/GainReductionMeter.hpp>
#include <components/VisibilityItemDelegate.hpp>
#include <components/VisibilityItemWidget.hpp>
#include <dialogs/NameDialog.hpp>
Expand All @@ -27,6 +28,8 @@
#include <properties-view.hpp>
#include <qt-wrappers.hpp>

#include <cstring>

#include <QLineEdit>

#ifdef _WIN32
Expand Down Expand Up @@ -241,6 +244,12 @@ void OBSBasicFilters::UpdatePropertiesView(int row, bool async)
*
* macOS might be especially affected as it doesn't switch keyboard focus
* to buttons like Windows does. */
// Tear down GR meter with the properties view when selection changes
if (gainReductionMeter) {
gainReductionMeter->hide();
gainReductionMeter->deleteLater();
gainReductionMeter = nullptr;
}
if (view) {
view->hide();
view->deleteLater();
Expand Down Expand Up @@ -270,6 +279,15 @@ void OBSBasicFilters::UpdatePropertiesView(int row, bool async)

view->setMinimumHeight(150);
UpdateSplitter();

// obs_properties has no meter widget type, so host GR UI here for compressor
const char *id = obs_source_get_id(filter);
if (id && strcmp(id, "compressor_filter") == 0) {
gainReductionMeter = new GainReductionMeter(ui->propertiesFrame, filter);
ui->propertiesLayout->addWidget(gainReductionMeter);
gainReductionMeter->show();
}

ui->propertiesLayout->addWidget(view);
view->show();
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/dialogs/OBSBasicFilters.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include <QDialog>

class GainReductionMeter;
class OBSBasic;
class OBSPropertiesView;

Expand All @@ -33,6 +34,8 @@ class OBSBasicFilters : public QDialog {
std::unique_ptr<Ui::OBSBasicFilters> ui;
OBSSource source;
OBSPropertiesView *view = nullptr;
// Shown only while a compressor_filter is selected in this dialog
GainReductionMeter *gainReductionMeter = nullptr;

std::vector<OBSSignal> obsSignals;
OBSSignal updatePropertiesSignal;
Expand Down
Loading
Loading