mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-07-07 03:53:38 +00:00
Added simple volume meter for reference of input levels.
This commit is contained in:
parent
1e525c4713
commit
bc542a3e75
|
@ -713,8 +713,9 @@ uint32_t audio_output_samplerate(audio_t audio)
|
||||||
|
|
||||||
/* TODO: Optimization of volume multiplication functions */
|
/* TODO: Optimization of volume multiplication functions */
|
||||||
|
|
||||||
static inline void mul_vol_u8bit(void *array, float volume, size_t total_num)
|
static inline int mul_vol_u8bit(void *array, float volume, size_t total_num)
|
||||||
{
|
{
|
||||||
|
int maxVol = 0;
|
||||||
uint8_t *vals = array;
|
uint8_t *vals = array;
|
||||||
int32_t vol = (int32_t)(volume * 127.0f);
|
int32_t vol = (int32_t)(volume * 127.0f);
|
||||||
|
|
||||||
|
@ -722,18 +723,23 @@ static inline void mul_vol_u8bit(void *array, float volume, size_t total_num)
|
||||||
int32_t val = (int32_t)vals[i] - 128;
|
int32_t val = (int32_t)vals[i] - 128;
|
||||||
int32_t output = val * vol / 127;
|
int32_t output = val * vol / 127;
|
||||||
vals[i] = (uint8_t)(CLAMP(output, MIN_S8, MAX_S8) + 128);
|
vals[i] = (uint8_t)(CLAMP(output, MIN_S8, MAX_S8) + 128);
|
||||||
|
maxVol = max(maxVol, abs(vals[i]));
|
||||||
}
|
}
|
||||||
|
return maxVol * (10000 / MAX_S8);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void mul_vol_16bit(void *array, float volume, size_t total_num)
|
static inline int mul_vol_16bit(void *array, float volume, size_t total_num)
|
||||||
{
|
{
|
||||||
|
int maxVol = 0;
|
||||||
uint16_t *vals = array;
|
uint16_t *vals = array;
|
||||||
int64_t vol = (int64_t)(volume * 32767.0f);
|
int64_t vol = (int64_t)(volume * 32767.0f);
|
||||||
|
|
||||||
for (size_t i = 0; i < total_num; i++) {
|
for (size_t i = 0; i < total_num; i++) {
|
||||||
int64_t output = (int64_t)vals[i] * vol / 32767;
|
int64_t output = (int64_t)vals[i] * vol / 32767;
|
||||||
vals[i] = (int32_t)CLAMP(output, MIN_S16, MAX_S16);
|
vals[i] = (int32_t)CLAMP(output, MIN_S16, MAX_S16);
|
||||||
|
maxVol = max(maxVol, abs(vals[i]));
|
||||||
}
|
}
|
||||||
|
return maxVol * (10000 / MAX_S16);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline float conv_24bit_to_float(uint8_t *vals)
|
static inline float conv_24bit_to_float(uint8_t *vals)
|
||||||
|
@ -756,19 +762,23 @@ static inline void conv_float_to_24bit(float fval, uint8_t *vals)
|
||||||
vals[2] = (val >> 16) & 0xFF;
|
vals[2] = (val >> 16) & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void mul_vol_24bit(void *array, float volume, size_t total_num)
|
static inline int mul_vol_24bit(void *array, float volume, size_t total_num)
|
||||||
{
|
{
|
||||||
|
float maxVol = 0.f;
|
||||||
uint8_t *vals = array;
|
uint8_t *vals = array;
|
||||||
|
|
||||||
for (size_t i = 0; i < total_num; i++) {
|
for (size_t i = 0; i < total_num; i++) {
|
||||||
float val = conv_24bit_to_float(vals) * volume;
|
float val = conv_24bit_to_float(vals) * volume;
|
||||||
conv_float_to_24bit(CLAMP(val, -1.0f, 1.0f), vals);
|
conv_float_to_24bit(CLAMP(val, -1.0f, 1.0f), vals);
|
||||||
vals += 3;
|
vals += 3;
|
||||||
|
maxVol = max(maxVol, (float)fabs(val));
|
||||||
}
|
}
|
||||||
|
return (int) (maxVol * 10000.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void mul_vol_32bit(void *array, float volume, size_t total_num)
|
static inline int mul_vol_32bit(void *array, float volume, size_t total_num)
|
||||||
{
|
{
|
||||||
|
int maxVol = 0;
|
||||||
int32_t *vals = array;
|
int32_t *vals = array;
|
||||||
double dvol = (double)volume;
|
double dvol = (double)volume;
|
||||||
|
|
||||||
|
@ -776,20 +786,33 @@ static inline void mul_vol_32bit(void *array, float volume, size_t total_num)
|
||||||
double val = (double)vals[i] / 2147483647.0;
|
double val = (double)vals[i] / 2147483647.0;
|
||||||
double output = val * dvol;
|
double output = val * dvol;
|
||||||
vals[i] = (int32_t)(CLAMP(output, -1.0, 1.0) * 2147483647.0);
|
vals[i] = (int32_t)(CLAMP(output, -1.0, 1.0) * 2147483647.0);
|
||||||
|
maxVol = max(maxVol, abs(vals[i]));
|
||||||
}
|
}
|
||||||
|
return maxVol * (10000 / MAX_S32);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void mul_vol_float(void *array, float volume, size_t total_num)
|
static inline int mul_vol_float(void *array, float volume, size_t total_num)
|
||||||
{
|
{
|
||||||
|
float maxVol = 0;
|
||||||
float *vals = array;
|
float *vals = array;
|
||||||
|
|
||||||
for (size_t i = 0; i < total_num; i++)
|
for (size_t i = 0; i < total_num; i++) {
|
||||||
vals[i] *= volume;
|
vals[i] *= volume;
|
||||||
|
maxVol = max(maxVol, (float)fabs(vals[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)(maxVol * 10000.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void audio_line_place_data_pos(struct audio_line *line,
|
// [Danni] changed to int for volume feedback. Seems like the most logical
|
||||||
|
// place to calculate this to avoid unnessisary iterations.
|
||||||
|
// scaled to max of 10000.
|
||||||
|
|
||||||
|
static int audio_line_place_data_pos(struct audio_line *line,
|
||||||
const struct audio_data *data, size_t position)
|
const struct audio_data *data, size_t position)
|
||||||
{
|
{
|
||||||
|
int maxVol = 0;
|
||||||
|
|
||||||
bool planar = line->audio->planes > 1;
|
bool planar = line->audio->planes > 1;
|
||||||
size_t total_num = data->frames * (planar ? 1 : line->audio->channels);
|
size_t total_num = data->frames * (planar ? 1 : line->audio->channels);
|
||||||
size_t total_size = data->frames * line->audio->block_size;
|
size_t total_size = data->frames * line->audio->block_size;
|
||||||
|
@ -803,19 +826,19 @@ static void audio_line_place_data_pos(struct audio_line *line,
|
||||||
switch (line->audio->info.format) {
|
switch (line->audio->info.format) {
|
||||||
case AUDIO_FORMAT_U8BIT:
|
case AUDIO_FORMAT_U8BIT:
|
||||||
case AUDIO_FORMAT_U8BIT_PLANAR:
|
case AUDIO_FORMAT_U8BIT_PLANAR:
|
||||||
mul_vol_u8bit(array, data->volume, total_num);
|
maxVol = mul_vol_u8bit(array, data->volume, total_num);
|
||||||
break;
|
break;
|
||||||
case AUDIO_FORMAT_16BIT:
|
case AUDIO_FORMAT_16BIT:
|
||||||
case AUDIO_FORMAT_16BIT_PLANAR:
|
case AUDIO_FORMAT_16BIT_PLANAR:
|
||||||
mul_vol_16bit(array, data->volume, total_num);
|
maxVol = mul_vol_16bit(array, data->volume, total_num);
|
||||||
break;
|
break;
|
||||||
case AUDIO_FORMAT_32BIT:
|
case AUDIO_FORMAT_32BIT:
|
||||||
case AUDIO_FORMAT_32BIT_PLANAR:
|
case AUDIO_FORMAT_32BIT_PLANAR:
|
||||||
mul_vol_32bit(array, data->volume, total_num);
|
maxVol = mul_vol_32bit(array, data->volume, total_num);
|
||||||
break;
|
break;
|
||||||
case AUDIO_FORMAT_FLOAT:
|
case AUDIO_FORMAT_FLOAT:
|
||||||
case AUDIO_FORMAT_FLOAT_PLANAR:
|
case AUDIO_FORMAT_FLOAT_PLANAR:
|
||||||
mul_vol_float(array, data->volume, total_num);
|
maxVol = mul_vol_float(array, data->volume, total_num);
|
||||||
break;
|
break;
|
||||||
case AUDIO_FORMAT_UNKNOWN:
|
case AUDIO_FORMAT_UNKNOWN:
|
||||||
blog(LOG_ERROR, "audio_line_place_data_pos: "
|
blog(LOG_ERROR, "audio_line_place_data_pos: "
|
||||||
|
@ -826,9 +849,10 @@ static void audio_line_place_data_pos(struct audio_line *line,
|
||||||
circlebuf_place(&line->buffers[i], position,
|
circlebuf_place(&line->buffers[i], position,
|
||||||
line->volume_buffers[i].array, total_size);
|
line->volume_buffers[i].array, total_size);
|
||||||
}
|
}
|
||||||
|
return maxVol;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void audio_line_place_data(struct audio_line *line,
|
static int audio_line_place_data(struct audio_line *line,
|
||||||
const struct audio_data *data)
|
const struct audio_data *data)
|
||||||
{
|
{
|
||||||
size_t pos = ts_diff_bytes(line->audio, data->timestamp,
|
size_t pos = ts_diff_bytes(line->audio, data->timestamp,
|
||||||
|
@ -842,25 +866,26 @@ static void audio_line_place_data(struct audio_line *line,
|
||||||
line->buffers[0].size);
|
line->buffers[0].size);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
audio_line_place_data_pos(line, data, pos);
|
return audio_line_place_data_pos(line, data, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
void audio_line_output(audio_line_t line, const struct audio_data *data)
|
int audio_line_output(audio_line_t line, const struct audio_data *data)
|
||||||
{
|
{
|
||||||
/* TODO: prevent insertation of data too far away from expected
|
/* TODO: prevent insertation of data too far away from expected
|
||||||
* audio timing */
|
* audio timing */
|
||||||
|
|
||||||
if (!line || !data) return;
|
if (!line || !data) return 0;
|
||||||
|
|
||||||
|
int maxVol = 0;
|
||||||
pthread_mutex_lock(&line->mutex);
|
pthread_mutex_lock(&line->mutex);
|
||||||
|
|
||||||
if (!line->buffers[0].size) {
|
if (!line->buffers[0].size) {
|
||||||
line->base_timestamp = data->timestamp -
|
line->base_timestamp = data->timestamp -
|
||||||
line->audio->info.buffer_ms * 1000000;
|
line->audio->info.buffer_ms * 1000000;
|
||||||
audio_line_place_data(line, data);
|
maxVol = audio_line_place_data(line, data);
|
||||||
|
|
||||||
} else if (line->base_timestamp <= data->timestamp) {
|
} else if (line->base_timestamp <= data->timestamp) {
|
||||||
audio_line_place_data(line, data);
|
maxVol = audio_line_place_data(line, data);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
blog(LOG_DEBUG, "Bad timestamp for audio line '%s', "
|
blog(LOG_DEBUG, "Bad timestamp for audio line '%s', "
|
||||||
|
@ -872,4 +897,5 @@ void audio_line_output(audio_line_t line, const struct audio_data *data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&line->mutex);
|
pthread_mutex_unlock(&line->mutex);
|
||||||
|
return maxVol;
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,7 +190,7 @@ EXPORT const struct audio_output_info *audio_output_getinfo(audio_t audio);
|
||||||
|
|
||||||
EXPORT audio_line_t audio_output_createline(audio_t audio, const char *name);
|
EXPORT audio_line_t audio_output_createline(audio_t audio, const char *name);
|
||||||
EXPORT void audio_line_destroy(audio_line_t line);
|
EXPORT void audio_line_destroy(audio_line_t line);
|
||||||
EXPORT void audio_line_output(audio_line_t line, const struct audio_data *data);
|
EXPORT int audio_line_output(audio_line_t line, const struct audio_data *data);
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "media-io/format-conversion.h"
|
#include "media-io/format-conversion.h"
|
||||||
#include "media-io/video-frame.h"
|
#include "media-io/video-frame.h"
|
||||||
|
#include "media-io/audio-io.h"
|
||||||
#include "util/threading.h"
|
#include "util/threading.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
#include "callback/calldata.h"
|
#include "callback/calldata.h"
|
||||||
|
@ -28,6 +29,8 @@
|
||||||
#include "obs.h"
|
#include "obs.h"
|
||||||
#include "obs-internal.h"
|
#include "obs-internal.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static inline bool source_valid(struct obs_source *source)
|
static inline bool source_valid(struct obs_source *source)
|
||||||
{
|
{
|
||||||
return source && source->context.data;
|
return source && source->context.data;
|
||||||
|
@ -79,6 +82,7 @@ static const char *source_signals[] = {
|
||||||
"void show(ptr source)",
|
"void show(ptr source)",
|
||||||
"void hide(ptr source)",
|
"void hide(ptr source)",
|
||||||
"void volume(ptr source, in out float volume)",
|
"void volume(ptr source, in out float volume)",
|
||||||
|
"void volumelevel(ptr source, in out float volume)",
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -568,8 +572,9 @@ static void source_output_audio_line(obs_source_t source,
|
||||||
in.timestamp += source->timing_adjust + source->sync_offset;
|
in.timestamp += source->timing_adjust + source->sync_offset;
|
||||||
in.volume = source->user_volume * source->present_volume *
|
in.volume = source->user_volume * source->present_volume *
|
||||||
obs->audio.user_volume * obs->audio.present_volume;
|
obs->audio.user_volume * obs->audio.present_volume;
|
||||||
|
|
||||||
audio_line_output(source->audio_line, &in);
|
int vol = audio_line_output(source->audio_line, &in);
|
||||||
|
obs_source_updatevolumelevel(source, vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum convert_type {
|
enum convert_type {
|
||||||
|
@ -1597,6 +1602,22 @@ void obs_source_setvolume(obs_source_t source, float volume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void obs_source_updatevolumelevel(obs_source_t source, int volume)
|
||||||
|
{
|
||||||
|
if (source) {
|
||||||
|
struct calldata data = { 0 };
|
||||||
|
calldata_setptr(&data, "source", source);
|
||||||
|
calldata_setint(&data, "volumelevel", volume);
|
||||||
|
|
||||||
|
signal_handler_signal(source->context.signals, "volumelevel", &data);
|
||||||
|
signal_handler_signal(obs->signals, "source_volumelevel", &data);
|
||||||
|
|
||||||
|
volume = (int)calldata_int(&data, "volumelevel");
|
||||||
|
calldata_free(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void set_tree_preset_vol(obs_source_t parent, obs_source_t child,
|
static void set_tree_preset_vol(obs_source_t parent, obs_source_t child,
|
||||||
void *param)
|
void *param)
|
||||||
{
|
{
|
||||||
|
|
|
@ -554,6 +554,9 @@ EXPORT proc_handler_t obs_source_prochandler(obs_source_t source);
|
||||||
/** Sets the user volume for a source that has audio output */
|
/** Sets the user volume for a source that has audio output */
|
||||||
EXPORT void obs_source_setvolume(obs_source_t source, float volume);
|
EXPORT void obs_source_setvolume(obs_source_t source, float volume);
|
||||||
|
|
||||||
|
/** Updates live volume for a source */
|
||||||
|
EXPORT void obs_source_updatevolumelevel(obs_source_t source, int volume);
|
||||||
|
|
||||||
/** Sets the presentation volume for a source */
|
/** Sets the presentation volume for a source */
|
||||||
EXPORT void obs_source_set_present_volume(obs_source_t source, float volume);
|
EXPORT void obs_source_set_present_volume(obs_source_t source, float volume);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,19 @@ void VolControl::OBSVolumeChanged(void *data, calldata_t calldata)
|
||||||
Q_ARG(int, vol));
|
Q_ARG(int, vol));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [Danni] This may be a bit too resource intensive for such a simple
|
||||||
|
// application.
|
||||||
|
|
||||||
|
void VolControl::OBSVolumeLevel(void *data, calldata_t calldata)
|
||||||
|
{
|
||||||
|
VolControl *volControl = static_cast<VolControl*>(data);
|
||||||
|
int v = calldata_int(calldata, "volumelevel");
|
||||||
|
|
||||||
|
QMetaObject::invokeMethod(volControl, "VolumeLevel",
|
||||||
|
Q_ARG(int, v));
|
||||||
|
}
|
||||||
|
|
||||||
void VolControl::VolumeChanged(int vol)
|
void VolControl::VolumeChanged(int vol)
|
||||||
{
|
{
|
||||||
signalChanged = false;
|
signalChanged = false;
|
||||||
|
@ -24,6 +37,11 @@ void VolControl::VolumeChanged(int vol)
|
||||||
signalChanged = true;
|
signalChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VolControl::VolumeLevel(int vol)
|
||||||
|
{
|
||||||
|
volMeter->setValue(vol); /* linear */
|
||||||
|
}
|
||||||
|
|
||||||
void VolControl::SliderChanged(int vol)
|
void VolControl::SliderChanged(int vol)
|
||||||
{
|
{
|
||||||
if (signalChanged) {
|
if (signalChanged) {
|
||||||
|
@ -48,6 +66,7 @@ VolControl::VolControl(OBSSource source_)
|
||||||
|
|
||||||
nameLabel = new QLabel();
|
nameLabel = new QLabel();
|
||||||
volLabel = new QLabel();
|
volLabel = new QLabel();
|
||||||
|
volMeter = new QProgressBar();
|
||||||
slider = new QSlider(Qt::Horizontal);
|
slider = new QSlider(Qt::Horizontal);
|
||||||
|
|
||||||
QFont font = nameLabel->font();
|
QFont font = nameLabel->font();
|
||||||
|
@ -60,8 +79,17 @@ VolControl::VolControl(OBSSource source_)
|
||||||
slider->setMinimum(0);
|
slider->setMinimum(0);
|
||||||
slider->setMaximum(100);
|
slider->setMaximum(100);
|
||||||
slider->setValue(vol);
|
slider->setValue(vol);
|
||||||
//slider->setMaximumHeight(16);
|
slider->setMaximumHeight(10);
|
||||||
|
|
||||||
|
volMeter->setMaximumHeight(1);
|
||||||
|
volMeter->setMinimum(0);
|
||||||
|
volMeter->setMaximum(10000);
|
||||||
|
volMeter->setTextVisible(false);
|
||||||
|
|
||||||
|
// [Danni] Temporary color.
|
||||||
|
QString testColor = "QProgressBar {border: 0px} QProgressBar::chunk {width: 1px; background-color: #AA0000;}";
|
||||||
|
volMeter->setStyleSheet(testColor);
|
||||||
|
|
||||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
textLayout->addWidget(nameLabel);
|
textLayout->addWidget(nameLabel);
|
||||||
textLayout->addWidget(volLabel);
|
textLayout->addWidget(volLabel);
|
||||||
|
@ -71,6 +99,7 @@ VolControl::VolControl(OBSSource source_)
|
||||||
mainLayout->setContentsMargins(4, 4, 4, 4);
|
mainLayout->setContentsMargins(4, 4, 4, 4);
|
||||||
mainLayout->setSpacing(2);
|
mainLayout->setSpacing(2);
|
||||||
mainLayout->addItem(textLayout);
|
mainLayout->addItem(textLayout);
|
||||||
|
mainLayout->addWidget(volMeter);
|
||||||
mainLayout->addWidget(slider);
|
mainLayout->addWidget(slider);
|
||||||
|
|
||||||
setLayout(mainLayout);
|
setLayout(mainLayout);
|
||||||
|
@ -78,6 +107,9 @@ VolControl::VolControl(OBSSource source_)
|
||||||
signal_handler_connect(obs_source_signalhandler(source),
|
signal_handler_connect(obs_source_signalhandler(source),
|
||||||
"volume", OBSVolumeChanged, this);
|
"volume", OBSVolumeChanged, this);
|
||||||
|
|
||||||
|
signal_handler_connect(obs_source_signalhandler(source),
|
||||||
|
"volumelevel", OBSVolumeLevel, this);
|
||||||
|
|
||||||
QWidget::connect(slider, SIGNAL(valueChanged(int)),
|
QWidget::connect(slider, SIGNAL(valueChanged(int)),
|
||||||
this, SLOT(SliderChanged(int)));
|
this, SLOT(SliderChanged(int)));
|
||||||
}
|
}
|
||||||
|
@ -86,4 +118,7 @@ VolControl::~VolControl()
|
||||||
{
|
{
|
||||||
signal_handler_disconnect(obs_source_signalhandler(source),
|
signal_handler_disconnect(obs_source_signalhandler(source),
|
||||||
"volume", OBSVolumeChanged, this);
|
"volume", OBSVolumeChanged, this);
|
||||||
|
|
||||||
|
signal_handler_disconnect(obs_source_signalhandler(source),
|
||||||
|
"volumelevel", OBSVolumeLevel, this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <obs.hpp>
|
#include <obs.hpp>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QProgressBar>
|
||||||
|
|
||||||
/* TODO: Make a real volume control that isn't terrible */
|
/* TODO: Make a real volume control that isn't terrible */
|
||||||
|
|
||||||
|
@ -13,15 +14,18 @@ class VolControl : public QWidget {
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OBSSource source;
|
OBSSource source;
|
||||||
QLabel *nameLabel;
|
QLabel *nameLabel;
|
||||||
QLabel *volLabel;
|
QLabel *volLabel;
|
||||||
QSlider *slider;
|
QProgressBar *volMeter;
|
||||||
bool signalChanged;
|
QSlider *slider;
|
||||||
|
bool signalChanged;
|
||||||
|
|
||||||
static void OBSVolumeChanged(void *param, calldata_t calldata);
|
static void OBSVolumeChanged(void *param, calldata_t calldata);
|
||||||
|
static void OBSVolumeLevel(void *data, calldata_t calldata);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void VolumeChanged(int vol);
|
void VolumeChanged(int vol);
|
||||||
|
void VolumeLevel(int vol);
|
||||||
void SliderChanged(int vol);
|
void SliderChanged(int vol);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
Loading…
Reference in a new issue