Merge pull request #1128 from takev/volume-meters

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