UI: Rework volume-meters, adding more information

The following features have been added to the audio-meters:
 * Stereo PPM-level meter, with 40 dB/1.7s decay rate.
 * Stereo VU-level meter, with 300 ms integration time.
 * Stereo Peak-hold meter, with 20 second sustain.
 * Input peak level color-squares in front of every meter.
 * Minor-ticks for each dB.
 * Major-ticks for every 5 dB.
 * Meter is divided in sections at -20 dB and -9 dB.

The ballistic parameters chosen here where taken from:
 * https://en.wikipedia.org/wiki/Peak_programme_meter (SMPTE RP.0155)
 * https://en.wikipedia.org/wiki/VU_meter

In the rework I have removed any ballistic calculations from
libobs/obs-audio-controls.c making the calculations here a lot more
simple doing only MAX and RMS calculations for only the samples in
the current update. The actual ballistics are now done by just
the UI/volume-control.cpp because ballistics need to be updated
based on the repaint-rate of the user-interface.

The dB to pixel conversion has been moved from
libobs/obs-audio-controls.c to UI/volume-control.cpp as well to reduce
coupling between these two objects, especially when implementing the
major- and minor-ticks and the sections.

All colors and ballistic parameters are adjustable via QT style sheets.
There are slight differences in colors for each of the themes.
This commit is contained in:
Tjienta Vara 2017-12-31 17:43:57 +01:00
parent 2f577c1b71
commit 50ce228455
7 changed files with 835 additions and 345 deletions

View file

@ -464,10 +464,16 @@ QSlider::handle:disabled {
/* Volume Control */
VolumeMeter {
qproperty-bkColor: rgb(31,30,31); /* veryDark */
qproperty-magColor:;
qproperty-peakColor:;
qproperty-peakHoldColor: rgb(225,224,225); /* veryLight */
qproperty-backgroundNominalColor: rgb(38, 127, 38);
qproperty-backgroundWarningColor: rgb(127, 127, 38);
qproperty-backgroundErrorColor: rgb(127, 38, 38);
qproperty-foregroundNominalColor: rgb(76, 255, 76);
qproperty-foregroundWarningColor: rgb(255, 255, 76);
qproperty-foregroundErrorColor: rgb(255, 76, 76);
qproperty-magnitudeColor: rgb(0, 0, 0);
qproperty-majorTickColor: rgb(225,224,225); /* veryLight */
qproperty-minorTickColor: rgb(122,121,122); /* light */
qproperty-peakDecayRate: 23.4; /* Override of the standard PPM Type I rate. */
}

View file

@ -55,10 +55,16 @@ OBSHotkeyLabel[hotkeyPairHover=true] {
/* Volume Control */
VolumeMeter {
qproperty-bkColor: rgb(221, 221, 221);
qproperty-magColor: rgb(32, 125, 23);
qproperty-peakColor: rgb(62, 241, 43);
qproperty-peakHoldColor: rgb(0, 0, 0);
qproperty-backgroundNominalColor: rgb(15, 100, 15);
qproperty-backgroundWarningColor: rgb(100, 100, 15);
qproperty-backgroundErrorColor: rgb(100, 15, 15);
qproperty-foregroundNominalColor: rgb(50, 200, 50);
qproperty-foregroundWarningColor: rgb(255, 200, 50);
qproperty-foregroundErrorColor: rgb(200, 50, 50);
qproperty-magnitudeColor: rgb(0, 0, 0);
qproperty-majorTickColor: rgb(0, 0, 0);
qproperty-minorTickColor: rgb(50, 50, 50);
qproperty-peakDecayRate: 23.4; /* Override of the standard PPM Type I rate. */
}

View file

@ -670,12 +670,16 @@ QProgressBar::chunk {
/**************************/
VolumeMeter {
qproperty-bkColor: rgb(35, 38, 41); /* Dark Gray */
qproperty-magColor: rgb(153, 204, 0);
qproperty-peakColor: rgb(96, 128, 0);
qproperty-peakHoldColor: rgb(210, 255, 77);
qproperty-clipColor1: rgb(230, 40, 50);
qproperty-clipColor2: rgb(140, 0, 40);
qproperty-backgroundNominalColor: rgb(0, 128, 79);
qproperty-backgroundWarningColor: rgb(128, 57, 0);
qproperty-backgroundErrorColor: rgb(128, 9, 0);
qproperty-foregroundNominalColor: rgb(119, 255, 143);
qproperty-foregroundWarningColor: rgb(255, 157, 76);
qproperty-foregroundErrorColor: rgb(255, 89, 76);
qproperty-magnitudeColor: rgb(49, 54, 59); /* Blue-gray */
qproperty-majorTickColor: rgb(239, 240, 241); /* White */
qproperty-minorTickColor: rgb(118, 121, 124); /* Light Gray */
qproperty-peakDecayRate: 23.4; /* Override of the standard PPM Type I rate. */
}
/*******************/

View file

@ -6,6 +6,7 @@
#include <obs-audio-controls.h>
#include <util/platform.h>
#include <util/threading.h>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
@ -19,6 +20,8 @@
using namespace std;
#define CLAMP(x, min, max) ((x) < min ? min : ((x) > max ? max : (x)))
QWeakPointer<VolumeMeterTimer> VolumeMeter::updateTimer;
void VolControl::OBSVolumeChanged(void *data, float db)
@ -29,15 +32,14 @@ void VolControl::OBSVolumeChanged(void *data, float db)
QMetaObject::invokeMethod(volControl, "VolumeChanged");
}
void VolControl::OBSVolumeLevel(void *data, float level, float mag,
float peak, float muted)
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<VolControl*>(data);
if (muted)
level = mag = peak = 0.0f;
volControl->volMeter->setLevels(mag, level, peak);
volControl->volMeter->setLevels(magnitude, peak, inputPeak);
}
void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata)
@ -121,7 +123,7 @@ VolControl::VolControl(OBSSource source_, bool showConfig)
nameLabel = new QLabel();
volLabel = new QLabel();
volMeter = new VolumeMeter();
volMeter = new VolumeMeter(0, obs_volmeter);
mute = new MuteCheckBox();
slider = new QSlider(Qt::Horizontal);
@ -215,81 +217,222 @@ VolControl::~VolControl()
obs_volmeter_destroy(obs_volmeter);
}
QColor VolumeMeter::getBkColor() const
QColor VolumeMeter::getBackgroundNominalColor() const
{
return bkColor;
return backgroundNominalColor;
}
void VolumeMeter::setBkColor(QColor c)
void VolumeMeter::setBackgroundNominalColor(QColor c)
{
bkColor = c;
backgroundNominalColor = c;
}
QColor VolumeMeter::getMagColor() const
QColor VolumeMeter::getBackgroundWarningColor() const
{
return magColor;
return backgroundWarningColor;
}
void VolumeMeter::setMagColor(QColor c)
void VolumeMeter::setBackgroundWarningColor(QColor c)
{
magColor = c;
backgroundWarningColor = c;
}
QColor VolumeMeter::getPeakColor() const
QColor VolumeMeter::getBackgroundErrorColor() const
{
return peakColor;
return backgroundErrorColor;
}
void VolumeMeter::setPeakColor(QColor c)
void VolumeMeter::setBackgroundErrorColor(QColor c)
{
peakColor = c;
backgroundErrorColor = c;
}
QColor VolumeMeter::getPeakHoldColor() const
QColor VolumeMeter::getForegroundNominalColor() const
{
return peakHoldColor;
return foregroundNominalColor;
}
void VolumeMeter::setPeakHoldColor(QColor c)
void VolumeMeter::setForegroundNominalColor(QColor c)
{
peakHoldColor = c;
foregroundNominalColor = c;
}
QColor VolumeMeter::getClipColor1() const
QColor VolumeMeter::getForegroundWarningColor() const
{
return clipColor1;
return foregroundWarningColor;
}
void VolumeMeter::setClipColor1(QColor c)
void VolumeMeter::setForegroundWarningColor(QColor c)
{
clipColor1 = c;
foregroundWarningColor = c;
}
QColor VolumeMeter::getClipColor2() const
QColor VolumeMeter::getForegroundErrorColor() const
{
return clipColor2;
return foregroundErrorColor;
}
void VolumeMeter::setClipColor2(QColor c)
void VolumeMeter::setForegroundErrorColor(QColor c)
{
clipColor2 = c;
foregroundErrorColor = c;
}
VolumeMeter::VolumeMeter(QWidget *parent)
: QWidget(parent)
QColor VolumeMeter::getClipColor() const
{
setMinimumSize(1, 3);
return clipColor;
}
//Default meter color settings, they only show if there is no stylesheet, do not remove.
bkColor.setRgb(0xDD, 0xDD, 0xDD);
magColor.setRgb(0x20, 0x7D, 0x17);
peakColor.setRgb(0x3E, 0xF1, 0x2B);
peakHoldColor.setRgb(0x00, 0x00, 0x00);
void VolumeMeter::setClipColor(QColor c)
{
clipColor = c;
}
clipColor1.setRgb(0x7F, 0x00, 0x00);
clipColor2.setRgb(0xFF, 0x00, 0x00);
QColor VolumeMeter::getMagnitudeColor() const
{
return magnitudeColor;
}
void VolumeMeter::setMagnitudeColor(QColor c)
{
magnitudeColor = c;
}
QColor VolumeMeter::getMajorTickColor() const
{
return majorTickColor;
}
void VolumeMeter::setMajorTickColor(QColor c)
{
majorTickColor = c;
}
QColor VolumeMeter::getMinorTickColor() const
{
return minorTickColor;
}
void VolumeMeter::setMinorTickColor(QColor c)
{
minorTickColor = 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;
}
VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter)
: QWidget(parent), obs_volmeter(obs_volmeter)
{
// 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.7; // 20 dB / 1.7 sec
magnitudeIntegrationTime = 0.3; // 99% in 300 ms
peakHoldDuration = 20.0; // 20 seconds
inputPeakHoldDuration = 1.0; // 1 second
handleChannelCofigurationChange();
updateTimerRef = updateTimer.toStrongRef();
if (!updateTimerRef) {
updateTimerRef = QSharedPointer<VolumeMeterTimer>::create();
@ -305,35 +448,350 @@ VolumeMeter::~VolumeMeter()
updateTimerRef->RemoveVolControl(this);
}
void VolumeMeter::setLevels(float nmag, float npeak, float npeakHold)
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);
mag += nmag;
peak += npeak;
peakHold += npeakHold;
multiple += 1.0f;
lastUpdateTime = ts;
}
inline void VolumeMeter::calcLevels()
{
uint64_t ts = os_gettime_ns();
QMutexLocker locker(&dataMutex);
if (lastUpdateTime && ts - lastUpdateTime > 1000000000) {
mag = peak = peakHold = 0.0f;
multiple = 1.0f;
lastUpdateTime = 0;
currentLastUpdateTime = ts;
for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) {
currentMagnitude[channelNr] = magnitude[channelNr];
currentPeak[channelNr] = peak[channelNr];
currentInputPeak[channelNr] = inputPeak[channelNr];
}
if (multiple > 0.0f) {
curMag = mag / multiple;
curPeak = peak / multiple;
curPeakHold = peakHold / multiple;
// 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);
}
mag = peak = peakHold = multiple = 0.0f;
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 high meter, with one pixel between
// each. Then 9 pixels below it for ticks and numbers.
setMinimumSize(130, displayNrAudioChannels * 4 + 8);
resetLevels();
}
}
inline bool VolumeMeter::detectIdle(uint64_t ts)
{
float 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 20 dB / 1.7 seconds.
qreal decay = 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.
qreal attack = (currentMagnitude[channelNr] -
displayMagnitude[channelNr]) *
(timeSinceLastRedraw /
magnitudeIntegrationTime) * 0.99;
displayMagnitude[channelNr] = CLAMP(
displayMagnitude[channelNr] + attack,
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);
if (peakHold < minimumInputLevel) {
painter.fillRect(x, y, width, height, backgroundNominalColor);
} else if (peakHold < warningLevel) {
painter.fillRect(x, y, width, height, foregroundNominalColor);
} else if (peakHold < errorLevel) {
painter.fillRect(x, y, width, height, foregroundWarningColor);
} else if (peakHold <= clipLevel) {
painter.fillRect(x, y, width, height, foregroundErrorColor);
} else {
painter.fillRect(x, y, width, height, clipColor);
}
}
void VolumeMeter::paintTicks(QPainter &painter, int x, int y,
int width, int height)
{
qreal scale = width / minimumLevel;
// Use a font that can be rendered small.
QFont font = QFont("Arial");
font.setPixelSize(7);
painter.setFont(font);
painter.setPen(majorTickColor);
// Draw major tick lines and numeric indicators.
for (int i = 0; i > minimumLevel; i-= 5) {
int position = x + width - (i * scale) - 1;
char str[5];
snprintf(str, sizeof (str), "%i", i);
if (i == 0 || i == 5) {
painter.drawText(position - 3, height, QString(str));
} else {
painter.drawText(position - 5, height, QString(str));
}
painter.drawLine(position, y, position, y + 2);
}
// Draw minor tick lines.
painter.setPen(minorTickColor);
for (int i = 0; i > minimumLevel; i--) {
int position = x + width - (i * scale) - 1;
if (i % 5 != 0) {
painter.drawLine(position, y, position, y + 1);
}
}
}
void VolumeMeter::paintMeter(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 = x + width - (magnitude * scale);
int peakPosition = x + width - (peak * scale);
int peakHoldPosition = x + width - (peakHold * scale);
int warningPosition = x + width - (warningLevel * scale);
int errorPosition = x + width - (errorLevel * scale);
int nominalLength = warningPosition - minimumPosition;
int warningLength = errorPosition - warningPosition;
int errorLength = maximumPosition - errorPosition;
locker.unlock();
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 {
painter.fillRect(
minimumPosition, y,
nominalLength, height,
foregroundNominalColor);
painter.fillRect(
warningPosition, y,
warningLength, height,
foregroundWarningColor);
painter.fillRect(
errorPosition, y,
errorLength, height,
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) {
// Magnitude below minimum, no drawing.
} else if (magnitudePosition < warningPosition) {
painter.fillRect(
magnitudePosition - 3, y,
3, height,
magnitudeColor);
} else if (magnitudePosition < errorPosition) {
painter.fillRect(
magnitudePosition - 3, y,
3, height,
magnitudeColor);
} else {
painter.fillRect(
magnitudePosition - 3, y,
3, height,
magnitudeColor);
}
}
@ -341,48 +799,54 @@ void VolumeMeter::paintEvent(QPaintEvent *event)
{
UNUSED_PARAMETER(event);
QPainter painter(this);
QLinearGradient gradient;
uint64_t ts = os_gettime_ns();
qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001;
int width = size().width();
int height = size().height();
calcLevels();
handleChannelCofigurationChange();
calculateBallistics(ts, timeSinceLastRedraw);
bool idle = detectIdle(ts);
int scaledMag = int((float)width * curMag);
int scaledPeak = int((float)width * curPeak);
int scaledPeakHold = int((float)width * curPeakHold);
// Draw the ticks in a off-screen buffer when the widget changes size.
QSize tickPaintCacheSize = QSize(width - 5, 9);
if (tickPaintCache == NULL ||
tickPaintCache->size() != tickPaintCacheSize) {
delete tickPaintCache;
tickPaintCache = new QPixmap(tickPaintCacheSize);
float db = obs_volmeter_get_cur_db(OBS_FADER_LOG, curPeakHold);
QColor clearColor(0, 0, 0, 0);
tickPaintCache->fill(clearColor);
gradient.setStart(qreal(scaledMag), 0);
gradient.setFinalStop(qreal(scaledPeak), 0);
gradient.setColorAt(0, db == 0.0f ? clipColor1 : magColor);
gradient.setColorAt(1, db == 0.0f ? clipColor2 : peakColor);
QPainter tickPainter(tickPaintCache);
paintTicks(tickPainter, 0, 0, tickPaintCacheSize.width(),
tickPaintCacheSize.height());
tickPainter.end();
}
// RMS
painter.fillRect(0, 0,
scaledMag, height,
db == 0.0f ? clipColor1 : magColor);
// Actual painting of the widget starts here.
QPainter painter(this);
painter.drawPixmap(5, height - 9, *tickPaintCache);
// RMS - Peak gradient
painter.fillRect(scaledMag, 0,
scaledPeak - scaledMag + 1, height,
QBrush(gradient));
for (int channelNr = 0; channelNr < displayNrAudioChannels;
channelNr++) {
paintMeter(painter,
5, channelNr * 4, width - 5, 3,
displayMagnitude[channelNr], displayPeak[channelNr],
displayPeakHold[channelNr]);
// Background
painter.fillRect(scaledPeak, 0,
width - scaledPeak, height,
bkColor);
// Peak hold
if (peakHold == 1.0f)
scaledPeakHold--;
painter.setPen(peakHoldColor);
painter.drawLine(scaledPeakHold, 0,
scaledPeakHold, height);
if (!idle) {
// By not drawing the input meter boxes the user can
// see that the audio stream has been stopped, without
// having too much visual impact.
paintInputMeter(painter,
0, channelNr * 4, 3, 3,
displayInputPeakHold[channelNr]);
}
}
lastRedrawTime = ts;
}
void VolumeMeterTimer::AddVolControl(VolumeMeter *meter)

View file

@ -13,45 +13,178 @@ class VolumeMeterTimer;
class VolumeMeter : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor bkColor READ getBkColor WRITE setBkColor DESIGNABLE true)
Q_PROPERTY(QColor magColor READ getMagColor WRITE setMagColor DESIGNABLE true)
Q_PROPERTY(QColor peakColor READ getPeakColor WRITE setPeakColor DESIGNABLE true)
Q_PROPERTY(QColor peakHoldColor READ getPeakHoldColor WRITE setPeakHoldColor DESIGNABLE true)
Q_PROPERTY(QColor clipColor1 READ getClipColor1 WRITE setClipColor1 DESIGNABLE true)
Q_PROPERTY(QColor clipColor2 READ getClipColor2 WRITE setClipColor2 DESIGNABLE true)
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:
obs_volmeter_t *obs_volmeter;
static QWeakPointer<VolumeMeterTimer> updateTimer;
QSharedPointer<VolumeMeterTimer> updateTimerRef;
float curMag = 0.0f, curPeak = 0.0f, curPeakHold = 0.0f;
inline void calcLevels();
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 paintMeter(QPainter &painter, int x, int y,
int width, int height,
float magnitude, float peak, float peakHold);
void paintTicks(QPainter &painter, int x, int y, int width, int height);
QMutex dataMutex;
float mag = 0.0f, peak = 0.0f, peakHold = 0.0f;
float multiple = 0.0f;
uint64_t lastUpdateTime = 0;
QColor bkColor, magColor, peakColor, peakHoldColor;
QColor clipColor1, clipColor2;
uint64_t currentLastUpdateTime = 0;
float currentMagnitude[MAX_AUDIO_CHANNELS];
float currentPeak[MAX_AUDIO_CHANNELS];
float currentInputPeak[MAX_AUDIO_CHANNELS];
QPixmap *tickPaintCache = NULL;
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];
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;
public:
explicit VolumeMeter(QWidget *parent = 0);
explicit VolumeMeter(QWidget *parent = 0,
obs_volmeter_t *obs_volmeter = 0);
~VolumeMeter();
void setLevels(float nmag, float npeak, float npeakHold);
QColor getBkColor() const;
void setBkColor(QColor c);
QColor getMagColor() const;
void setMagColor(QColor c);
QColor getPeakColor() const;
void setPeakColor(QColor c);
QColor getPeakHoldColor() const;
void setPeakHoldColor(QColor c);
QColor getClipColor1() const;
void setClipColor1(QColor c);
QColor getClipColor2() const;
void setClipColor2(QColor c);
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);
protected:
void paintEvent(QPaintEvent *event);
@ -92,8 +225,10 @@ private:
obs_volmeter_t *obs_volmeter;
static void OBSVolumeChanged(void *param, float db);
static void OBSVolumeLevel(void *data, float level, float mag,
float peak, float muted);
static void OBSVolumeLevel(void *data,
const float magnitude[MAX_AUDIO_CHANNELS],
const float peak[MAX_AUDIO_CHANNELS],
const float inputPeak[MAX_AUDIO_CHANNELS]);
static void OBSVolumeMuted(void *data, calldata_t *calldata);
void EmitConfigClicked();

View file

@ -32,6 +32,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#pragma warning(disable : 4756)
#endif
#define CLAMP(x, min, max) ((x) < min ? min : ((x) > max ? max : (x)))
typedef float (*obs_fader_conversion_t)(const float val);
struct fader_cb {
@ -61,8 +63,6 @@ struct meter_cb {
struct obs_volmeter {
pthread_mutex_t mutex;
obs_fader_conversion_t pos_to_db;
obs_fader_conversion_t db_to_pos;
obs_source_t *source;
enum obs_fader_type type;
float cur_db;
@ -70,20 +70,10 @@ struct obs_volmeter {
pthread_mutex_t callback_mutex;
DARRAY(struct meter_cb)callbacks;
unsigned int channels;
unsigned int update_ms;
unsigned int update_frames;
unsigned int peakhold_ms;
unsigned int peakhold_frames;
unsigned int peakhold_count;
unsigned int ival_frames;
float ival_sum;
float ival_max;
float vol_peak;
float vol_mag;
float vol_max;
float vol_magnitude[MAX_AUDIO_CHANNELS];
float vol_peak[MAX_AUDIO_CHANNELS];
};
static float cubic_def_to_db(const float def)
@ -205,13 +195,14 @@ static void signal_volume_changed(struct obs_fader *fader, const float db)
}
static void signal_levels_updated(struct obs_volmeter *volmeter,
const float level, const float magnitude, const float peak,
bool muted)
const float magnitude[MAX_AUDIO_CHANNELS],
const float peak[MAX_AUDIO_CHANNELS],
const float input_peak[MAX_AUDIO_CHANNELS])
{
pthread_mutex_lock(&volmeter->callback_mutex);
for (size_t i = volmeter->callbacks.num; i > 0; i--) {
struct meter_cb cb = volmeter->callbacks.array[i - 1];
cb.callback(cb.param, level, magnitude, peak, muted);
cb.callback(cb.param, magnitude, peak, input_peak);
}
pthread_mutex_unlock(&volmeter->callback_mutex);
}
@ -265,145 +256,84 @@ static void volmeter_source_destroyed(void *vptr, calldata_t *calldata)
obs_volmeter_detach_source(volmeter);
}
/* TODO: Separate for individual channels */
static void volmeter_sum_and_max(float *data[MAX_AV_PLANES], size_t frames,
float *sum, float *max)
{
float s = *sum;
float m = *max;
for (size_t plane = 0; plane < MAX_AV_PLANES; plane++) {
if (!data[plane])
break;
for (float *c = data[plane]; c < data[plane] + frames; ++c) {
const float pow = *c * *c;
s += pow;
m = (m > pow) ? m : pow;
}
}
*sum = s;
*max = m;
}
/**
* @todo The IIR low pass filter has a different behavior depending on the
* update interval and sample rate, it should be replaced with something
* that is independent from both.
*/
static void volmeter_calc_ival_levels(obs_volmeter_t *volmeter)
{
const unsigned int samples = volmeter->ival_frames * volmeter->channels;
const float alpha = 0.15f;
const float ival_max = sqrtf(volmeter->ival_max);
const float ival_rms = sqrtf(volmeter->ival_sum / (float)samples);
if (ival_max > volmeter->vol_max) {
volmeter->vol_max = ival_max;
} else {
volmeter->vol_max = alpha * volmeter->vol_max +
(1.0f - alpha) * ival_max;
}
if (volmeter->vol_max > volmeter->vol_peak ||
volmeter->peakhold_count > volmeter->peakhold_frames) {
volmeter->vol_peak = volmeter->vol_max;
volmeter->peakhold_count = 0;
} else {
volmeter->peakhold_count += volmeter->ival_frames;
}
volmeter->vol_mag = alpha * ival_rms +
volmeter->vol_mag * (1.0f - alpha);
/* reset interval data */
volmeter->ival_frames = 0;
volmeter->ival_sum = 0.0f;
volmeter->ival_max = 0.0f;
}
static bool volmeter_process_audio_data(obs_volmeter_t *volmeter,
static void volmeter_process_audio_data(obs_volmeter_t *volmeter,
const struct audio_data *data)
{
bool updated = false;
size_t frames = 0;
size_t left = data->frames;
float *adata[MAX_AV_PLANES];
int nr_samples = data->frames;
int channel_nr = 0;
for (size_t i = 0; i < MAX_AV_PLANES; i++)
adata[i] = (float*)data->data[i];
while (left) {
frames = (volmeter->ival_frames + left >
volmeter->update_frames)
? volmeter->update_frames - volmeter->ival_frames
: left;
volmeter_sum_and_max(adata, frames, &volmeter->ival_sum,
&volmeter->ival_max);
volmeter->ival_frames += (unsigned int)frames;
left -= frames;
for (size_t i = 0; i < MAX_AV_PLANES; i++) {
if (!adata[i])
break;
adata[i] += frames;
for (size_t plane_nr = 0; plane_nr < MAX_AV_PLANES; plane_nr++) {
float *samples = (float *)data->data[plane_nr];
if (!samples) {
// This plane does not contain data.
continue;
}
/* break if we did not reach the end of the interval */
if (volmeter->ival_frames != volmeter->update_frames)
break;
// For each plane calculate:
// * peak = the maximum-absolute of the sample values.
// * magnitude = root-mean-square of the sample values.
// A VU meter needs to integrate over 300ms, but this will
// be handled by the ballistics of the meter itself,
// reality. Which makes this calculation independent of
// sample rate or update rate.
float peak = 0.0;
float sum_of_squares = 0.0;
for (int sample_nr = 0; sample_nr < nr_samples; sample_nr++) {
float sample = samples[sample_nr];
volmeter_calc_ival_levels(volmeter);
updated = true;
peak = fmaxf(peak, fabsf(sample));
sum_of_squares += (sample * sample);
}
volmeter->vol_magnitude[channel_nr] = sqrtf(sum_of_squares /
nr_samples);
volmeter->vol_peak[channel_nr] = peak;
channel_nr++;
}
return updated;
// Clear audio channels that are not in use.
for (; channel_nr < MAX_AUDIO_CHANNELS; channel_nr++) {
volmeter->vol_magnitude[channel_nr] = 0.0;
volmeter->vol_peak[channel_nr] = 0.0;
}
}
static void volmeter_source_data_received(void *vptr, obs_source_t *source,
const struct audio_data *data, bool muted)
{
struct obs_volmeter *volmeter = (struct obs_volmeter *) vptr;
bool updated = false;
float mul, level, mag, peak;
float mul;
float magnitude[MAX_AUDIO_CHANNELS];
float peak[MAX_AUDIO_CHANNELS];
float input_peak[MAX_AUDIO_CHANNELS];
pthread_mutex_lock(&volmeter->mutex);
updated = volmeter_process_audio_data(volmeter, data);
volmeter_process_audio_data(volmeter, data);
if (updated) {
mul = db_to_mul(volmeter->cur_db);
level = volmeter->db_to_pos(mul_to_db(volmeter->vol_max * mul));
mag = volmeter->db_to_pos(mul_to_db(volmeter->vol_mag * mul));
peak = volmeter->db_to_pos(
mul_to_db(volmeter->vol_peak * mul));
// Adjust magnitude/peak based on the volume level set by the user.
// And convert to dB.
mul = muted ? 0.0 : db_to_mul(volmeter->cur_db);
for (int channel_nr = 0; channel_nr < MAX_AUDIO_CHANNELS;
channel_nr++) {
magnitude[channel_nr] = mul_to_db(
volmeter->vol_magnitude[channel_nr] * mul);
peak[channel_nr] = mul_to_db(
volmeter->vol_peak[channel_nr] * mul);
input_peak[channel_nr] = mul_to_db(
volmeter->vol_peak[channel_nr]);
}
// The input-peak is NOT adjusted with volume, so that the user
// can check the input-gain.
pthread_mutex_unlock(&volmeter->mutex);
if (updated)
signal_levels_updated(volmeter, level, mag, peak, muted);
signal_levels_updated(volmeter, magnitude, peak, input_peak);
UNUSED_PARAMETER(source);
}
static void volmeter_update_audio_settings(obs_volmeter_t *volmeter)
{
audio_t *audio = obs_get_audio();
const unsigned int sr = audio_output_get_sample_rate(audio);
uint32_t channels = (uint32_t)audio_output_get_channels(audio);
pthread_mutex_lock(&volmeter->mutex);
volmeter->channels = channels;
volmeter->update_frames = volmeter->update_ms * sr / 1000;
volmeter->peakhold_frames = volmeter->peakhold_ms * sr / 1000;
pthread_mutex_unlock(&volmeter->mutex);
}
obs_fader_t *obs_fader_create(enum obs_fader_type type)
{
struct obs_fader *fader = bzalloc(sizeof(struct obs_fader));
@ -634,28 +564,9 @@ obs_volmeter_t *obs_volmeter_create(enum obs_fader_type type)
if (pthread_mutex_init(&volmeter->callback_mutex, NULL) != 0)
goto fail;
/* set conversion functions */
switch(type) {
case OBS_FADER_CUBIC:
volmeter->pos_to_db = cubic_def_to_db;
volmeter->db_to_pos = cubic_db_to_def;
break;
case OBS_FADER_IEC:
volmeter->pos_to_db = iec_def_to_db;
volmeter->db_to_pos = iec_db_to_def;
break;
case OBS_FADER_LOG:
volmeter->pos_to_db = log_def_to_db;
volmeter->db_to_pos = log_db_to_def;
break;
default:
goto fail;
break;
}
volmeter->type = type;
obs_volmeter_set_update_interval(volmeter, 50);
obs_volmeter_set_peak_hold(volmeter, 1500);
return volmeter;
fail:
@ -739,8 +650,6 @@ void obs_volmeter_set_update_interval(obs_volmeter_t *volmeter,
pthread_mutex_lock(&volmeter->mutex);
volmeter->update_ms = ms;
pthread_mutex_unlock(&volmeter->mutex);
volmeter_update_audio_settings(volmeter);
}
unsigned int obs_volmeter_get_update_interval(obs_volmeter_t *volmeter)
@ -755,28 +664,26 @@ unsigned int obs_volmeter_get_update_interval(obs_volmeter_t *volmeter)
return interval;
}
void obs_volmeter_set_peak_hold(obs_volmeter_t *volmeter, const unsigned int ms)
int obs_volmeter_get_nr_channels(obs_volmeter_t *volmeter)
{
if (!volmeter)
return;
int source_nr_audio_channels;
int obs_nr_audio_channels;
pthread_mutex_lock(&volmeter->mutex);
volmeter->peakhold_ms = ms;
pthread_mutex_unlock(&volmeter->mutex);
if (volmeter->source) {
source_nr_audio_channels = get_audio_channels(
volmeter->source->sample_info.speakers);
} else {
source_nr_audio_channels = 1;
}
volmeter_update_audio_settings(volmeter);
}
struct obs_audio_info audio_info;
if (obs_get_audio_info(&audio_info)) {
obs_nr_audio_channels = get_audio_channels(audio_info.speakers);
} else {
obs_nr_audio_channels = 2;
}
unsigned int obs_volmeter_get_peak_hold(obs_volmeter_t *volmeter)
{
if (!volmeter)
return 0;
pthread_mutex_lock(&volmeter->mutex);
const unsigned int peakhold = volmeter->peakhold_ms;
pthread_mutex_unlock(&volmeter->mutex);
return peakhold;
return CLAMP(source_nr_audio_channels, 1, obs_nr_audio_channels);
}
void obs_volmeter_add_callback(obs_volmeter_t *volmeter,
@ -805,26 +712,3 @@ void obs_volmeter_remove_callback(obs_volmeter_t *volmeter,
pthread_mutex_unlock(&volmeter->callback_mutex);
}
float obs_volmeter_get_cur_db(enum obs_fader_type type, const float def)
{
float db;
switch(type) {
case OBS_FADER_CUBIC:
db = cubic_def_to_db(def);
break;
case OBS_FADER_IEC:
db = iec_def_to_db(def);
break;
case OBS_FADER_LOG:
db = log_def_to_db(def);
break;
default:
goto fail;
break;
}
return db;
fail:
return -INFINITY;
}

View file

@ -229,30 +229,21 @@ EXPORT void obs_volmeter_set_update_interval(obs_volmeter_t *volmeter,
EXPORT unsigned int obs_volmeter_get_update_interval(obs_volmeter_t *volmeter);
/**
* @brief Set the peak hold time for the volume meter
* @brief Get the number of channels which are configured for this source.
* @param volmeter pointer to the volume meter object
* @param ms peak hold time in ms
*/
EXPORT void obs_volmeter_set_peak_hold(obs_volmeter_t *volmeter,
const unsigned int ms);
EXPORT int obs_volmeter_get_nr_channels(obs_volmeter_t *volmeter);
/**
* @brief Get the peak hold time for the volume meter
* @param volmeter pointer to the volume meter object
* @return the peak hold time in ms
*/
EXPORT unsigned int obs_volmeter_get_peak_hold(obs_volmeter_t *volmeter);
typedef void (*obs_volmeter_updated_t)(void *param, float level,
float magnitude, float peak, float muted);
typedef void (*obs_volmeter_updated_t)(void *param,
const float magnitude[MAX_AUDIO_CHANNELS],
const float peak[MAX_AUDIO_CHANNELS],
const float input_peak[MAX_AUDIO_CHANNELS]);
EXPORT void obs_volmeter_add_callback(obs_volmeter_t *volmeter,
obs_volmeter_updated_t callback, void *param);
EXPORT void obs_volmeter_remove_callback(obs_volmeter_t *volmeter,
obs_volmeter_updated_t callback, void *param);
EXPORT float obs_volmeter_get_cur_db(enum obs_fader_type type, const float def);
#ifdef __cplusplus
}
#endif