mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-09-25 07:50:39 -05:00
Merge pull request #1128 from takev/volume-meters
UI: Rework volume-meters, adding more information
This commit is contained in:
+10
-4
@@ -468,10 +468,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. */
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,10 +59,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. */
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -689,12 +689,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. */
|
||||
}
|
||||
|
||||
/*******************/
|
||||
|
||||
+559
-95
@@ -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)
|
||||
|
||||
+164
-29
@@ -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();
|
||||
|
||||
+76
-192
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user