Browse Source

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.
Tjienta Vara 8 years ago
parent
commit
50ce228455

+ 10 - 4
UI/data/themes/Dark.qss

@@ -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. */
 }
 
 

+ 10 - 4
UI/data/themes/Default.qss

@@ -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. */
 }
 
 

+ 10 - 6
UI/data/themes/Rachni.qss

@@ -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. */
 }
 
 /*******************/

+ 555 - 91
UI/volume-control.cpp

@@ -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 backgroundNominalColor;
+}
+
+void VolumeMeter::setBackgroundNominalColor(QColor c)
+{
+	backgroundNominalColor = c;
+}
+
+QColor VolumeMeter::getBackgroundWarningColor() const
+{
+	return backgroundWarningColor;
+}
+
+void VolumeMeter::setBackgroundWarningColor(QColor c)
+{
+	backgroundWarningColor = c;
+}
+
+QColor VolumeMeter::getBackgroundErrorColor() const
+{
+	return backgroundErrorColor;
+}
+
+void VolumeMeter::setBackgroundErrorColor(QColor c)
+{
+	backgroundErrorColor = c;
+}
+
+QColor VolumeMeter::getForegroundNominalColor() const
+{
+	return foregroundNominalColor;
+}
+
+void VolumeMeter::setForegroundNominalColor(QColor c)
+{
+	foregroundNominalColor = c;
+}
+
+QColor VolumeMeter::getForegroundWarningColor() const
+{
+	return foregroundWarningColor;
+}
+
+void VolumeMeter::setForegroundWarningColor(QColor c)
 {
-	return bkColor;
+	foregroundWarningColor = c;
 }
 
-void VolumeMeter::setBkColor(QColor c)
+QColor VolumeMeter::getForegroundErrorColor() const
 {
-	bkColor = c;
+	return foregroundErrorColor;
 }
 
-QColor VolumeMeter::getMagColor() const
+void VolumeMeter::setForegroundErrorColor(QColor c)
 {
-	return magColor;
+	foregroundErrorColor = c;
 }
 
-void VolumeMeter::setMagColor(QColor c)
+QColor VolumeMeter::getClipColor() const
 {
-	magColor = c;
+	return clipColor;
 }
 
-QColor VolumeMeter::getPeakColor() const
+void VolumeMeter::setClipColor(QColor c)
 {
-	return peakColor;
+	clipColor = c;
 }
 
-void VolumeMeter::setPeakColor(QColor c)
+QColor VolumeMeter::getMagnitudeColor() const
 {
-	peakColor = c;
+	return magnitudeColor;
 }
 
-QColor VolumeMeter::getPeakHoldColor() const
+void VolumeMeter::setMagnitudeColor(QColor c)
 {
-	return peakHoldColor;
+	magnitudeColor = c;
 }
 
-void VolumeMeter::setPeakHoldColor(QColor c)
+QColor VolumeMeter::getMajorTickColor() const
 {
-	peakHoldColor = c;
+	return majorTickColor;
 }
 
-QColor VolumeMeter::getClipColor1() const
+void VolumeMeter::setMajorTickColor(QColor c)
 {
-	return clipColor1;
+	majorTickColor = c;
 }
 
-void VolumeMeter::setClipColor1(QColor c)
+QColor VolumeMeter::getMinorTickColor() const
 {
-	clipColor1 = c;
+	return minorTickColor;
 }
 
-QColor VolumeMeter::getClipColor2() const
+void VolumeMeter::setMinorTickColor(QColor c)
 {
-	return clipColor2;
+	minorTickColor = c;
 }
 
-void VolumeMeter::setClipColor2(QColor c)
+qreal VolumeMeter::getMinimumLevel() const
 {
-	clipColor2 = c;
+	return minimumLevel;
 }
 
+void VolumeMeter::setMinimumLevel(qreal v)
+{
+	minimumLevel = v;
+}
 
-VolumeMeter::VolumeMeter(QWidget *parent)
-			: QWidget(parent)
+qreal VolumeMeter::getWarningLevel() const
 {
-	setMinimumSize(1, 3);
+	return warningLevel;
+}
 
-	//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::setWarningLevel(qreal v)
+{
+	warningLevel = v;
+}
 
-	clipColor1.setRgb(0x7F, 0x00, 0x00);
-	clipColor2.setRgb(0xFF, 0x00, 0x00);
+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,84 +448,405 @@ 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;
+	currentLastUpdateTime = ts;
+	for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) {
+		currentMagnitude[channelNr] = magnitude[channelNr];
+		currentPeak[channelNr] = peak[channelNr];
+		currentInputPeak[channelNr] = inputPeak[channelNr];
+	}
+
+	// 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);
 }
 
-inline void VolumeMeter::calcLevels()
+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()
 {
-	uint64_t ts = os_gettime_ns();
 	QMutexLocker locker(&dataMutex);
 
-	if (lastUpdateTime && ts - lastUpdateTime > 1000000000) {
-		mag = peak = peakHold = 0.0f;
-		multiple = 1.0f;
-		lastUpdateTime = 0;
+	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();
 	}
+}
 
-	if (multiple > 0.0f) {
-		curMag = mag / multiple;
-		curPeak = peak / multiple;
-		curPeakHold = peakHold / multiple;
+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;
+		}
+	}
 
-		mag = peak = peakHold = multiple = 0.0f;
+	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);
 	}
 }
 
-void VolumeMeter::paintEvent(QPaintEvent *event)
+inline void VolumeMeter::calculateBallistics(uint64_t ts,
+	qreal timeSinceLastRedraw)
 {
-	UNUSED_PARAMETER(event);
+	QMutexLocker locker(&dataMutex);
 
-	QPainter painter(this);
-	QLinearGradient gradient;
+	for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) {
+		calculateBallisticsForChannel(channelNr, ts,
+			timeSinceLastRedraw);
+	}
+}
 
-	int width  = size().width();
-	int height = size().height();
+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);
 
-	calcLevels();
+	// 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];
 
-	int scaledMag      = int((float)width * curMag);
-	int scaledPeak     = int((float)width * curPeak);
-	int scaledPeakHold = int((float)width * curPeakHold);
+		snprintf(str, sizeof (str), "%i", i);
 
-	float db = obs_volmeter_get_cur_db(OBS_FADER_LOG, curPeakHold);
+		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);
+	}
 
-	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);
+	// Draw minor tick lines.
+	painter.setPen(minorTickColor);
+	for (int i = 0; i > minimumLevel; i--) {
+		int position = x + width - (i * scale) - 1;
 
-	// RMS
-	painter.fillRect(0, 0, 
-			scaledMag, height,
-			db == 0.0f ? clipColor1 : magColor);
+		if (i % 5 != 0) {
+			painter.drawLine(position, y, position, y + 1);
+		}
+	}
+}
 
-	// RMS - Peak gradient
-	painter.fillRect(scaledMag, 0,
-			scaledPeak - scaledMag + 1, height,
-			QBrush(gradient));
+void VolumeMeter::paintMeter(QPainter &painter, int x, int y,
+	int width, int height, float magnitude, float peak, float peakHold)
+{
+	qreal scale = width / minimumLevel;
 
-	// Background
-	painter.fillRect(scaledPeak, 0,
-			width - scaledPeak, height,
-			bkColor);
+	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);
+	}
 
-	// Peak hold
-	if (peakHold == 1.0f)
-		scaledPeakHold--;
+	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);
+	}
 
-	painter.setPen(peakHoldColor);
-	painter.drawLine(scaledPeakHold, 0,
-		scaledPeakHold, height);
+	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);
+	}
+}
+
+void VolumeMeter::paintEvent(QPaintEvent *event)
+{
+	UNUSED_PARAMETER(event);
+
+	uint64_t ts = os_gettime_ns();
+	qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001;
+
+	int width  = size().width();
+	int height = size().height();
+
+	handleChannelCofigurationChange();
+	calculateBallistics(ts, timeSinceLastRedraw);
+	bool idle = detectIdle(ts);
+
+	// 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);
+
+		QColor clearColor(0, 0, 0, 0);
+		tickPaintCache->fill(clearColor);
+
+		QPainter tickPainter(tickPaintCache);
+		paintTicks(tickPainter, 0, 0, tickPaintCacheSize.width(),
+			tickPaintCacheSize.height());
+		tickPainter.end();
+	}
+
+	// Actual painting of the widget starts here.
+	QPainter painter(this);
+	painter.drawPixmap(5, height - 9, *tickPaintCache);
+
+	for (int channelNr = 0; channelNr < displayNrAudioChannels;
+		channelNr++) {
+		paintMeter(painter,
+			5, channelNr * 4, width - 5, 3,
+			displayMagnitude[channelNr], displayPeak[channelNr],
+			displayPeakHold[channelNr]);
+
+		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)

+ 164 - 29
UI/volume-control.hpp

@@ -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();

+ 78 - 194
libobs/obs-audio-controls.c

@@ -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];
+
+			peak = fmaxf(peak, fabsf(sample));
+			sum_of_squares += (sample * sample);
+		}
 
-		volmeter_calc_ival_levels(volmeter);
-		updated = true;
+		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);
-
-	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));
+	volmeter_process_audio_data(volmeter, data);
+
+	// 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);
-
-	volmeter_update_audio_settings(volmeter);
-}
-
-unsigned int obs_volmeter_get_peak_hold(obs_volmeter_t *volmeter)
-{
-	if (!volmeter)
-		return 0;
+	if (volmeter->source) {
+		source_nr_audio_channels = get_audio_channels(
+			volmeter->source->sample_info.speakers);
+	} else {
+		source_nr_audio_channels = 1;
+	}
 
-	pthread_mutex_lock(&volmeter->mutex);
-	const unsigned int peakhold = volmeter->peakhold_ms;
-	pthread_mutex_unlock(&volmeter->mutex);
+	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;
+	}
 
-	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;
-}

+ 6 - 15
libobs/obs-audio-controls.h

@@ -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);
-
-/**
- * @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);
+EXPORT int obs_volmeter_get_nr_channels(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