mirror of https://github.com/AxioDL/amuse.git
Emitter bug fixes and test macro for amuseplay
This commit is contained in:
parent
c6781df90a
commit
4b2b86f420
|
@ -1,17 +1,10 @@
|
||||||
#include "amuse/amuse.hpp"
|
#include "amuse/amuse.hpp"
|
||||||
#include "amuse/BooBackend.hpp"
|
#include "amuse/BooBackend.hpp"
|
||||||
#include "athena/FileReader.hpp"
|
|
||||||
#include "boo/boo.hpp"
|
#include "boo/boo.hpp"
|
||||||
#include "boo/audiodev/IAudioVoiceEngine.hpp"
|
|
||||||
#include "logvisor/logvisor.hpp"
|
#include "logvisor/logvisor.hpp"
|
||||||
#include "optional.hpp"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
#define EMITTER_TEST 0
|
||||||
#include <unordered_map>
|
|
||||||
#include <stdarg.h>
|
|
||||||
|
|
||||||
static logvisor::Module Log("amuseplay");
|
static logvisor::Module Log("amuseplay");
|
||||||
|
|
||||||
|
@ -93,6 +86,14 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
bool m_breakout = false;
|
bool m_breakout = false;
|
||||||
int m_panicCount = 0;
|
int m_panicCount = 0;
|
||||||
|
|
||||||
|
#if EMITTER_TEST
|
||||||
|
amuse::Vector3f m_pos = {};
|
||||||
|
amuse::Vector3f m_dir = {0.f, 0.f, 0.f};
|
||||||
|
amuse::Vector3f m_listenerDir = {0.f, 40.f, 0.f};
|
||||||
|
std::shared_ptr<amuse::Emitter> m_emitter;
|
||||||
|
std::shared_ptr<amuse::Listener> m_listener;
|
||||||
|
#endif
|
||||||
|
|
||||||
void UpdateSongDisplay()
|
void UpdateSongDisplay()
|
||||||
{
|
{
|
||||||
size_t voxCount = 0;
|
size_t voxCount = 0;
|
||||||
|
@ -238,10 +239,17 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
void UpdateSFXDisplay()
|
void UpdateSFXDisplay()
|
||||||
{
|
{
|
||||||
bool playing = m_vox && m_vox->state() == amuse::VoiceState::Playing;
|
bool playing = m_vox && m_vox->state() == amuse::VoiceState::Playing;
|
||||||
|
#if EMITTER_TEST
|
||||||
|
printf(
|
||||||
|
"\r "
|
||||||
|
"\r %c SFX %d, VOL: %d%% POS: (%f,%f)\r",
|
||||||
|
playing ? '>' : ' ', m_sfxId, int(std::rint(m_volume * 100)), m_pos[0], m_pos[1]);
|
||||||
|
#else
|
||||||
printf(
|
printf(
|
||||||
"\r "
|
"\r "
|
||||||
"\r %c SFX %d, VOL: %d%%\r",
|
"\r %c SFX %d, VOL: %d%%\r",
|
||||||
playing ? '>' : ' ', m_sfxId, int(std::rint(m_volume * 100)));
|
playing ? '>' : ' ', m_sfxId, int(std::rint(m_volume * 100)));
|
||||||
|
#endif
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,8 +260,15 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
bool playing = m_vox && m_vox->state() == amuse::VoiceState::Playing;
|
bool playing = m_vox && m_vox->state() == amuse::VoiceState::Playing;
|
||||||
if (playing)
|
if (playing)
|
||||||
{
|
{
|
||||||
|
#if EMITTER_TEST
|
||||||
|
if (m_emitter)
|
||||||
|
m_emitter->getVoice()->keyOff();
|
||||||
|
m_emitter = m_engine->addEmitter(m_pos, m_dir, 100.f, 0.f, m_sfxId, 0.f, 1.f, true);
|
||||||
|
m_vox = m_emitter->getVoice();
|
||||||
|
#else
|
||||||
m_vox->keyOff();
|
m_vox->keyOff();
|
||||||
m_vox = m_engine->fxStart(m_sfxId, m_volume, 0.f);
|
m_vox = m_engine->fxStart(m_sfxId, m_volume, 0.f);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSFXDisplay();
|
UpdateSFXDisplay();
|
||||||
|
@ -271,8 +286,26 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
if (sfxIt != sortEntries.cend())
|
if (sfxIt != sortEntries.cend())
|
||||||
SelectSFX(sfxIt->first);
|
SelectSFX(sfxIt->first);
|
||||||
|
|
||||||
|
#if EMITTER_TEST
|
||||||
|
float emitterTheta = 0.f;
|
||||||
|
float zeroVec[3] = {};
|
||||||
|
float heading[3] = {0.f, 1.f, 0.f};
|
||||||
|
float up[3] = {0.f, 0.f, 1.f};
|
||||||
|
m_listener = m_engine->addListener(zeroVec, m_listenerDir, heading, up, 5.f, 5.f, 1000.f, 1.f);
|
||||||
|
#endif
|
||||||
|
|
||||||
while (m_running)
|
while (m_running)
|
||||||
{
|
{
|
||||||
|
#if EMITTER_TEST
|
||||||
|
//float dist = std::sin(emitterTheta * 0.25f);
|
||||||
|
m_pos[0] = std::cos(emitterTheta) * 5.f;
|
||||||
|
m_pos[1] = std::sin(emitterTheta) * 5.f;
|
||||||
|
if (m_emitter)
|
||||||
|
m_emitter->setVectors(m_pos, m_dir);
|
||||||
|
emitterTheta += 1.f / 60.f;
|
||||||
|
m_updateDisp = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
m_events.dispatchEvents();
|
m_events.dispatchEvents();
|
||||||
|
|
||||||
if (m_wantsNext)
|
if (m_wantsNext)
|
||||||
|
@ -310,6 +343,9 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
if (m_vox && m_vox->state() == amuse::VoiceState::Dead)
|
if (m_vox && m_vox->state() == amuse::VoiceState::Dead)
|
||||||
{
|
{
|
||||||
m_vox.reset();
|
m_vox.reset();
|
||||||
|
#if EMITTER_TEST
|
||||||
|
m_emitter.reset();
|
||||||
|
#endif
|
||||||
UpdateSFXDisplay();
|
UpdateSFXDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,11 +353,19 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
{
|
{
|
||||||
m_breakout = false;
|
m_breakout = false;
|
||||||
m_vox.reset();
|
m_vox.reset();
|
||||||
|
#if EMITTER_TEST
|
||||||
|
m_emitter.reset();
|
||||||
|
#endif
|
||||||
m_seq->allOff(true);
|
m_seq->allOff(true);
|
||||||
m_seq.reset();
|
m_seq.reset();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if EMITTER_TEST
|
||||||
|
m_engine->removeListener(m_listener.get());
|
||||||
|
m_listener.reset();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void charKeyDownRepeat(unsigned long charCode)
|
void charKeyDownRepeat(unsigned long charCode)
|
||||||
|
@ -389,7 +433,14 @@ struct AppCallback : boo::IApplicationCallback
|
||||||
if (m_vox && m_vox->state() == amuse::VoiceState::Playing)
|
if (m_vox && m_vox->state() == amuse::VoiceState::Playing)
|
||||||
m_vox->keyOff();
|
m_vox->keyOff();
|
||||||
else if (m_sfxId != -1)
|
else if (m_sfxId != -1)
|
||||||
|
{
|
||||||
|
#if EMITTER_TEST
|
||||||
|
m_emitter = m_engine->addEmitter(m_pos, m_dir, 100.f, 0.f, m_sfxId, 0.f, 1.f, true);
|
||||||
|
m_vox = m_emitter->getVoice();
|
||||||
|
#else
|
||||||
m_vox = m_engine->fxStart(m_sfxId, m_volume, 0.f);
|
m_vox = m_engine->fxStart(m_sfxId, m_volume, 0.f);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
m_updateDisp = true;
|
m_updateDisp = true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -23,6 +23,17 @@ static float Length(const Vector3f& a)
|
||||||
return std::sqrt(Dot(a, a));
|
return std::sqrt(Dot(a, a));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static float Normalize(Vector3f& out)
|
||||||
|
{
|
||||||
|
float dist = Length(out);
|
||||||
|
if (dist == 0.f)
|
||||||
|
return 0.f;
|
||||||
|
out[0] /= dist;
|
||||||
|
out[1] /= dist;
|
||||||
|
out[2] /= dist;
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
/** Voice wrapper with positional-3D level control */
|
/** Voice wrapper with positional-3D level control */
|
||||||
class Emitter : public Entity
|
class Emitter : public Entity
|
||||||
{
|
{
|
||||||
|
@ -34,6 +45,7 @@ class Emitter : public Entity
|
||||||
float m_minVol;
|
float m_minVol;
|
||||||
float m_falloff;
|
float m_falloff;
|
||||||
bool m_doppler;
|
bool m_doppler;
|
||||||
|
bool m_dirty = true;
|
||||||
|
|
||||||
friend class Engine;
|
friend class Engine;
|
||||||
void _destroy();
|
void _destroy();
|
||||||
|
@ -42,13 +54,13 @@ class Emitter : public Entity
|
||||||
|
|
||||||
public:
|
public:
|
||||||
~Emitter();
|
~Emitter();
|
||||||
Emitter(Engine& engine, const AudioGroup& group, std::shared_ptr<Voice>&& vox,
|
Emitter(Engine& engine, const AudioGroup& group, const std::shared_ptr<Voice>& vox,
|
||||||
float maxDist, float minVol, float falloff, bool doppler);
|
float maxDist, float minVol, float falloff, bool doppler);
|
||||||
|
|
||||||
void setVectors(const float* pos, const float* dir);
|
void setVectors(const float* pos, const float* dir);
|
||||||
void setMaxVol(float maxVol) { m_maxVol = clamp(0.f, maxVol, 1.f); }
|
void setMaxVol(float maxVol) { m_maxVol = clamp(0.f, maxVol, 1.f); m_dirty = true; }
|
||||||
|
|
||||||
std::shared_ptr<Voice>& getVoice() { return m_vox; }
|
const std::shared_ptr<Voice>& getVoice() const { return m_vox; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace amuse
|
||||||
class Listener
|
class Listener
|
||||||
{
|
{
|
||||||
friend class Emitter;
|
friend class Emitter;
|
||||||
|
friend class Engine;
|
||||||
Vector3f m_pos = {};
|
Vector3f m_pos = {};
|
||||||
Vector3f m_dir = {};
|
Vector3f m_dir = {};
|
||||||
Vector3f m_heading = {};
|
Vector3f m_heading = {};
|
||||||
|
@ -17,11 +18,12 @@ class Listener
|
||||||
float m_frontDiff;
|
float m_frontDiff;
|
||||||
float m_backDiff;
|
float m_backDiff;
|
||||||
float m_soundSpeed;
|
float m_soundSpeed;
|
||||||
|
bool m_dirty = true;
|
||||||
public:
|
public:
|
||||||
Listener(float volume, float frontDiff, float backDiff, float soundSpeed)
|
Listener(float volume, float frontDiff, float backDiff, float soundSpeed)
|
||||||
: m_volume(clamp(0.f, volume, 1.f)), m_frontDiff(frontDiff), m_backDiff(backDiff), m_soundSpeed(soundSpeed) {}
|
: m_volume(clamp(0.f, volume, 1.f)), m_frontDiff(frontDiff), m_backDiff(backDiff), m_soundSpeed(soundSpeed) {}
|
||||||
void setVectors(const float* pos, const float* dir, const float* heading, const float* up);
|
void setVectors(const float* pos, const float* dir, const float* heading, const float* up);
|
||||||
void setVolume(float vol) { m_volume = clamp(0.f, vol, 1.f); }
|
void setVolume(float vol) { m_volume = clamp(0.f, vol, 1.f); m_dirty = true; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ static void Delta(Vector3f& out, const Vector3f& a, const Vector3f& b)
|
||||||
|
|
||||||
Emitter::~Emitter() {}
|
Emitter::~Emitter() {}
|
||||||
|
|
||||||
Emitter::Emitter(Engine& engine, const AudioGroup& group, std::shared_ptr<Voice>&& vox,
|
Emitter::Emitter(Engine& engine, const AudioGroup& group, const std::shared_ptr<Voice>& vox,
|
||||||
float maxDist, float minVol, float falloff, bool doppler)
|
float maxDist, float minVol, float falloff, bool doppler)
|
||||||
: Entity(engine, group, vox->getObjectId()), m_vox(std::move(vox)), m_maxDist(maxDist),
|
: Entity(engine, group, vox->getObjectId()), m_vox(vox), m_maxDist(maxDist),
|
||||||
m_minVol(clamp(0.f, minVol, 1.f)), m_falloff(clamp(-1.f, falloff, 1.f)), m_doppler(doppler)
|
m_minVol(clamp(0.f, minVol, 1.f)), m_falloff(clamp(-1.f, falloff, 1.f)), m_doppler(doppler)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,22 @@ float Emitter::_attenuationCurve(float dist) const
|
||||||
|
|
||||||
void Emitter::_update()
|
void Emitter::_update()
|
||||||
{
|
{
|
||||||
|
if (!m_dirty)
|
||||||
|
{
|
||||||
|
/* Ensure that all listeners are also not dirty */
|
||||||
|
bool dirty = false;
|
||||||
|
for (auto& listener : m_engine.m_activeListeners)
|
||||||
|
{
|
||||||
|
if (listener->m_dirty)
|
||||||
|
{
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dirty)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
float coefs[8] = {};
|
float coefs[8] = {};
|
||||||
double avgDopplerRatio = 0.0;
|
double avgDopplerRatio = 0.0;
|
||||||
|
|
||||||
|
@ -85,10 +101,13 @@ void Emitter::_update()
|
||||||
{
|
{
|
||||||
/* Positive values indicate emitter and listener closing in */
|
/* Positive values indicate emitter and listener closing in */
|
||||||
Vector3f dirDelta;
|
Vector3f dirDelta;
|
||||||
Delta(dirDelta, listener->m_dir, m_dir);
|
Delta(dirDelta, m_dir, listener->m_dir);
|
||||||
float sign = -Dot(listener->m_dir, m_dir);
|
Vector3f posDelta;
|
||||||
|
Delta(posDelta, listener->m_pos, m_pos);
|
||||||
|
Normalize(posDelta);
|
||||||
|
float deltaSpeed = Dot(dirDelta, posDelta);
|
||||||
if (listener->m_soundSpeed != 0.f)
|
if (listener->m_soundSpeed != 0.f)
|
||||||
avgDopplerRatio += 1.0 + std::copysign(Length(dirDelta), sign) / listener->m_soundSpeed;
|
avgDopplerRatio += 1.0 + deltaSpeed / listener->m_soundSpeed;
|
||||||
else
|
else
|
||||||
avgDopplerRatio += 1.0;
|
avgDopplerRatio += 1.0;
|
||||||
}
|
}
|
||||||
|
@ -98,8 +117,13 @@ void Emitter::_update()
|
||||||
{
|
{
|
||||||
m_vox->setChannelCoefs(coefs);
|
m_vox->setChannelCoefs(coefs);
|
||||||
if (m_doppler)
|
if (m_doppler)
|
||||||
|
{
|
||||||
m_vox->m_dopplerRatio = avgDopplerRatio / float(m_engine.m_activeListeners.size());
|
m_vox->m_dopplerRatio = avgDopplerRatio / float(m_engine.m_activeListeners.size());
|
||||||
|
m_vox->m_pitchDirty = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Emitter::setVectors(const float* pos, const float* dir)
|
void Emitter::setVectors(const float* pos, const float* dir)
|
||||||
|
@ -109,6 +133,7 @@ void Emitter::setVectors(const float* pos, const float* dir)
|
||||||
m_pos[i] = pos[i];
|
m_pos[i] = pos[i];
|
||||||
m_dir[i] = dir[i];
|
m_dir[i] = dir[i];
|
||||||
}
|
}
|
||||||
|
m_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,8 @@ void Engine::_on5MsInterval(IBackendVoiceAllocator& engine, double dt)
|
||||||
seq->advance(dt);
|
seq->advance(dt);
|
||||||
for (std::shared_ptr<Emitter>& emitter : m_activeEmitters)
|
for (std::shared_ptr<Emitter>& emitter : m_activeEmitters)
|
||||||
emitter->_update();
|
emitter->_update();
|
||||||
|
for (std::shared_ptr<Listener>& listener : m_activeListeners)
|
||||||
|
listener->m_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::_onPumpCycleComplete(IBackendVoiceAllocator& engine)
|
void Engine::_onPumpCycleComplete(IBackendVoiceAllocator& engine)
|
||||||
|
@ -338,7 +340,7 @@ std::shared_ptr<Emitter> Engine::addEmitter(const float* pos, const float* dir,
|
||||||
std::list<std::shared_ptr<Voice>>::iterator vox =
|
std::list<std::shared_ptr<Voice>>::iterator vox =
|
||||||
_allocateVoice(*grp, std::get<1>(search->second), NativeSampleRate, true, true, smx);
|
_allocateVoice(*grp, std::get<1>(search->second), NativeSampleRate, true, true, smx);
|
||||||
auto emitIt = m_activeEmitters.emplace(m_activeEmitters.end(),
|
auto emitIt = m_activeEmitters.emplace(m_activeEmitters.end(),
|
||||||
new Emitter(*this, *grp, std::move(*vox), maxDist, minVol, falloff, doppler));
|
new Emitter(*this, *grp, *vox, maxDist, minVol, falloff, doppler));
|
||||||
Emitter& ret = *(*emitIt);
|
Emitter& ret = *(*emitIt);
|
||||||
|
|
||||||
ObjectId oid = (grp->getDataFormat() == DataFormat::PC) ? entry->objId : SBig(entry->objId);
|
ObjectId oid = (grp->getDataFormat() == DataFormat::PC) ? entry->objId : SBig(entry->objId);
|
||||||
|
@ -346,10 +348,11 @@ std::shared_ptr<Emitter> Engine::addEmitter(const float* pos, const float* dir,
|
||||||
{
|
{
|
||||||
ret._destroy();
|
ret._destroy();
|
||||||
m_activeEmitters.erase(emitIt);
|
m_activeEmitters.erase(emitIt);
|
||||||
|
_destroyVoice(vox);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
(*vox)->setPan(entry->panning);
|
ret.getVoice()->setPan(entry->panning);
|
||||||
ret.setVectors(pos, dir);
|
ret.setVectors(pos, dir);
|
||||||
ret.setMaxVol(maxVol);
|
ret.setMaxVol(maxVol);
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,6 @@ static void Cross(Vector3f& out, const Vector3f& a, const Vector3f& b)
|
||||||
out[2] = a[0] * b[1] - a[1] * b[0];
|
out[2] = a[0] * b[1] - a[1] * b[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static float Normalize(Vector3f& out)
|
|
||||||
{
|
|
||||||
float dist = Length(out);
|
|
||||||
if (dist == 0.f)
|
|
||||||
return 0.f;
|
|
||||||
out[0] /= dist;
|
|
||||||
out[1] /= dist;
|
|
||||||
out[2] /= dist;
|
|
||||||
return dist;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Listener::setVectors(const float* pos, const float* dir, const float* heading, const float* up)
|
void Listener::setVectors(const float* pos, const float* dir, const float* heading, const float* up)
|
||||||
{
|
{
|
||||||
for (int i=0 ; i<3 ; ++i)
|
for (int i=0 ; i<3 ; ++i)
|
||||||
|
@ -30,10 +19,13 @@ void Listener::setVectors(const float* pos, const float* dir, const float* headi
|
||||||
m_heading[i] = heading[i];
|
m_heading[i] = heading[i];
|
||||||
m_up[i] = up[i];
|
m_up[i] = up[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
Normalize(m_heading);
|
Normalize(m_heading);
|
||||||
Normalize(m_up);
|
Normalize(m_up);
|
||||||
Cross(m_right, m_heading, m_up);
|
Cross(m_right, m_heading, m_up);
|
||||||
Normalize(m_right);
|
Normalize(m_right);
|
||||||
|
|
||||||
|
m_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ std::list<std::shared_ptr<Voice>>::iterator Voice::_destroyVoice(std::list<std::
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static T ApplyVolume(float vol, T samp)
|
static T ApplyVolume(float vol, T samp)
|
||||||
{
|
{
|
||||||
return samp * 0.7f * vol;
|
return samp * vol;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Voice::_procSamplePre(int16_t& samp)
|
void Voice::_procSamplePre(int16_t& samp)
|
||||||
|
@ -1003,7 +1003,7 @@ void Voice::_panLaw(float coefs[8], float frontPan, float backPan, float totalSp
|
||||||
coefs[4] *= -totalSpan * 0.5f + 0.5f;
|
coefs[4] *= -totalSpan * 0.5f + 0.5f;
|
||||||
|
|
||||||
/* LFE */
|
/* LFE */
|
||||||
coefs[5] = 1.f;
|
coefs[5] = 0.35f;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -1029,7 +1029,7 @@ void Voice::_panLaw(float coefs[8], float frontPan, float backPan, float totalSp
|
||||||
coefs[4] *= (totalSpan <= 0.f) ? -totalSpan : 0.f;
|
coefs[4] *= (totalSpan <= 0.f) ? -totalSpan : 0.f;
|
||||||
|
|
||||||
/* LFE */
|
/* LFE */
|
||||||
coefs[5] = 1.f;
|
coefs[5] = 0.35f;
|
||||||
|
|
||||||
/* Side Left */
|
/* Side Left */
|
||||||
coefs[6] = (backPan <= 0.f) ? -backPan : 0.f;
|
coefs[6] = (backPan <= 0.f) ? -backPan : 0.f;
|
||||||
|
|
Loading…
Reference in New Issue