pan y pitchs faders horizontales, funciona varias instancias con
multidispositivo y patcheable en jack.
This commit is contained in:
parent
7631e54d51
commit
103a33820e
12 changed files with 188 additions and 105 deletions
|
@ -46,11 +46,11 @@ AudioLayerWidget::AudioLayerWidget(QWidget *parent, int layer):
|
|||
m_progressTime = new QTimeEdit;
|
||||
m_progressTime->setToolTip("Current Time");
|
||||
m_progressTime->setObjectName("Current Time");
|
||||
m_progressTime->setDisplayFormat("mm:ss:zz");
|
||||
m_progressTime->setDisplayFormat("mm:ss:zzz");
|
||||
m_progressTime->setReadOnly(true);
|
||||
m_progressTime->setButtonSymbols(QAbstractSpinBox::NoButtons);
|
||||
m_progressTime->setMinimumWidth(80);
|
||||
m_progressTime->setMaximumWidth(80);
|
||||
//m_progressTime->setMaximumWidth(80);
|
||||
m_progressTime->setFocusPolicy(Qt::NoFocus);
|
||||
m_progressTime->setAlignment(Qt::AlignHCenter);
|
||||
m_progressTime->setContentsMargins(0,0,0,0);
|
||||
|
@ -68,22 +68,22 @@ AudioLayerWidget::AudioLayerWidget(QWidget *parent, int layer):
|
|||
status->addWidget(m_progressTime);
|
||||
status->addWidget(m_totalTimeValue);
|
||||
layout->addLayout(status);
|
||||
QHBoxLayout *volumeBox = new QHBoxLayout;
|
||||
m_volume = new SliderGroup("Vol", 0 , 100, 2, NULL);
|
||||
volumeBox->addWidget(m_volume);
|
||||
connect(m_volume, SIGNAL(valueChanged(int)), this, SLOT(volumeChanged(int)));
|
||||
QVBoxLayout *volumeBox = new QVBoxLayout;
|
||||
m_pitch = new SliderGroup("Pitch", 0 , 255, 0, NULL);
|
||||
volumeBox->addWidget(m_pitch);
|
||||
connect(m_pitch, SIGNAL(valueChanged(int)), this, SLOT(pitchChanged(int)));
|
||||
m_pan = new SliderGroup("Pan", 0 , 255, 0, NULL);
|
||||
volumeBox->addWidget(m_pan);
|
||||
connect(m_pan, SIGNAL(valueChanged(int)), this, SLOT(panChanged(int)));
|
||||
m_pitch = new SliderGroup("Pitch", 0 , 255, 0, NULL);
|
||||
volumeBox->addWidget(m_pitch);
|
||||
m_volume = new SliderGroup("Vol", 0 , 100, 2, NULL);
|
||||
volumeBox->addWidget(m_volume);
|
||||
connect(m_volume, SIGNAL(valueChanged(int)), this, SLOT(volumeChanged(int)));
|
||||
volumeBox->setSpacing(0);
|
||||
volumeBox->setContentsMargins(0, 0, 0, 0);
|
||||
connect(m_pitch, SIGNAL(valueChanged(int)), this, SLOT(pitchChanged(int)));
|
||||
layout->addLayout(volumeBox);
|
||||
layout->setAlignment(Qt::AlignHCenter);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
layout->setContentsMargins(1, 1, 1, 1);
|
||||
this->setLayout(layout);
|
||||
}
|
||||
|
||||
|
@ -113,13 +113,17 @@ void AudioLayerWidget::toggleSuspendResume()
|
|||
switch (m_status) {
|
||||
case Status::PlayingLoop:
|
||||
case Status::PlayingOnce:
|
||||
case Status::PlayingFolder:
|
||||
case Status::PlayingFolderLoop:
|
||||
case Status::PlayingFolderRandom:
|
||||
m_oldStatus = m_status;
|
||||
this->setPlaybackStatus(Status::Paused);
|
||||
emit uiPlaybackChanged(m_layer, Status::Paused);
|
||||
break;
|
||||
case Status::Paused:
|
||||
case Status::Stopped:
|
||||
this->setPlaybackStatus(Status::PlayingLoop);
|
||||
emit uiPlaybackChanged(m_layer, Status::PlayingLoop);
|
||||
this->setPlaybackStatus(m_oldStatus);
|
||||
emit uiPlaybackChanged(m_layer, m_oldStatus);
|
||||
case Status::Iddle:
|
||||
break;
|
||||
}
|
||||
|
@ -174,10 +178,9 @@ void AudioLayerWidget::setMediaFile(QString file)
|
|||
|
||||
void AudioLayerWidget::setPlaybackStatus(Status s)
|
||||
{
|
||||
Status status = static_cast<Status>(s);
|
||||
m_suspendResumeButton->blockSignals(true);
|
||||
m_status = status;
|
||||
m_suspendResumeButton->setText(StatusStr[status]);
|
||||
m_status = s;
|
||||
m_suspendResumeButton->setText(StatusStr[s]);
|
||||
m_suspendResumeButton->blockSignals(false);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ public:
|
|||
|
||||
private:
|
||||
Status m_status;
|
||||
Status m_oldStatus;
|
||||
int m_layer;
|
||||
QPushButton *m_suspendResumeButton;
|
||||
ClickableLabel *m_fileValue;
|
||||
|
|
|
@ -5,7 +5,8 @@ AudioWidget::AudioWidget(QWidget *parent) :
|
|||
QWidget(parent)
|
||||
, m_layout(new QHBoxLayout())
|
||||
{
|
||||
for (int i= 0; i < Settings::getInstance()->getLayersNumber(); i++ ) {
|
||||
m_layers = Settings::getInstance()->getLayersNumber();
|
||||
for (uint i= 0; i < m_layers; i++ ) {
|
||||
AudioLayerWidget *alw = new AudioLayerWidget(this, i);
|
||||
m_layout->insertWidget(i, alw);
|
||||
connect(alw, SIGNAL(uiSliderChanged(int, Slider, int)), this, SIGNAL(uiSliderChanged(int, Slider, int)));
|
||||
|
@ -64,7 +65,7 @@ void AudioWidget::cursorChanged(int layer, float cursor)
|
|||
|
||||
void AudioWidget::refreshUi()
|
||||
{
|
||||
for (int i = 0; i < MAX_LAYERS; i++)
|
||||
for (uint i = 0; i < m_layers; i++)
|
||||
{
|
||||
if (m_layerUpdate[i].updated) {
|
||||
QLayoutItem * const item = m_layout->itemAt(i);
|
||||
|
|
|
@ -18,6 +18,7 @@ private:
|
|||
QHBoxLayout *m_layout;
|
||||
layerData m_layerUpdate[MAX_LAYERS];
|
||||
QTimer *m_refreshUi;
|
||||
uint m_layers;
|
||||
|
||||
public slots:
|
||||
void volChanged(int layer, float vol);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
#define DEFAULT_FILE "lms-audio.xlm"
|
||||
#define MAX_LAYERS 4
|
||||
#define MAX_AUDIODEVICES 8
|
||||
#define UI_REFRESH_TIME 66
|
||||
#define UI_REFRESH_TIME 100
|
||||
#define FADE_TIME 25 // DMX Frame time, 40 fps, avoid clicks
|
||||
|
||||
struct dmxSetting {
|
||||
|
@ -32,12 +32,12 @@ static const char* StatusStr[] =
|
|||
{
|
||||
"Stop",
|
||||
"Pause",
|
||||
"Playing One",
|
||||
"Playing One Loop",
|
||||
"Play One",
|
||||
"Play One Loop",
|
||||
"Iddle",
|
||||
"Playing Folder",
|
||||
"Playing Folder Loop",
|
||||
"Playing Folder Random",
|
||||
"Play Folder",
|
||||
"Play Folder Loop",
|
||||
"Play Folder Rand",
|
||||
0x0
|
||||
};
|
||||
|
||||
|
|
|
@ -122,7 +122,8 @@ void libreMediaServerAudio::dmxInput(int layer, int channel, int value)
|
|||
#ifndef NOGUI
|
||||
if (m_ui) {
|
||||
m_lmsUi->m_aw->playbackChanged(layer, s);
|
||||
//m_lmsUi->m_aw->cursorChanged(layer, m_mae.getCursor(layer));
|
||||
m_played.clear();
|
||||
m_played.append(m_ola->getValue(layer, DMX_FILE));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -153,8 +154,6 @@ void libreMediaServerAudio::refreshUi() {
|
|||
m_updateUi[i][3] = -1;
|
||||
}
|
||||
if (m_mae.getAtEnd(i)) {
|
||||
if (m_played.isEmpty())
|
||||
m_played.append(m_ola->getValue(i, DMX_FILE));
|
||||
if (m_currentStatus[i] == Status::PlayingOnce) {
|
||||
m_currentStatus[i] = Status::Stopped;
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ ma_result MiniAudioEngine::startContext()
|
|||
resourceManagerConfig = ma_resource_manager_config_init();
|
||||
resourceManagerConfig.decodedFormat = ma_format_f32; /* ma_format_f32 should almost always be used as that's what the engine (and most everything else) uses for mixing. */
|
||||
resourceManagerConfig.decodedChannels = 0;
|
||||
resourceManagerConfig.decodedSampleRate = ma_standard_sample_rate_48000;
|
||||
resourceManagerConfig.decodedSampleRate = ma_standard_sample_rate_44100;
|
||||
resourceManagerConfig.jobThreadCount = 4;
|
||||
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
||||
if (result != MA_SUCCESS) {
|
||||
|
@ -129,6 +129,7 @@ ma_result MiniAudioEngine::loadMedia(int layer, char *file)
|
|||
result = ma_sound_init_from_file(&engine, file, \
|
||||
MA_SOUND_FLAG_NO_SPATIALIZATION \
|
||||
| MA_SOUND_FLAG_DECODE \
|
||||
| MA_SOUND_FLAG_STREAM \
|
||||
/*| MA_SOUND_FLAG_NO_PITCH \*/
|
||||
, NULL, NULL, &m_currentSound[layer]);
|
||||
if (result != MA_SUCCESS)
|
||||
|
@ -144,22 +145,15 @@ ma_result MiniAudioEngine::loadMedia(int layer, char *file)
|
|||
float MiniAudioEngine::getDuration(int layer)
|
||||
{
|
||||
ma_result result;
|
||||
ma_uint64 lengthInPCMFrames;
|
||||
ma_uint32 sampleRate;
|
||||
float ret;
|
||||
|
||||
if (m_mediaLoaded[layer] == false)
|
||||
return MA_DOES_NOT_EXIST;
|
||||
result = ma_sound_get_length_in_pcm_frames(&m_currentSound[layer], &lengthInPCMFrames);
|
||||
result = ma_sound_get_length_in_seconds(&m_currentSound[layer], &ret);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
result = ma_sound_get_data_format(&m_currentSound[layer], NULL, NULL, &sampleRate, NULL, 0);
|
||||
if (result != MA_SUCCESS) {
|
||||
return MA_ERROR;
|
||||
}
|
||||
ret = 1000.0f * (lengthInPCMFrames / float(sampleRate));
|
||||
return ret;
|
||||
return (ret * 1000);
|
||||
}
|
||||
|
||||
float MiniAudioEngine::getCursor(int layer)
|
||||
|
@ -233,27 +227,25 @@ ma_result MiniAudioEngine::playbackChanged(int layer, Status status)
|
|||
|
||||
if (m_mediaLoaded[layer] == false)
|
||||
return MA_DOES_NOT_EXIST;
|
||||
bool loop = false;
|
||||
switch (status) {
|
||||
case Status::Paused:
|
||||
result = ma_sound_stop_with_fade_in_milliseconds(&m_currentSound[layer], FADE_TIME);
|
||||
break;
|
||||
case Status::Stopped:
|
||||
result = ma_sound_stop_with_fade_in_milliseconds(&m_currentSound[layer], FADE_TIME);
|
||||
ma_sound_stop_with_fade_in_milliseconds(&m_currentSound[layer], FADE_TIME);
|
||||
result = this->seekToCursor(layer, m_currentLayerValues[layer].cursor);
|
||||
break;
|
||||
case Status::PlayingLoop:
|
||||
ma_sound_set_stop_time_in_milliseconds(&m_currentSound[layer], ~(ma_uint64)0);
|
||||
ma_sound_set_looping(&m_currentSound[layer], true);
|
||||
result = ma_sound_start(&m_currentSound[layer]);
|
||||
break;
|
||||
loop = true;
|
||||
case Status::PlayingOnce:
|
||||
case Status::PlayingFolder:
|
||||
case Status::PlayingFolderLoop:
|
||||
case Status::PlayingFolderRandom:
|
||||
ma_sound_set_stop_time_in_milliseconds(&m_currentSound[layer], ~(ma_uint64)0);
|
||||
ma_sound_set_looping(&m_currentSound[layer], false);
|
||||
ma_sound_set_looping(&m_currentSound[layer], loop);
|
||||
result = ma_sound_start(&m_currentSound[layer]);
|
||||
break;
|
||||
//this->volChanged(layer, m_currentLayerValues[layer].vol); // glitch when seek to cursor, how flush the audio buffer?
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -273,8 +265,7 @@ ma_result MiniAudioEngine::seekToCursor(int layer, int cursor)
|
|||
if (result != MA_SUCCESS) { return result; }
|
||||
start = (cursor * end) / 65025;
|
||||
result = ma_sound_seek_to_pcm_frame(&m_currentSound[layer], start);
|
||||
//if (result != MA_SUCCESS) { return result; }
|
||||
//result = ma_data_source_set_loop_point_in_pcm_frames(&m_currentSound[layer], start, end);
|
||||
//result = ma_data_source_set_loop_point_in_pcm_frames(&m_currentSound[layer], start, end); // this do nothing here, it must be done after set_looping or start?
|
||||
return (result);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
#ifndef MINIAUDIOENGINE_H
|
||||
#define MINIAUDIOENGINE_H
|
||||
|
||||
#define MA_ENABLE_ONLY_SPECIFIC_BACKENDS 1
|
||||
#define MA_ENABLE_JACK 1
|
||||
#define MA_NO_GENERATION 1
|
||||
#define MA_DEBUG_OUTPUT 1
|
||||
#define MA_DISABLE_PULSEAUDIO
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "miniaudio.h"
|
||||
#include "defines.h" // MAX_LAYERS
|
||||
#include <QDebug> // prints messages
|
||||
#define MA_DEBUG_OUTPUT
|
||||
|
||||
class MiniAudioEngine
|
||||
{
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
#include "slidergroup.h"
|
||||
#include <QCursor>
|
||||
#include <QStyle>
|
||||
|
||||
DoubleSpinBoxClickable::DoubleSpinBoxClickable(QWidget *parent)
|
||||
: QDoubleSpinBox{parent} {}
|
||||
|
||||
DoubleSpinBoxClickable::~DoubleSpinBoxClickable() {}
|
||||
|
||||
SliderClickDisable::SliderClickDisable(QWidget *parent)
|
||||
: QSlider{parent} {}
|
||||
|
||||
SliderClickDisable::~SliderClickDisable() {}
|
||||
|
||||
SliderGroup::SliderGroup(QString name,
|
||||
int min,
|
||||
int max,
|
||||
|
@ -7,42 +19,46 @@ SliderGroup::SliderGroup(QString name,
|
|||
QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout;
|
||||
QBoxLayout *layout;
|
||||
if (decimals) {
|
||||
layout = new QVBoxLayout;
|
||||
slider.setOrientation(Qt::Vertical);
|
||||
}
|
||||
else {
|
||||
layout = new QHBoxLayout;
|
||||
slider.setOrientation(Qt::Horizontal);
|
||||
slider.setMinimumHeight(10);
|
||||
}
|
||||
layout->setAlignment(Qt::AlignHCenter);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
//this->setMaximumWidth(40);
|
||||
slider = new QSlider(Qt::Orientation::Vertical);
|
||||
slider->setFocusPolicy(Qt::StrongFocus);
|
||||
slider->setTickPosition(QSlider::TicksBothSides);
|
||||
slider->setTickInterval((max - min) / 11);
|
||||
slider->setMinimumHeight(0);
|
||||
slider->setSingleStep(1);
|
||||
slider->setRange(min, max);
|
||||
slider->setValue(0);
|
||||
slider->setMinimumWidth(50);
|
||||
slider->setToolTip(name);
|
||||
slider->setStyleSheet("QSlider {"
|
||||
slider.setFocusPolicy(Qt::StrongFocus);
|
||||
slider.setTickPosition(QSlider::TicksBothSides);
|
||||
slider.setTickInterval((max - min) / 11);
|
||||
slider.setMinimumHeight(0);
|
||||
slider.setSingleStep(1);
|
||||
slider.setRange(min, max);
|
||||
slider.setValue(0);
|
||||
slider.setMinimumWidth(50);
|
||||
slider.setToolTip(name);
|
||||
slider.setStyleSheet("QSlider {"
|
||||
"border: 1px solid #5a4855;"
|
||||
"margin: 0px;"
|
||||
"height: 200px;"
|
||||
"width: 50px;}"
|
||||
"margin: 0px;}"
|
||||
);
|
||||
slider->setContentsMargins(0, 0, 0, 0);
|
||||
valueBox = new QDoubleSpinBox();
|
||||
valueBox->setFocusPolicy(Qt::NoFocus);
|
||||
valueBox->setButtonSymbols(QAbstractSpinBox::NoButtons);
|
||||
valueBox->setMinimumWidth(50);
|
||||
valueBox->setRange(min, max);
|
||||
valueBox->setValue(0);
|
||||
valueBox->setDecimals(decimals);
|
||||
valueBox->setObjectName(name);
|
||||
valueBox->setToolTip(name);
|
||||
valueBox->setAlignment(Qt::AlignHCenter);
|
||||
valueBox->setContentsMargins(0, 0, 0, 0);
|
||||
connect(slider, SIGNAL(valueChanged(int)), this, SLOT(sliderValueChanged(int)));
|
||||
//connect(slider, SIGNAL(mousePressEvent(QMouseEvent)), this, SLOT(mousePressEvent(QMouseEvent *)));
|
||||
layout->addWidget(slider);
|
||||
layout->addWidget(valueBox);
|
||||
slider.setContentsMargins(0, 0, 0, 0);
|
||||
valueBox.setFocusPolicy(Qt::NoFocus);
|
||||
valueBox.setButtonSymbols(QAbstractSpinBox::NoButtons);
|
||||
valueBox.setMinimumWidth(50);
|
||||
valueBox.setRange(min, max);
|
||||
valueBox.setValue(0);
|
||||
valueBox.setDecimals(decimals);
|
||||
valueBox.setObjectName(name);
|
||||
valueBox.setToolTip(name);
|
||||
valueBox.setAlignment(Qt::AlignHCenter);
|
||||
valueBox.setContentsMargins(0, 0, 0, 0);
|
||||
connect(&slider, SIGNAL(valueChanged(int)), this, SLOT(sliderValueChanged(int)));
|
||||
connect(&valueBox, SIGNAL(enableSlider()), this, SLOT(enableSlider()));
|
||||
layout->addWidget(&slider);
|
||||
layout->addWidget(&valueBox);
|
||||
this->setStyleSheet("border: 1px solid #5a4855;"
|
||||
"width: 50px;"
|
||||
"margin: 0px;"
|
||||
|
@ -55,27 +71,19 @@ SliderGroup::SliderGroup(QString name,
|
|||
|
||||
void SliderGroup::sliderValueChanged(int value)
|
||||
{
|
||||
valueBox->blockSignals(true);
|
||||
valueBox->setValue(value);
|
||||
valueBox->blockSignals(false);
|
||||
valueBox.blockSignals(true);
|
||||
valueBox.setValue(value);
|
||||
valueBox.blockSignals(false);
|
||||
emit valueChanged(value);
|
||||
};
|
||||
|
||||
void SliderGroup::setValue(float value)
|
||||
{
|
||||
slider->blockSignals(true);
|
||||
valueBox->blockSignals(true);
|
||||
if (int(value) != slider->value())
|
||||
slider->setValue(value);
|
||||
valueBox->setValue(value);
|
||||
slider->blockSignals(false);
|
||||
valueBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void SliderGroup::mousePressEvent(QMouseEvent* event) {
|
||||
Q_UNUSED(event);
|
||||
if (slider->isEnabled())
|
||||
slider->setDisabled(true);
|
||||
else
|
||||
slider->setDisabled(false);
|
||||
slider.blockSignals(true);
|
||||
valueBox.blockSignals(true);
|
||||
if (int(value) != slider.value())
|
||||
slider.setValue(value);
|
||||
valueBox.setValue(value);
|
||||
slider.blockSignals(false);
|
||||
valueBox.blockSignals(false);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,78 @@
|
|||
#include <QDoubleSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QSlider>
|
||||
#include <QDebug>
|
||||
#include <QEvent>
|
||||
#include <QMouseEvent>
|
||||
/*
|
||||
//slider->installEventFilter(new QSliderAnalyser);
|
||||
class QSliderAnalyser
|
||||
: public QObject
|
||||
{
|
||||
public:
|
||||
QSliderAnalyser()
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~QSliderAnalyser()
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* object, QEvent* event) override
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
qDebug() << event->type() << object->objectName();
|
||||
}
|
||||
return QObject::eventFilter(object, event);
|
||||
}
|
||||
};*/
|
||||
|
||||
class DoubleSpinBoxClickable: public QDoubleSpinBox
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DoubleSpinBoxClickable(QWidget *parent = 0);
|
||||
~DoubleSpinBoxClickable();
|
||||
|
||||
signals:
|
||||
void enableSlider();
|
||||
|
||||
protected:
|
||||
void mousePressEvent ( QMouseEvent * event )
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
qDebug() << "enabling slider";
|
||||
emit(enableSlider());
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
};
|
||||
|
||||
class SliderClickDisable
|
||||
: public QSlider
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SliderClickDisable(QWidget *parent = Q_NULLPTR);
|
||||
~SliderClickDisable();
|
||||
|
||||
protected:
|
||||
void mousePressEvent ( QMouseEvent * event )
|
||||
{
|
||||
if (event->button() == Qt::RightButton)
|
||||
{
|
||||
if (this->isEnabled()) {
|
||||
qDebug() << "disabling slider";
|
||||
this->setDisabled(true);
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
QSlider::mousePressEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
class SliderGroup : public QWidget
|
||||
{
|
||||
|
@ -25,10 +97,12 @@ public slots:
|
|||
void sliderValueChanged(int value);
|
||||
|
||||
private:
|
||||
QSlider *slider;
|
||||
QDoubleSpinBox *valueBox;
|
||||
SliderClickDisable slider;
|
||||
DoubleSpinBoxClickable valueBox;
|
||||
|
||||
private slots:
|
||||
void enableSlider() { slider.setEnabled(true); }
|
||||
|
||||
void mousePressEvent(QMouseEvent* event);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue