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