bug fix in socket buffer, fix in channel, ASIO buffer size setting should work now, some other small fixes
This commit is contained in:
parent
56c91ce201
commit
ee28e3bc8e
8 changed files with 132 additions and 96 deletions
|
@ -380,10 +380,13 @@ void CNetBuf::Clear ( const EClearType eClearType )
|
||||||
|
|
||||||
void CNetBuf::FadeInAudioDataBlock ( CVector<double>& vecdData )
|
void CNetBuf::FadeInAudioDataBlock ( CVector<double>& vecdData )
|
||||||
{
|
{
|
||||||
|
// correct fading length if necessary
|
||||||
|
const int iCurFadingLen = min ( vecdData.Size(), iNumSamFading );
|
||||||
|
|
||||||
// apply linear fading
|
// apply linear fading
|
||||||
for ( int i = 0; i < iNumSamFading; i++ )
|
for ( int i = 0; i < iCurFadingLen; i++ )
|
||||||
{
|
{
|
||||||
vecdData[i] *= ( (double) i / iNumSamFading );
|
vecdData[i] *= ( (double) i / iCurFadingLen );
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset flag
|
// reset flag
|
||||||
|
@ -394,14 +397,17 @@ void CNetBuf::FadeOutExtrapolateAudioDataBlock ( CVector<double>& vecdData,
|
||||||
const double dExPDiff,
|
const double dExPDiff,
|
||||||
const double dExPLastV )
|
const double dExPLastV )
|
||||||
{
|
{
|
||||||
|
// correct fading length if necessary
|
||||||
|
const int iCurFadingLenExtra = min ( vecdData.Size(), iNumSamFadingExtra );
|
||||||
|
|
||||||
// apply linear extrapolation and linear fading
|
// apply linear extrapolation and linear fading
|
||||||
for ( int i = 0; i < iNumSamFadingExtra; i++ )
|
for ( int i = 0; i < iCurFadingLenExtra; i++ )
|
||||||
{
|
{
|
||||||
// calculate extrapolated value
|
// calculate extrapolated value
|
||||||
vecdData[i] = ( ( i + 1 ) * dExPDiff + dExPLastV );
|
vecdData[i] = ( ( i + 1 ) * dExPDiff + dExPLastV );
|
||||||
|
|
||||||
// linear fading
|
// linear fading
|
||||||
vecdData[i] *= ( (double) ( iNumSamFadingExtra - i ) / iNumSamFadingExtra );
|
vecdData[i] *= ( (double) ( iCurFadingLenExtra - i ) / iCurFadingLenExtra );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -622,11 +622,19 @@ CChannel::CChannel ( const bool bNIsServer ) : bIsServer ( bNIsServer ),
|
||||||
MIN_SERVER_BLOCK_SIZE_SAMPLES * DEF_NET_BLOCK_SIZE_FACTOR,
|
MIN_SERVER_BLOCK_SIZE_SAMPLES * DEF_NET_BLOCK_SIZE_FACTOR,
|
||||||
CT_MSADPCM );
|
CT_MSADPCM );
|
||||||
|
|
||||||
SetNetwBufSizeFactOut ( DEF_NET_BLOCK_SIZE_FACTOR );
|
if ( bIsServer )
|
||||||
|
{
|
||||||
|
SetNetwBufSizeFactOut ( DEF_NET_BLOCK_SIZE_FACTOR );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetNetwBufSizeOut ( MIN_SERVER_BLOCK_SIZE_SAMPLES );
|
||||||
|
}
|
||||||
|
|
||||||
// set initial audio compression format for output
|
// set initial audio compression format for output
|
||||||
SetAudioCompressionOut ( CT_MSADPCM );
|
SetAudioCompressionOut ( CT_MSADPCM );
|
||||||
|
|
||||||
|
|
||||||
// connections -------------------------------------------------------------
|
// connections -------------------------------------------------------------
|
||||||
QObject::connect ( &Protocol,
|
QObject::connect ( &Protocol,
|
||||||
SIGNAL ( MessReadyForSending ( CVector<uint8_t> ) ),
|
SIGNAL ( MessReadyForSending ( CVector<uint8_t> ) ),
|
||||||
|
@ -730,11 +738,16 @@ void CChannel::SetNetwBufSizeOut ( const int iNewAudioBlockSizeOut )
|
||||||
// this function is intended for the client (not the server)
|
// this function is intended for the client (not the server)
|
||||||
QMutexLocker locker ( &Mutex );
|
QMutexLocker locker ( &Mutex );
|
||||||
|
|
||||||
// store new value
|
// direct setting of audio buffer (without buffer size factor) is
|
||||||
iCurAudioBlockSizeOut = iNewAudioBlockSizeOut;
|
// right now only intendet for the client, not the server
|
||||||
|
if ( !bIsServer )
|
||||||
|
{
|
||||||
|
// store new value
|
||||||
|
iCurAudioBlockSizeOut = iNewAudioBlockSizeOut;
|
||||||
|
|
||||||
iAudComprSizeOut =
|
iAudComprSizeOut =
|
||||||
AudioCompressionOut.Init ( iNewAudioBlockSizeOut, eAudComprTypeOut );
|
AudioCompressionOut.Init ( iNewAudioBlockSizeOut, eAudComprTypeOut );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CChannel::SetNetwBufSizeFactOut ( const int iNewNetwBlSiFactOut )
|
void CChannel::SetNetwBufSizeFactOut ( const int iNewNetwBlSiFactOut )
|
||||||
|
|
|
@ -150,29 +150,40 @@ bool CClient::SetServerAddr ( QString strNAddr )
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void CClient::SetSndCrdPreferredMonoBlSizeIndex ( const int iNewIdx )
|
void CClient::SetSndCrdPreferredMonoBlSizeIndex ( const int iNewIdx )
|
||||||
{
|
{
|
||||||
// right now we simply set the internal value
|
// right now we simply set the internal value
|
||||||
if ( ( iNewIdx >= 0 ) && ( CSndCrdBufferSizes::GetNumOfBufferSizes() ) )
|
if ( ( iNewIdx >= 0 ) && ( CSndCrdBufferSizes::GetNumOfBufferSizes() ) )
|
||||||
{
|
{
|
||||||
iSndCrdPreferredMonoBlSizeIndex = iNewIdx;
|
iSndCrdPreferredMonoBlSizeIndex = iNewIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// init with new parameter, if client was running then first
|
||||||
|
// stop it and restart again after new initialization
|
||||||
|
const bool bWasRunning = Sound.IsRunning();
|
||||||
|
if ( bWasRunning )
|
||||||
|
{
|
||||||
|
Sound.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// init with new block size index parameter
|
||||||
|
Init ( iSndCrdPreferredMonoBlSizeIndex );
|
||||||
|
|
||||||
|
if ( bWasRunning )
|
||||||
|
{
|
||||||
|
Sound.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell the server that audio coding has changed (it
|
||||||
|
// is important to call this function AFTER we have applied
|
||||||
|
// the new setting to the channel!)
|
||||||
|
Channel.CreateNetTranspPropsMessFromCurrentSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO take action on new parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void CClient::Start()
|
void CClient::Start()
|
||||||
{
|
{
|
||||||
// init object
|
// init object
|
||||||
|
Init ( iSndCrdPreferredMonoBlSizeIndex );
|
||||||
// TEST
|
|
||||||
Init ( 192 );
|
|
||||||
|
|
||||||
// enable channel
|
// enable channel
|
||||||
Channel.SetEnable ( true );
|
Channel.SetEnable ( true );
|
||||||
|
@ -203,8 +214,12 @@ void CClient::AudioCallback ( CVector<short>& psData, void* arg )
|
||||||
pMyClientObj->ProcessAudioData ( psData );
|
pMyClientObj->ProcessAudioData ( psData );
|
||||||
}
|
}
|
||||||
|
|
||||||
void CClient::Init ( const int iPrefMonoBlockSizeSamAtSndCrdSamRate )
|
void CClient::Init ( const int iPrefMonoBlockSizeSamIndexAtSndCrdSamRate )
|
||||||
{
|
{
|
||||||
|
// translate block size index in actual block size
|
||||||
|
const int iPrefMonoBlockSizeSamAtSndCrdSamRate = CSndCrdBufferSizes::
|
||||||
|
GetBufferSizeFromIndex ( iPrefMonoBlockSizeSamIndexAtSndCrdSamRate );
|
||||||
|
|
||||||
// get actual sound card buffer size using preferred size
|
// get actual sound card buffer size using preferred size
|
||||||
iSndCrdMonoBlockSizeSam = Sound.Init ( iPrefMonoBlockSizeSamAtSndCrdSamRate );
|
iSndCrdMonoBlockSizeSam = Sound.Init ( iPrefMonoBlockSizeSamAtSndCrdSamRate );
|
||||||
iSndCrdStereoBlockSizeSam = 2 * iSndCrdMonoBlockSizeSam;
|
iSndCrdStereoBlockSizeSam = 2 * iSndCrdMonoBlockSizeSam;
|
||||||
|
|
|
@ -156,7 +156,7 @@ protected:
|
||||||
// callback function must be static, otherwise it does not work
|
// callback function must be static, otherwise it does not work
|
||||||
static void AudioCallback ( CVector<short>& psData, void* arg );
|
static void AudioCallback ( CVector<short>& psData, void* arg );
|
||||||
|
|
||||||
void Init ( const int iPrefMonoBlockSizeSamAtSndCrdSamRate );
|
void Init ( const int iPrefMonoBlockSizeSamIndexAtSndCrdSamRate );
|
||||||
void ProcessAudioData ( CVector<short>& vecsStereoSndCrd );
|
void ProcessAudioData ( CVector<short>& vecsStereoSndCrd );
|
||||||
void UpdateTimeResponseMeasurement();
|
void UpdateTimeResponseMeasurement();
|
||||||
void UpdateSocketBufferSize();
|
void UpdateSocketBufferSize();
|
||||||
|
|
|
@ -251,7 +251,7 @@
|
||||||
<property name="minimumSize" >
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>110</width>
|
<width>110</width>
|
||||||
<height>0</height>
|
<height>20</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShape" >
|
<property name="frameShape" >
|
||||||
|
@ -281,7 +281,7 @@
|
||||||
<property name="minimumSize" >
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>110</width>
|
<width>110</width>
|
||||||
<height>0</height>
|
<height>20</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShape" >
|
<property name="frameShape" >
|
||||||
|
@ -386,7 +386,7 @@
|
||||||
<property name="minimumSize" >
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>50</width>
|
<width>50</width>
|
||||||
<height>0</height>
|
<height>20</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShape" >
|
<property name="frameShape" >
|
||||||
|
@ -427,7 +427,7 @@
|
||||||
<property name="minimumSize" >
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>50</width>
|
<width>50</width>
|
||||||
<height>0</height>
|
<height>20</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="font" >
|
<property name="font" >
|
||||||
|
@ -484,6 +484,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="TextUpstreamValue" >
|
<widget class="QLabel" name="TextUpstreamValue" >
|
||||||
|
<property name="minimumSize" >
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="font" >
|
<property name="font" >
|
||||||
<font>
|
<font>
|
||||||
<weight>75</weight>
|
<weight>75</weight>
|
||||||
|
|
|
@ -61,11 +61,6 @@ void CSoundBase::Stop()
|
||||||
{
|
{
|
||||||
wait ( 5000 );
|
wait ( 5000 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO stop audio interface (previously done in Close function, we
|
|
||||||
// better should implement a stop function in derived sound classes
|
|
||||||
Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSoundBase::run()
|
void CSoundBase::run()
|
||||||
|
|
|
@ -43,9 +43,8 @@ ASIOBufferInfo bufferInfos[2 * NUM_IN_OUT_CHANNELS]; // for input and output b
|
||||||
ASIOChannelInfo channelInfos[2 * NUM_IN_OUT_CHANNELS];
|
ASIOChannelInfo channelInfos[2 * NUM_IN_OUT_CHANNELS];
|
||||||
bool bASIOPostOutput;
|
bool bASIOPostOutput;
|
||||||
ASIOCallbacks asioCallbacks;
|
ASIOCallbacks asioCallbacks;
|
||||||
int iBufferSizeMono;
|
|
||||||
int iBufferSizeStereo;
|
|
||||||
int iASIOBufferSizeMono;
|
int iASIOBufferSizeMono;
|
||||||
|
int iASIOBufferSizeStereo;
|
||||||
|
|
||||||
CVector<short> vecsTmpAudioSndCrdStereo;
|
CVector<short> vecsTmpAudioSndCrdStereo;
|
||||||
|
|
||||||
|
@ -88,12 +87,12 @@ void CSound::SetDev ( const int iNewDev )
|
||||||
// loading and initializing the new driver failed, go back to original
|
// loading and initializing the new driver failed, go back to original
|
||||||
// driver and display error message
|
// driver and display error message
|
||||||
LoadAndInitializeDriver ( lCurDev );
|
LoadAndInitializeDriver ( lCurDev );
|
||||||
Init ( iBufferSizeStereo );
|
Init ( iASIOBufferSizeStereo );
|
||||||
|
|
||||||
throw CGenErr ( strErrorMessage.c_str() );
|
throw CGenErr ( strErrorMessage.c_str() );
|
||||||
}
|
}
|
||||||
|
|
||||||
Init ( iBufferSizeStereo );
|
Init ( iASIOBufferSizeStereo );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -319,92 +318,75 @@ int CSound::GetActualBufferSize ( const int iDesiredBufferSizeMono )
|
||||||
|
|
||||||
int CSound::Init ( const int iNewPrefMonoBufferSize )
|
int CSound::Init ( const int iNewPrefMonoBufferSize )
|
||||||
{
|
{
|
||||||
// first, stop audio and dispose ASIO buffers
|
|
||||||
ASIOStop();
|
|
||||||
|
|
||||||
ASIOMutex.lock(); // get mutex lock
|
ASIOMutex.lock(); // get mutex lock
|
||||||
{
|
{
|
||||||
|
// get the actual sound card buffer size which is supported
|
||||||
|
// by the audio hardware
|
||||||
|
iASIOBufferSizeMono =
|
||||||
|
GetActualBufferSize ( iNewPrefMonoBufferSize );
|
||||||
|
|
||||||
// init base clasee
|
// init base clasee
|
||||||
CSoundBase::Init ( iNewPrefMonoBufferSize );
|
CSoundBase::Init ( iASIOBufferSizeMono );
|
||||||
|
|
||||||
// set internal buffer size value and calculate stereo buffer size
|
// set internal buffer size value and calculate stereo buffer size
|
||||||
iBufferSizeMono = iNewPrefMonoBufferSize;
|
iASIOBufferSizeStereo = 2 * iASIOBufferSizeMono;
|
||||||
iBufferSizeStereo = 2 * iBufferSizeMono;
|
|
||||||
|
|
||||||
|
|
||||||
// TODO possible BUG!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
// iBufferSizeMono must not be the same as iASIOBufferSizeMono
|
|
||||||
|
|
||||||
|
// set the sample rate
|
||||||
|
ASIOSetSampleRate ( SND_CRD_SAMPLE_RATE );
|
||||||
|
|
||||||
// create memory for intermediate audio buffer
|
// create memory for intermediate audio buffer
|
||||||
vecsTmpAudioSndCrdStereo.Init ( iBufferSizeStereo );
|
vecsTmpAudioSndCrdStereo.Init ( iASIOBufferSizeStereo );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int i;
|
|
||||||
|
|
||||||
// set the sample rate and check if sample rate is supported
|
|
||||||
ASIOSetSampleRate ( SND_CRD_SAMPLE_RATE );
|
|
||||||
|
|
||||||
|
|
||||||
// TEST
|
|
||||||
iASIOBufferSizeMono = GetActualBufferSize ( iBufferSizeMono );
|
|
||||||
|
|
||||||
|
|
||||||
for ( i = 0; i < NUM_IN_OUT_CHANNELS; i++ )
|
|
||||||
{
|
|
||||||
// prepare input channels
|
|
||||||
bufferInfos[i].isInput = ASIOTrue;
|
|
||||||
bufferInfos[i].channelNum = i;
|
|
||||||
bufferInfos[i].buffers[0] = 0;
|
|
||||||
bufferInfos[i].buffers[1] = 0;
|
|
||||||
|
|
||||||
// prepare output channels
|
|
||||||
bufferInfos[NUM_IN_OUT_CHANNELS + i].isInput = ASIOFalse;
|
|
||||||
bufferInfos[NUM_IN_OUT_CHANNELS + i].channelNum = i;
|
|
||||||
bufferInfos[NUM_IN_OUT_CHANNELS + i].buffers[0] = 0;
|
|
||||||
bufferInfos[NUM_IN_OUT_CHANNELS + i].buffers[1] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and activate ASIO buffers (buffer size in samples)
|
|
||||||
ASIOCreateBuffers ( bufferInfos, 2 /* in/out */ * NUM_IN_OUT_CHANNELS /* stereo */,
|
|
||||||
iASIOBufferSizeMono, &asioCallbacks );
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// check wether the driver requires the ASIOOutputReady() optimization
|
|
||||||
// (can be used by the driver to reduce output latency by one block)
|
|
||||||
bASIOPostOutput = ( ASIOOutputReady() == ASE_OK );
|
|
||||||
|
|
||||||
|
|
||||||
|
// create and activate ASIO buffers (buffer size in samples)
|
||||||
|
ASIOCreateBuffers ( bufferInfos,
|
||||||
|
2 /* in/out */ * NUM_IN_OUT_CHANNELS /* stereo */,
|
||||||
|
iASIOBufferSizeMono, &asioCallbacks );
|
||||||
|
|
||||||
|
// check wether the driver requires the ASIOOutputReady() optimization
|
||||||
|
// (can be used by the driver to reduce output latency by one block)
|
||||||
|
bASIOPostOutput = ( ASIOOutputReady() == ASE_OK );
|
||||||
}
|
}
|
||||||
ASIOMutex.unlock();
|
ASIOMutex.unlock();
|
||||||
|
|
||||||
// initialization is done, (re)start audio
|
return iASIOBufferSizeMono;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSound::Start()
|
||||||
|
{
|
||||||
|
// start audio
|
||||||
ASIOStart();
|
ASIOStart();
|
||||||
|
|
||||||
// TEST
|
// call base class
|
||||||
return iNewPrefMonoBufferSize;
|
CSoundBase::Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSound::Stop()
|
||||||
|
{
|
||||||
|
// stop audio
|
||||||
|
ASIOStop();
|
||||||
|
|
||||||
|
// call base class
|
||||||
|
CSoundBase::Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSound::Close()
|
void CSound::Close()
|
||||||
{
|
{
|
||||||
// stop driver
|
|
||||||
ASIOStop();
|
// TODO
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CSound::CSound ( void (*fpNewCallback) ( CVector<short>& psData, void* arg ), void* arg ) :
|
CSound::CSound ( void (*fpNewCallback) ( CVector<short>& psData, void* arg ), void* arg ) :
|
||||||
CSoundBase ( true, fpNewCallback, arg )
|
CSoundBase ( true, fpNewCallback, arg )
|
||||||
{
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
pSound = this;
|
pSound = this;
|
||||||
|
|
||||||
|
|
||||||
// get available ASIO driver names in system
|
// get available ASIO driver names in system
|
||||||
for ( int i = 0; i < MAX_NUMBER_SOUND_CARDS; i++ )
|
for ( i = 0; i < MAX_NUMBER_SOUND_CARDS; i++ )
|
||||||
{
|
{
|
||||||
cDriverNames[i] = new char[32];
|
cDriverNames[i] = new char[32];
|
||||||
}
|
}
|
||||||
|
@ -422,6 +404,23 @@ pSound = this;
|
||||||
// init device index with illegal value to show that driver is not initialized
|
// init device index with illegal value to show that driver is not initialized
|
||||||
lCurDev = -1;
|
lCurDev = -1;
|
||||||
|
|
||||||
|
// init buffer infos, we always want to have two input and
|
||||||
|
// two output channels
|
||||||
|
for ( i = 0; i < NUM_IN_OUT_CHANNELS; i++ )
|
||||||
|
{
|
||||||
|
// prepare input channels
|
||||||
|
bufferInfos[i].isInput = ASIOTrue;
|
||||||
|
bufferInfos[i].channelNum = i;
|
||||||
|
bufferInfos[i].buffers[0] = 0;
|
||||||
|
bufferInfos[i].buffers[1] = 0;
|
||||||
|
|
||||||
|
// prepare output channels
|
||||||
|
bufferInfos[NUM_IN_OUT_CHANNELS + i].isInput = ASIOFalse;
|
||||||
|
bufferInfos[NUM_IN_OUT_CHANNELS + i].channelNum = i;
|
||||||
|
bufferInfos[NUM_IN_OUT_CHANNELS + i].buffers[0] = 0;
|
||||||
|
bufferInfos[NUM_IN_OUT_CHANNELS + i].buffers[1] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// set up the asioCallback structure
|
// set up the asioCallback structure
|
||||||
asioCallbacks.bufferSwitch = &bufferSwitch;
|
asioCallbacks.bufferSwitch = &bufferSwitch;
|
||||||
asioCallbacks.sampleRateDidChange = &sampleRateChanged;
|
asioCallbacks.sampleRateDidChange = &sampleRateChanged;
|
||||||
|
|
|
@ -60,6 +60,8 @@ public:
|
||||||
virtual ~CSound();
|
virtual ~CSound();
|
||||||
|
|
||||||
virtual int Init ( const int iNewPrefMonoBufferSize );
|
virtual int Init ( const int iNewPrefMonoBufferSize );
|
||||||
|
virtual void Start();
|
||||||
|
virtual void Stop();
|
||||||
virtual void Close();
|
virtual void Close();
|
||||||
|
|
||||||
virtual void OpenDriverSetup() { ASIOControlPanel(); }
|
virtual void OpenDriverSetup() { ASIOControlPanel(); }
|
||||||
|
|
Loading…
Reference in a new issue