Audio: Make Music, Sound, SoundEmitter movable
This commit is contained in:
parent
31fc8c9dad
commit
3c4c0fab66
|
|
@ -81,6 +81,7 @@ Nazara Engine:
|
||||||
- Fixed IPv6 addresses not being correctly encoded/decoded from the socket API.
|
- Fixed IPv6 addresses not being correctly encoded/decoded from the socket API.
|
||||||
- Fix copy and move semantic on HandledObject and ObjectHandle
|
- Fix copy and move semantic on HandledObject and ObjectHandle
|
||||||
- Add support for emissive and normal maps in .mtl loader using custom keywords ([map_]emissive and [map_]normal)
|
- Add support for emissive and normal maps in .mtl loader using custom keywords ([map_]emissive and [map_]normal)
|
||||||
|
- Music, Sound and SoundEmitter are now movable
|
||||||
|
|
||||||
Nazara Development Kit:
|
Nazara Development Kit:
|
||||||
- Added ImageWidget (#139)
|
- Added ImageWidget (#139)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ namespace Nz
|
||||||
public:
|
public:
|
||||||
Music() = default;
|
Music() = default;
|
||||||
Music(const Music&) = delete;
|
Music(const Music&) = delete;
|
||||||
Music(Music&&) = delete;
|
Music(Music&&) noexcept = default;
|
||||||
~Music();
|
~Music();
|
||||||
|
|
||||||
bool Create(SoundStream* soundStream);
|
bool Create(SoundStream* soundStream);
|
||||||
|
|
@ -67,10 +67,10 @@ namespace Nz
|
||||||
void Stop() override;
|
void Stop() override;
|
||||||
|
|
||||||
Music& operator=(const Music&) = delete;
|
Music& operator=(const Music&) = delete;
|
||||||
Music& operator=(Music&&) = delete;
|
Music& operator=(Music&&) noexcept = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MovablePtr<MusicImpl> m_impl = nullptr;
|
MovablePtr<MusicImpl> m_impl;
|
||||||
|
|
||||||
bool FillAndQueueBuffer(unsigned int buffer);
|
bool FillAndQueueBuffer(unsigned int buffer);
|
||||||
void MusicThread();
|
void MusicThread();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Nz
|
||||||
Sound() = default;
|
Sound() = default;
|
||||||
Sound(const SoundBuffer* soundBuffer);
|
Sound(const SoundBuffer* soundBuffer);
|
||||||
Sound(const Sound& sound);
|
Sound(const Sound& sound);
|
||||||
Sound(Sound&&) = default;
|
Sound(Sound&&) noexcept = default;
|
||||||
~Sound();
|
~Sound();
|
||||||
|
|
||||||
void EnableLooping(bool loop) override;
|
void EnableLooping(bool loop) override;
|
||||||
|
|
@ -47,7 +47,7 @@ namespace Nz
|
||||||
void Stop() override;
|
void Stop() override;
|
||||||
|
|
||||||
Sound& operator=(const Sound&) = delete; ///TODO?
|
Sound& operator=(const Sound&) = delete; ///TODO?
|
||||||
Sound& operator=(Sound&&) = default;
|
Sound& operator=(Sound&&) noexcept = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SoundBufferConstRef m_buffer;
|
SoundBufferConstRef m_buffer;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <Nazara/Audio/Config.hpp>
|
#include <Nazara/Audio/Config.hpp>
|
||||||
#include <Nazara/Audio/Enums.hpp>
|
#include <Nazara/Audio/Enums.hpp>
|
||||||
#include <Nazara/Math/Vector3.hpp>
|
#include <Nazara/Math/Vector3.hpp>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
///TODO: Inherit SoundEmitter from Node
|
///TODO: Inherit SoundEmitter from Node
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ namespace Nz
|
||||||
class NAZARA_AUDIO_API SoundEmitter
|
class NAZARA_AUDIO_API SoundEmitter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
SoundEmitter(SoundEmitter&& emitter) noexcept;
|
||||||
virtual ~SoundEmitter();
|
virtual ~SoundEmitter();
|
||||||
|
|
||||||
virtual void EnableLooping(bool loop) = 0;
|
virtual void EnableLooping(bool loop) = 0;
|
||||||
|
|
@ -51,16 +53,17 @@ namespace Nz
|
||||||
|
|
||||||
virtual void Stop() = 0;
|
virtual void Stop() = 0;
|
||||||
|
|
||||||
SoundEmitter& operator=(const SoundEmitter&) = delete; ///TODO
|
SoundEmitter& operator=(const SoundEmitter&) = delete;
|
||||||
SoundEmitter& operator=(SoundEmitter&&) = delete;
|
SoundEmitter& operator=(SoundEmitter&&) noexcept;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
SoundEmitter();
|
SoundEmitter();
|
||||||
SoundEmitter(const SoundEmitter& emitter);
|
SoundEmitter(const SoundEmitter& emitter);
|
||||||
SoundEmitter(SoundEmitter&&) = delete;
|
|
||||||
|
|
||||||
SoundStatus GetInternalStatus() const;
|
SoundStatus GetInternalStatus() const;
|
||||||
|
|
||||||
|
static constexpr unsigned int InvalidSource = std::numeric_limits<unsigned int>::max();
|
||||||
|
|
||||||
unsigned int m_source;
|
unsigned int m_source;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,8 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Music::Pause()
|
void Music::Pause()
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcePause(m_source);
|
alSourcePause(m_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Sound::EnableLooping(bool loop)
|
void Sound::EnableLooping(bool loop)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcei(m_source, AL_LOOPING, loop);
|
alSourcei(m_source, AL_LOOPING, loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +89,8 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
UInt32 Sound::GetPlayingOffset() const
|
UInt32 Sound::GetPlayingOffset() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALint samples = 0;
|
ALint samples = 0;
|
||||||
alGetSourcei(m_source, AL_SAMPLE_OFFSET, &samples);
|
alGetSourcei(m_source, AL_SAMPLE_OFFSET, &samples);
|
||||||
|
|
||||||
|
|
@ -108,6 +112,8 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
bool Sound::IsLooping() const
|
bool Sound::IsLooping() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALint loop;
|
ALint loop;
|
||||||
alGetSourcei(m_source, AL_LOOPING, &loop);
|
alGetSourcei(m_source, AL_LOOPING, &loop);
|
||||||
|
|
||||||
|
|
@ -205,6 +211,8 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Sound::Pause()
|
void Sound::Pause()
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcePause(m_source);
|
alSourcePause(m_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,6 +223,7 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Sound::Play()
|
void Sound::Play()
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
NazaraAssert(IsPlayable(), "Music is not playable");
|
NazaraAssert(IsPlayable(), "Music is not playable");
|
||||||
|
|
||||||
alSourcePlay(m_source);
|
alSourcePlay(m_source);
|
||||||
|
|
@ -229,6 +238,7 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Sound::SetBuffer(const SoundBuffer* buffer)
|
void Sound::SetBuffer(const SoundBuffer* buffer)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
NazaraAssert(!buffer || buffer->IsValid(), "Invalid sound buffer");
|
NazaraAssert(!buffer || buffer->IsValid(), "Invalid sound buffer");
|
||||||
|
|
||||||
if (m_buffer == buffer)
|
if (m_buffer == buffer)
|
||||||
|
|
@ -251,14 +261,19 @@ namespace Nz
|
||||||
*/
|
*/
|
||||||
void Sound::SetPlayingOffset(UInt32 offset)
|
void Sound::SetPlayingOffset(UInt32 offset)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcei(m_source, AL_SAMPLE_OFFSET, static_cast<ALint>(offset/1000.f * m_buffer->GetSampleRate()));
|
alSourcei(m_source, AL_SAMPLE_OFFSET, static_cast<ALint>(offset/1000.f * m_buffer->GetSampleRate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Stops the sound
|
* \brief Stops the sound
|
||||||
|
*
|
||||||
|
* \remark This is one of the only function that can be called on a moved sound (and does nothing)
|
||||||
*/
|
*/
|
||||||
void Sound::Stop()
|
void Sound::Stop()
|
||||||
{
|
{
|
||||||
alSourceStop(m_source);
|
if (m_source != InvalidSource)
|
||||||
|
alSourceStop(m_source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ namespace Nz
|
||||||
/*!
|
/*!
|
||||||
* \brief Constructs a SoundEmitter object
|
* \brief Constructs a SoundEmitter object
|
||||||
*/
|
*/
|
||||||
|
|
||||||
SoundEmitter::SoundEmitter()
|
SoundEmitter::SoundEmitter()
|
||||||
{
|
{
|
||||||
alGenSources(1, &m_source);
|
alGenSources(1, &m_source);
|
||||||
|
|
@ -39,22 +38,40 @@ namespace Nz
|
||||||
|
|
||||||
SoundEmitter::SoundEmitter(const SoundEmitter& emitter)
|
SoundEmitter::SoundEmitter(const SoundEmitter& emitter)
|
||||||
{
|
{
|
||||||
alGenSources(1, &m_source);
|
if (emitter.m_source != InvalidSource)
|
||||||
|
{
|
||||||
|
alGenSources(1, &m_source);
|
||||||
|
|
||||||
SetAttenuation(emitter.GetAttenuation());
|
SetAttenuation(emitter.GetAttenuation());
|
||||||
SetMinDistance(emitter.GetMinDistance());
|
SetMinDistance(emitter.GetMinDistance());
|
||||||
SetPitch(emitter.GetPitch());
|
SetPitch(emitter.GetPitch());
|
||||||
// No copy for position or velocity
|
// No copy for position or velocity
|
||||||
SetVolume(emitter.GetVolume());
|
SetVolume(emitter.GetVolume());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
m_source = InvalidSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Constructs a SoundEmitter object by moving another
|
||||||
|
*
|
||||||
|
* \param emitter SoundEmitter to move
|
||||||
|
*
|
||||||
|
* \remark The moved sound emitter cannot be used after being moved
|
||||||
|
*/
|
||||||
|
SoundEmitter::SoundEmitter(SoundEmitter&& emitter) noexcept :
|
||||||
|
m_source(emitter.m_source)
|
||||||
|
{
|
||||||
|
emitter.m_source = InvalidSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Destructs the object
|
* \brief Destructs the object
|
||||||
*/
|
*/
|
||||||
|
|
||||||
SoundEmitter::~SoundEmitter()
|
SoundEmitter::~SoundEmitter()
|
||||||
{
|
{
|
||||||
alDeleteSources(1, &m_source);
|
if (m_source != InvalidSource)
|
||||||
|
alDeleteSources(1, &m_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
@ -65,6 +82,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::EnableSpatialization(bool spatialization)
|
void SoundEmitter::EnableSpatialization(bool spatialization)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcei(m_source, AL_SOURCE_RELATIVE, !spatialization);
|
alSourcei(m_source, AL_SOURCE_RELATIVE, !spatialization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +94,8 @@ namespace Nz
|
||||||
|
|
||||||
float SoundEmitter::GetAttenuation() const
|
float SoundEmitter::GetAttenuation() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALfloat attenuation;
|
ALfloat attenuation;
|
||||||
alGetSourcef(m_source, AL_ROLLOFF_FACTOR, &attenuation);
|
alGetSourcef(m_source, AL_ROLLOFF_FACTOR, &attenuation);
|
||||||
|
|
||||||
|
|
@ -88,6 +109,8 @@ namespace Nz
|
||||||
|
|
||||||
float SoundEmitter::GetMinDistance() const
|
float SoundEmitter::GetMinDistance() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALfloat distance;
|
ALfloat distance;
|
||||||
alGetSourcef(m_source, AL_REFERENCE_DISTANCE, &distance);
|
alGetSourcef(m_source, AL_REFERENCE_DISTANCE, &distance);
|
||||||
|
|
||||||
|
|
@ -101,6 +124,8 @@ namespace Nz
|
||||||
|
|
||||||
float SoundEmitter::GetPitch() const
|
float SoundEmitter::GetPitch() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALfloat pitch;
|
ALfloat pitch;
|
||||||
alGetSourcef(m_source, AL_PITCH, &pitch);
|
alGetSourcef(m_source, AL_PITCH, &pitch);
|
||||||
|
|
||||||
|
|
@ -114,6 +139,8 @@ namespace Nz
|
||||||
|
|
||||||
Vector3f SoundEmitter::GetPosition() const
|
Vector3f SoundEmitter::GetPosition() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
Vector3f position;
|
Vector3f position;
|
||||||
alGetSourcefv(m_source, AL_POSITION, position);
|
alGetSourcefv(m_source, AL_POSITION, position);
|
||||||
|
|
||||||
|
|
@ -127,6 +154,8 @@ namespace Nz
|
||||||
|
|
||||||
Vector3f SoundEmitter::GetVelocity() const
|
Vector3f SoundEmitter::GetVelocity() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
Vector3f velocity;
|
Vector3f velocity;
|
||||||
alGetSourcefv(m_source, AL_VELOCITY, velocity);
|
alGetSourcefv(m_source, AL_VELOCITY, velocity);
|
||||||
|
|
||||||
|
|
@ -140,6 +169,8 @@ namespace Nz
|
||||||
|
|
||||||
float SoundEmitter::GetVolume() const
|
float SoundEmitter::GetVolume() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALfloat gain;
|
ALfloat gain;
|
||||||
alGetSourcef(m_source, AL_GAIN, &gain);
|
alGetSourcef(m_source, AL_GAIN, &gain);
|
||||||
|
|
||||||
|
|
@ -153,6 +184,8 @@ namespace Nz
|
||||||
|
|
||||||
bool SoundEmitter::IsSpatialized() const
|
bool SoundEmitter::IsSpatialized() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALint relative;
|
ALint relative;
|
||||||
alGetSourcei(m_source, AL_SOURCE_RELATIVE, &relative);
|
alGetSourcei(m_source, AL_SOURCE_RELATIVE, &relative);
|
||||||
|
|
||||||
|
|
@ -167,6 +200,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetAttenuation(float attenuation)
|
void SoundEmitter::SetAttenuation(float attenuation)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcef(m_source, AL_ROLLOFF_FACTOR, attenuation);
|
alSourcef(m_source, AL_ROLLOFF_FACTOR, attenuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,6 +213,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetMinDistance(float minDistance)
|
void SoundEmitter::SetMinDistance(float minDistance)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcef(m_source, AL_REFERENCE_DISTANCE, minDistance);
|
alSourcef(m_source, AL_REFERENCE_DISTANCE, minDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,6 +226,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetPitch(float pitch)
|
void SoundEmitter::SetPitch(float pitch)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcef(m_source, AL_PITCH, pitch);
|
alSourcef(m_source, AL_PITCH, pitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +239,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetPosition(const Vector3f& position)
|
void SoundEmitter::SetPosition(const Vector3f& position)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcefv(m_source, AL_POSITION, position);
|
alSourcefv(m_source, AL_POSITION, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +252,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetPosition(float x, float y, float z)
|
void SoundEmitter::SetPosition(float x, float y, float z)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSource3f(m_source, AL_POSITION, x, y, z);
|
alSource3f(m_source, AL_POSITION, x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +265,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetVelocity(const Vector3f& velocity)
|
void SoundEmitter::SetVelocity(const Vector3f& velocity)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcefv(m_source, AL_VELOCITY, velocity);
|
alSourcefv(m_source, AL_VELOCITY, velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +278,8 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetVelocity(float velX, float velY, float velZ)
|
void SoundEmitter::SetVelocity(float velX, float velY, float velZ)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSource3f(m_source, AL_VELOCITY, velX, velY, velZ);
|
alSource3f(m_source, AL_VELOCITY, velX, velY, velZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,9 +291,25 @@ namespace Nz
|
||||||
|
|
||||||
void SoundEmitter::SetVolume(float volume)
|
void SoundEmitter::SetVolume(float volume)
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
alSourcef(m_source, AL_GAIN, volume * 0.01f);
|
alSourcef(m_source, AL_GAIN, volume * 0.01f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Assign a sound emitter by moving it
|
||||||
|
*
|
||||||
|
* \param emitter SoundEmitter to move
|
||||||
|
*
|
||||||
|
* \return *this
|
||||||
|
*/
|
||||||
|
SoundEmitter& SoundEmitter::operator=(SoundEmitter&& emitter) noexcept
|
||||||
|
{
|
||||||
|
std::swap(m_source, emitter.m_source);
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Gets the status of the sound emitter
|
* \brief Gets the status of the sound emitter
|
||||||
* \return Enumeration of type SoundStatus (Playing, Stopped, ...)
|
* \return Enumeration of type SoundStatus (Playing, Stopped, ...)
|
||||||
|
|
@ -254,6 +317,8 @@ namespace Nz
|
||||||
|
|
||||||
SoundStatus SoundEmitter::GetInternalStatus() const
|
SoundStatus SoundEmitter::GetInternalStatus() const
|
||||||
{
|
{
|
||||||
|
NazaraAssert(m_source != InvalidSource, "Invalid sound emitter");
|
||||||
|
|
||||||
ALint state;
|
ALint state;
|
||||||
alGetSourcei(m_source, AL_SOURCE_STATE, &state);
|
alGetSourcei(m_source, AL_SOURCE_STATE, &state);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue