diff --git a/libobs/obs-audio-controls.c b/libobs/obs-audio-controls.c index 08b825938..101b5b311 100644 --- a/libobs/obs-audio-controls.c +++ b/libobs/obs-audio-controls.c @@ -46,11 +46,27 @@ struct obs_fader { bool ignore_next_signal; }; +struct obs_volmeter { + pthread_mutex_t mutex; + signal_handler_t *signals; + 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; +}; + static const char *fader_signals[] = { "void volume_changed(ptr fader, float db)", NULL }; +static const char *volmeter_signals[] = { + "void levels_updated(ptr volmeter, float level, " + "float magnitude, float peak)", + NULL +}; + static inline float mul_to_db(const float mul) { return (mul == 0.0f) ? -INFINITY : 20.0f * log10f(mul); @@ -81,6 +97,94 @@ static float cubic_db_to_def(const float db) return cbrtf(db_to_mul(db)); } +static float iec_def_to_db(const float def) +{ + if (def == 1.0f) + return 0.0f; + else if (def <= 0.0f) + return -INFINITY; + + float db; + + if (def >= 0.75f) + db = (def - 1.0f) / 0.25f * 9.0f; + else if (def >= 0.5f) + db = (def - 0.75f) / 0.25f * 11.0f - 9.0f; + else if (def >= 0.3f) + db = (def - 0.5f) / 0.2f * 10.0f - 20.0f; + else if (def >= 0.15f) + db = (def - 0.3f) / 0.15f * 10.0f - 30.0f; + else if (def >= 0.075f) + db = (def - 0.15f) / 0.075f * 10.0f - 40.0f; + else if (def >= 0.025f) + db = (def - 0.075f) / 0.05f * 10.0f - 50.0f; + else if (def >= 0.001f) + db = (def - 0.025f) / 0.025f * 90.0f - 60.0f; + else + db = -INFINITY; + + return db; +} + +static float iec_db_to_def(const float db) +{ + if (db == 0.0f) + return 1.0f; + else if (db == -INFINITY) + return 0.0f; + + float def; + + if (db >= -9.0f) + def = (db + 9.0f) / 9.0f * 0.25f + 0.75f; + else if (db >= -20.0f) + def = (db + 20.0f) / 11.0f * 0.25f + 0.5f; + else if (db >= -30.0f) + def = (db + 30.0f) / 10.0f * 0.2f + 0.3f; + else if (db >= -40.0f) + def = (db + 40.0f) / 10.0f * 0.15f + 0.15f; + else if (db >= -50.0f) + def = (db + 50.0f) / 10.0f * 0.075f + 0.075f; + else if (db >= -60.0f) + def = (db + 60.0f) / 10.0f * 0.05f + 0.025f; + else if (db >= -114.0f) + def = (db + 150.0f) / 90.0f * 0.025f; + else + def = 0.0f; + + return def; +} + +#define LOG_OFFSET_DB 6.0f +#define LOG_RANGE_DB 96.0f +/* equals -log10f(LOG_OFFSET_DB) */ +#define LOG_OFFSET_VAL -0.77815125038364363f +/* equals -log10f(-LOG_RANGE_DB + LOG_OFFSET_DB) */ +#define LOG_RANGE_VAL -2.00860017176191756f + +static float log_def_to_db(const float def) +{ + if (def >= 1.0f) + return 0.0f; + else if (def <= 0.0f) + return -INFINITY; + + return -(LOG_RANGE_DB + LOG_OFFSET_DB) * powf( + (LOG_RANGE_DB + LOG_OFFSET_DB) / LOG_OFFSET_DB, -def) + + LOG_OFFSET_DB; +} + +static float log_db_to_def(const float db) +{ + if (db >= 0.0f) + return 1.0f; + else if (db <= -96.0f) + return 0.0f; + + return (-log10f(-db + LOG_OFFSET_DB) - LOG_RANGE_VAL) + / (LOG_OFFSET_VAL - LOG_RANGE_VAL); +} + static void signal_volume_changed(signal_handler_t *sh, struct obs_fader *fader, const float db) { @@ -96,6 +200,24 @@ static void signal_volume_changed(signal_handler_t *sh, calldata_free(&data); } +static void signal_levels_updated(signal_handler_t *sh, + struct obs_volmeter *volmeter, + const float level, const float magnitude, const float peak) +{ + struct calldata data; + + calldata_init(&data); + + calldata_set_ptr (&data, "volmeter", volmeter); + calldata_set_float(&data, "level", level); + calldata_set_float(&data, "magnitude", magnitude); + calldata_set_float(&data, "peak", peak); + + signal_handler_signal(sh, "levels_updated", &data); + + calldata_free(&data); +} + static void fader_source_volume_changed(void *vptr, calldata_t *calldata) { struct obs_fader *fader = (struct obs_fader *) vptr; @@ -118,6 +240,18 @@ static void fader_source_volume_changed(void *vptr, calldata_t *calldata) signal_volume_changed(sh, fader, db); } +static void volmeter_source_volume_changed(void *vptr, calldata_t *calldata) +{ + struct obs_volmeter *volmeter = (struct obs_volmeter *) vptr; + + pthread_mutex_lock(&volmeter->mutex); + + float mul = (float) calldata_float(calldata, "volume"); + volmeter->cur_db = mul_to_db(mul); + + pthread_mutex_unlock(&volmeter->mutex); +} + static void fader_source_destroyed(void *vptr, calldata_t *calldata) { UNUSED_PARAMETER(calldata); @@ -126,6 +260,37 @@ static void fader_source_destroyed(void *vptr, calldata_t *calldata) obs_fader_detach_source(fader); } +static void volmeter_source_volume_levels(void *vptr, calldata_t *calldata) +{ + struct obs_volmeter *volmeter = (struct obs_volmeter *) vptr; + + pthread_mutex_lock(&volmeter->mutex); + + float mul = db_to_mul(volmeter->cur_db); + + float level = (float) calldata_float(calldata, "level"); + float magnitude = (float) calldata_float(calldata, "magnitude"); + float peak = (float) calldata_float(calldata, "peak"); + + level = volmeter->db_to_pos(mul_to_db(level * mul)); + magnitude = volmeter->db_to_pos(mul_to_db(magnitude * mul)); + peak = volmeter->db_to_pos(mul_to_db(peak * mul)); + + signal_handler_t *sh = volmeter->signals; + + pthread_mutex_unlock(&volmeter->mutex); + + signal_levels_updated(sh, volmeter, level, magnitude, peak); +} + +static void volmeter_source_destroyed(void *vptr, calldata_t *calldata) +{ + UNUSED_PARAMETER(calldata); + struct obs_volmeter *volmeter = (struct obs_volmeter *) vptr; + + obs_volmeter_detach_source(volmeter); +} + obs_fader_t *obs_fader_create(enum obs_fader_type type) { struct obs_fader *fader = bzalloc(sizeof(struct obs_fader)); @@ -148,6 +313,18 @@ obs_fader_t *obs_fader_create(enum obs_fader_type type) fader->max_db = 0.0f; fader->min_db = -INFINITY; break; + case OBS_FADER_IEC: + fader->def_to_db = iec_def_to_db; + fader->db_to_def = iec_db_to_def; + fader->max_db = 0.0f; + fader->min_db = -INFINITY; + break; + case OBS_FADER_LOG: + fader->def_to_db = log_def_to_db; + fader->db_to_def = log_db_to_def; + fader->max_db = 0.0f; + fader->min_db = -96.0f; + break; default: goto fail; break; @@ -309,3 +486,114 @@ signal_handler_t *obs_fader_get_signal_handler(obs_fader_t *fader) return (fader) ? fader->signals : NULL; } +obs_volmeter_t *obs_volmeter_create(enum obs_fader_type type) +{ + struct obs_volmeter *volmeter = bzalloc(sizeof(struct obs_volmeter)); + if (!volmeter) + return NULL; + + pthread_mutex_init_value(&volmeter->mutex); + if (pthread_mutex_init(&volmeter->mutex, NULL) != 0) + goto fail; + volmeter->signals = signal_handler_create(); + if (!volmeter->signals) + goto fail; + if (!signal_handler_add_array(volmeter->signals, volmeter_signals)) + 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; + + return volmeter; +fail: + obs_volmeter_destroy(volmeter); + return NULL; +} + +void obs_volmeter_destroy(obs_volmeter_t *volmeter) +{ + if (!volmeter) + return; + + obs_volmeter_detach_source(volmeter); + signal_handler_destroy(volmeter->signals); + pthread_mutex_destroy(&volmeter->mutex); + + bfree(volmeter); +} + +bool obs_volmeter_attach_source(obs_volmeter_t *volmeter, obs_source_t *source) +{ + signal_handler_t *sh; + + if (!volmeter || !source) + return false; + + obs_volmeter_detach_source(volmeter); + + pthread_mutex_lock(&volmeter->mutex); + + sh = obs_source_get_signal_handler(source); + signal_handler_connect(sh, "volume", + volmeter_source_volume_changed, volmeter); + signal_handler_connect(sh, "volume_level", + volmeter_source_volume_levels, volmeter); + signal_handler_connect(sh, "destroy", + volmeter_source_destroyed, volmeter); + + volmeter->source = source; + volmeter->cur_db = mul_to_db(obs_source_get_volume(source)); + + pthread_mutex_unlock(&volmeter->mutex); + + return true; +} + +void obs_volmeter_detach_source(obs_volmeter_t *volmeter) +{ + signal_handler_t *sh; + + if (!volmeter) + return; + + pthread_mutex_lock(&volmeter->mutex); + + if (!volmeter->source) + goto exit; + + sh = obs_source_get_signal_handler(volmeter->source); + signal_handler_disconnect(sh, "volume", + volmeter_source_volume_changed, volmeter); + signal_handler_disconnect(sh, "volume_level", + volmeter_source_volume_levels, volmeter); + signal_handler_disconnect(sh, "destroy", + volmeter_source_destroyed, volmeter); + + volmeter->source = NULL; + +exit: + pthread_mutex_unlock(&volmeter->mutex); +} + +signal_handler_t *obs_volmeter_get_signal_handler(obs_volmeter_t *volmeter) +{ + return (volmeter) ? volmeter->signals : NULL; +} + diff --git a/libobs/obs-audio-controls.h b/libobs/obs-audio-controls.h index e0eeefe9a..ed5a5b798 100644 --- a/libobs/obs-audio-controls.h +++ b/libobs/obs-audio-controls.h @@ -41,7 +41,31 @@ enum obs_fader_type { * results while being quite performant. * The input value is mapped to mul values with the simple formula x^3. */ - OBS_FADER_CUBIC + OBS_FADER_CUBIC, + /** + * @brief A fader compliant to IEC 60-268-18 + * + * This type of fader has several segments with different slopes that + * map deflection linearly to dB values. The segments are defined as + * in the following table: + * + @code + Deflection | Volume + ------------------------------------------ + [ 100 %, 75 % ] | [ 0 dB, -9 dB ] + [ 75 %, 50 % ] | [ -9 dB, -20 dB ] + [ 50 %, 30 % ] | [ -20 dB, -30 dB ] + [ 30 %, 15 % ] | [ -30 dB, -40 dB ] + [ 15 %, 7.5 % ] | [ -40 dB, -50 dB ] + [ 7.5 %, 2.5 % ] | [ -50 dB, -60 dB ] + [ 2.5 %, 0 % ] | [ -60 dB, -inf dB ] + @endcode + */ + OBS_FADER_IEC, + /** + * @brief Logarithmic fader + */ + OBS_FADER_LOG }; /** @@ -137,6 +161,53 @@ EXPORT void obs_fader_detach_source(obs_fader_t *fader); */ EXPORT signal_handler_t *obs_fader_get_signal_handler(obs_fader_t *fader); +/** + * @brief Create a volume meter + * @param type the mapping type to use for the volume meter + * @return pointer to the volume meter object + * + * A volume meter object is used to prepare the sound levels reported by audio + * sources for display in a GUI. + * It will automatically take source volume into account and map the levels + * to a range [0.0f, 1.0f]. + */ +EXPORT obs_volmeter_t *obs_volmeter_create(enum obs_fader_type type); + +/** + * @brief Destroy a volume meter + * @param volmeter pointer to the volmeter object + * + * Destroy the volume meter and free all related data + */ +EXPORT void obs_volmeter_destroy(obs_volmeter_t *volmeter); + +/** + * @brief Attach the volume meter to a source + * @param volmeter pointer to the volume meter object + * @param source pointer to the source object + * @return true on success + * + * When the volume meter is attached to a source it will start to listen to + * volume updates on the source and after preparing the data emit its own + * signal. + */ +EXPORT bool obs_volmeter_attach_source(obs_volmeter_t *volmeter, + obs_source_t *source); + +/** + * @brief Detach the volume meter from the currently attached source + * @param volmeter pointer to the volume meter object + */ +EXPORT void obs_volmeter_detach_source(obs_volmeter_t *volmeter); + +/** + * @brief Get signal handler for the volume meter object + * @param volmeter pointer to the volume meter object + * @return signal handler + */ +EXPORT signal_handler_t *obs_volmeter_get_signal_handler( + obs_volmeter_t *volmeter); + #ifdef __cplusplus } #endif diff --git a/libobs/obs-source.c b/libobs/obs-source.c index c45dd99fb..37df484f1 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -627,15 +627,6 @@ static inline void handle_ts_jump(obs_source_t *source, uint64_t expected, reset_audio_timing(source, ts, os_time); } -#define VOL_MIN -96.0f -#define VOL_MAX 0.0f - -static inline float to_db(float val) -{ - float db = 20.0f * log10f(val); - return isfinite(db) ? db : VOL_MIN; -} - static void calc_volume_levels(struct obs_source *source, float *array, size_t frames, float volume) { @@ -665,8 +656,8 @@ static void calc_volume_levels(struct obs_source *source, float *array, UNUSED_PARAMETER(volume); - rms_val = to_db(sqrtf(sum_val / (float)count)); - max_val = to_db(sqrtf(max_val)); + rms_val = sqrtf(sum_val / (float)count); + max_val = sqrtf(max_val); if (max_val > source->vol_max) source->vol_max = max_val; diff --git a/libobs/obs.h b/libobs/obs.h index 04a7b4738..7f7388bb4 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -48,6 +48,7 @@ struct obs_encoder; struct obs_service; struct obs_module; struct obs_fader; +struct obs_volmeter; typedef struct obs_display obs_display_t; typedef struct obs_view obs_view_t; @@ -59,6 +60,7 @@ typedef struct obs_encoder obs_encoder_t; typedef struct obs_service obs_service_t; typedef struct obs_module obs_module_t; typedef struct obs_fader obs_fader_t; +typedef struct obs_volmeter obs_volmeter_t; #include "obs-source.h" #include "obs-encoder.h" diff --git a/obs/volume-control.cpp b/obs/volume-control.cpp index e14d9f10d..76fb1a5f7 100644 --- a/obs/volume-control.cpp +++ b/obs/volume-control.cpp @@ -12,31 +12,8 @@ using namespace std; -#define VOL_MIN -96.0f -#define VOL_MAX 0.0f - -/* - VOL_MIN_LOG = DBToLog(VOL_MIN) - VOL_MAX_LOG = DBToLog(VOL_MAX) - ... just in case someone wants to use a smaller scale - */ - -#define VOL_MIN_LOG -2.0086001717619175 -#define VOL_MAX_LOG -0.77815125038364363 - #define UPDATE_INTERVAL_MS 50 -static inline float DBToLog(float db) -{ - return -log10f(0.0f - (db - 6.0f)); -} - -static inline float DBToLinear(float db_full) -{ - float db = fmaxf(fminf(db_full, VOL_MAX), VOL_MIN); - return (DBToLog(db) - VOL_MIN_LOG) / (VOL_MAX_LOG - VOL_MIN_LOG); -} - void VolControl::OBSVolumeChanged(void *data, calldata_t *calldata) { Q_UNUSED(calldata); @@ -73,11 +50,8 @@ void VolControl::VolumeLevel(float mag, float peak, float peakHold) /* only update after a certain amount of time */ if ((curMeterTime - lastMeterTime) > UPDATE_INTERVAL_MS) { - float vol = (float)slider->value() * 0.01f; lastMeterTime = curMeterTime; - volMeter->setLevels(DBToLinear(mag) * vol, - DBToLinear(peak) * vol, - DBToLinear(peakHold) * vol); + volMeter->setLevels(mag, peak, peakHold); } } @@ -103,7 +77,8 @@ VolControl::VolControl(OBSSource source_) lastMeterTime (0), levelTotal (0.0f), levelCount (0.0f), - obs_fader (obs_fader_create(OBS_FADER_CUBIC)) + obs_fader (obs_fader_create(OBS_FADER_CUBIC)), + obs_volmeter (obs_volmeter_create(OBS_FADER_LOG)) { QVBoxLayout *mainLayout = new QVBoxLayout(); QHBoxLayout *textLayout = new QHBoxLayout(); @@ -141,13 +116,15 @@ VolControl::VolControl(OBSSource source_) signal_handler_connect(obs_fader_get_signal_handler(obs_fader), "volume_changed", OBSVolumeChanged, this); - signal_handler_connect(obs_source_get_signal_handler(source), - "volume_level", OBSVolumeLevel, this); + signal_handler_connect(obs_volmeter_get_signal_handler(obs_volmeter), + "levels_updated", OBSVolumeLevel, this); QWidget::connect(slider, SIGNAL(valueChanged(int)), this, SLOT(SliderChanged(int))); obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + /* Call volume changed once to init the slider position and label */ VolumeChanged(); } @@ -157,10 +134,11 @@ VolControl::~VolControl() signal_handler_disconnect(obs_fader_get_signal_handler(obs_fader), "volume_changed", OBSVolumeChanged, this); - signal_handler_disconnect(obs_source_get_signal_handler(source), - "volume_level", OBSVolumeLevel, this); + signal_handler_disconnect(obs_volmeter_get_signal_handler(obs_volmeter), + "levels_updated", OBSVolumeLevel, this); obs_fader_destroy(obs_fader); + obs_volmeter_destroy(obs_volmeter); } VolumeMeter::VolumeMeter(QWidget *parent) diff --git a/obs/volume-control.hpp b/obs/volume-control.hpp index 87c03ee57..c43324694 100644 --- a/obs/volume-control.hpp +++ b/obs/volume-control.hpp @@ -36,6 +36,7 @@ private: float levelTotal; float levelCount; obs_fader_t *obs_fader; + obs_volmeter_t *obs_volmeter; static void OBSVolumeChanged(void *param, calldata_t *calldata); static void OBSVolumeLevel(void *data, calldata_t *calldata);