1727 lines
50 KiB
C++
1727 lines
50 KiB
C++
/**********************************************************************
|
|
|
|
Filename : GSoundRendererFMODImpl.cpp
|
|
Content : Sound Driver - FMOD Ex
|
|
Created : November, 2008
|
|
Authors : Andrew Reisse, Maxim Didenko, Vladislav Merker
|
|
|
|
Copyright : (c) 1998-2009 Scaleform Corp. All Rights Reserved.
|
|
|
|
Licensees may use this file in accordance with the valid Scaleform
|
|
Commercial License Agreement provided with the software.
|
|
|
|
This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
|
THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR ANY PURPOSE.
|
|
|
|
**********************************************************************/
|
|
|
|
#ifndef INC_GSOUNDPLAYERFMODIMPL_H
|
|
#define INC_GSOUNDPLAYERFMODIMPL_H
|
|
|
|
#include "GSoundRendererFMOD.h"
|
|
|
|
#ifndef GFC_NO_SOUND
|
|
|
|
#include "GSoundRendererComImpl.h"
|
|
|
|
#include "GFunctions.h"
|
|
#include "GArray.h"
|
|
#include "GAtomic.h"
|
|
#include "GHash.h"
|
|
#include "GStd.h"
|
|
#include "GAllocator.h"
|
|
#include "GTimer.h"
|
|
#include "GThreads.h"
|
|
#include "GHeapNew.h"
|
|
|
|
#include <fmod.hpp>
|
|
#include <fmod_errors.h>
|
|
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
#include <fmod_event.hpp>
|
|
#include <fmod_event_net.hpp>
|
|
#endif
|
|
|
|
class GSoundRendererFMODImpl;
|
|
class GSoundChannelFMODImpl;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
class GSoundSampleFMODImpl : public GSoundSampleImplNode
|
|
{
|
|
public:
|
|
GSoundRendererFMODImpl* pPlayer;
|
|
FMOD::Sound* pSound;
|
|
GPtr<GSoundDataBase> pSoundData;
|
|
|
|
GSoundSampleFMODImpl(GSoundRendererFMODImpl* pp);
|
|
~GSoundSampleFMODImpl();
|
|
|
|
virtual GSoundRenderer* GetSoundRenderer() const;
|
|
virtual bool IsDataValid() const { return pSound != 0; }
|
|
virtual Float GetDuration() const;
|
|
// Returns the size, in bytes
|
|
virtual SInt GetBytesTotal() const;
|
|
// Returns the number of bytes loaded
|
|
virtual SInt GetBytesLoaded() const;
|
|
|
|
virtual void ReleaseResource();
|
|
void ReleaseFMODObjects();
|
|
|
|
bool IsPlaying(void*);
|
|
|
|
virtual GSoundChannelFMODImpl* Start(bool paused);
|
|
|
|
FMOD_RESULT CreateSubSound(GSoundData* psd, FMOD::Sound** psound);
|
|
FMOD_RESULT CreateSubSound(GSoundFile* psd, FMOD::Sound** psound);
|
|
FMOD_RESULT CreateSubSound(GAppendableSoundData* psd, FMOD::Sound** psound);
|
|
};
|
|
|
|
class GSoundChannelFMODImpl : public GSoundChannelImplNode
|
|
{
|
|
public:
|
|
GSoundRendererFMODImpl* pPlayer;
|
|
GSoundSampleFMODImpl* pSample;
|
|
FMOD::Channel* pChan;
|
|
|
|
GHash<FMOD_SYNCPOINT*, Transform> Tranforms;
|
|
|
|
GSoundChannelFMODImpl (GSoundRendererFMODImpl* pp, GSoundSampleFMODImpl* ps, FMOD::Channel* pc);
|
|
~GSoundChannelFMODImpl();
|
|
|
|
GSoundRenderer* GetSoundRenderer() const;
|
|
GSoundSample* GetSample() const { return pSample; }
|
|
|
|
virtual void ReleaseResource();
|
|
void ReleaseFMODObjects();
|
|
|
|
virtual void Stop();
|
|
virtual void Pause(bool pause);
|
|
virtual bool IsPlaying() const;
|
|
virtual void SetPosition(Float seconds);
|
|
virtual Float GetPosition();
|
|
virtual void Loop(SInt count, Float start = 0, Float end = 0);
|
|
virtual Float GetVolume();
|
|
virtual void SetVolume(Float volume);
|
|
virtual Float GetPan();
|
|
virtual void SetPan(Float volume);
|
|
virtual void SetTransforms(const GArray<Transform>& transforms);
|
|
virtual void SetSpatialInfo(const GArray<Vector> []) {}
|
|
|
|
static FMOD_RESULT F_CALLBACK CallBackFunc(
|
|
FMOD_CHANNEL* pchan, FMOD_CHANNEL_CALLBACKTYPE cb, void* cmddata1, void* cmddata2);
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
class GSoundChannelFMODImplAux;
|
|
class GSoundSampleFMODImplAux;
|
|
typedef GHash<GSoundRenderer::AuxStreamer*, GArray<GSoundChannelFMODImplAux*> > AuxStreamersType;
|
|
|
|
class GSoundRendererFMODImpl : public GSoundRendererFMOD
|
|
{
|
|
public:
|
|
FMOD::System* pDevice;
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
FMOD::EventSystem* pEventSys;
|
|
#endif
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
GPtr<GThread> pUpdateThread;
|
|
GEvent Event;
|
|
volatile bool StopThread;
|
|
#endif
|
|
GLock AuxStreamsLock;
|
|
AuxStreamersType AuxStreamers;
|
|
|
|
GDListNode SampleList;
|
|
GLock SampleListLock;
|
|
bool CallFMODUpdate;
|
|
bool ThreadedUpdate;
|
|
#ifdef GFC_OS_XBOX360
|
|
int ProcNumber;
|
|
#endif
|
|
Float SystemBitRate;
|
|
|
|
GSoundRendererFMODImpl();
|
|
virtual ~GSoundRendererFMODImpl() { xFinalize(); }
|
|
|
|
virtual bool GetRenderCaps(UInt32 *pcaps);
|
|
|
|
virtual FMOD::System* GetFMODSystem() { return pDevice; }
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
virtual FMOD::EventSystem* GetFMODEventSystem() { return pEventSys; }
|
|
#endif
|
|
|
|
virtual bool Initialize(FMOD::System* pd, bool call_fmod_update, bool threaded_update
|
|
#ifdef GFC_OS_XBOX360
|
|
, int processor_core = 5
|
|
#endif
|
|
);
|
|
virtual void Finalize() { xFinalize(); }
|
|
void xFinalize(); // Avoid call to a virtual method from the destructor
|
|
|
|
virtual GSoundSampleFMODImpl* CreateSampleFromData(GSoundDataBase* psd);
|
|
virtual GSoundSampleFMODImpl* CreateSampleFromFile(const char* fname, bool streaming);
|
|
virtual GSoundSampleFMODImpl* CreateSampleFromAuxStreamer(AuxStreamer* pStreamder,
|
|
UInt32 channels,
|
|
UInt32 samplerate,
|
|
AuxStreamer::PCMFormat fmt);
|
|
|
|
virtual GSoundChannelFMODImpl* PlaySample(GSoundSample* ps, bool paused);
|
|
|
|
virtual Float GetMasterVolume();
|
|
virtual void SetMasterVolume(Float volume);
|
|
|
|
virtual Float Update();
|
|
virtual void Mute(bool mute);
|
|
|
|
void AttachAuxStreamer(GSoundChannelFMODImplAux*);
|
|
void DetachAuxStreamer(GSoundChannelFMODImplAux*);
|
|
|
|
void LogError(FMOD_RESULT result);
|
|
void PrintStatistics();
|
|
Float UpdateAuxStreams();
|
|
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
static SInt UpdateFunc(GThread*, void* h);
|
|
void PulseEvent() { Event.PulseEvent(); }
|
|
#endif
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
FMOD_RESULT F_CALLBACK DecodeOpen(const char *sd, int unicode, unsigned int *filesize, void **handle, void **userdata)
|
|
{
|
|
GUNUSED2(unicode, handle);
|
|
GAppendableSoundData* psd = (GAppendableSoundData*)sd;
|
|
psd->SeekPos(0);
|
|
*userdata = psd;
|
|
*filesize = 0x0FFFFFFF;
|
|
return FMOD_OK;
|
|
}
|
|
FMOD_RESULT F_CALLBACK DecodeClose(void *handle, void *userdata)
|
|
{
|
|
GUNUSED2(userdata, handle);
|
|
GAppendableSoundData* psd = (GAppendableSoundData*)userdata;
|
|
psd->SeekPos(0);
|
|
return FMOD_OK;
|
|
}
|
|
FMOD_RESULT F_CALLBACK DecodeRead(void *handle, void *buffer, unsigned int sizebytes, unsigned int *bytesread, void *userdata)
|
|
{
|
|
GUNUSED(handle);
|
|
GAppendableSoundData* psd = (GAppendableSoundData*)userdata;
|
|
*bytesread = psd->GetData((UByte*)buffer, sizebytes);
|
|
return FMOD_OK;
|
|
}
|
|
FMOD_RESULT F_CALLBACK DecodeSeek(void *handle, unsigned int pos, void *userdata)
|
|
{
|
|
GUNUSED(handle);
|
|
GAppendableSoundData* psd = (GAppendableSoundData*)userdata;
|
|
if (!psd->SeekPos(pos))
|
|
return FMOD_ERR_FILE_COULDNOTSEEK;
|
|
return FMOD_OK;
|
|
}
|
|
|
|
FMOD_RESULT CreateSubSound(GSoundRendererFMODImpl* prenderer, GAppendableSoundData* psd, FMOD::Sound** psound)
|
|
{
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
|
exinfo.length = 0x0FFFFFFF;
|
|
int flags = FMOD_OPENONLY | FMOD_SOFTWARE | FMOD_IGNORETAGS;
|
|
switch (psd->GetFormat() & GSoundData::Sample_Format)
|
|
{
|
|
case GSoundData::Sample_PCM:
|
|
exinfo.format = (psd->GetFormat() & 0x7) == 2 ? FMOD_SOUND_FORMAT_PCM16 : FMOD_SOUND_FORMAT_PCM8;
|
|
exinfo.defaultfrequency = psd->GetRate();
|
|
exinfo.numchannels = (psd->GetFormat() & GSoundData::Sample_Stereo) ? 2 : 1;
|
|
flags |= FMOD_OPENRAW;
|
|
break;
|
|
case GSoundData::Sample_MP3:
|
|
exinfo.format = FMOD_SOUND_FORMAT_MPEG;
|
|
break;
|
|
default:
|
|
return FMOD_ERR_FORMAT;
|
|
}
|
|
|
|
exinfo.useropen = &DecodeOpen;
|
|
exinfo.userclose= &DecodeClose;
|
|
exinfo.userread = &DecodeRead;
|
|
exinfo.userseek = &DecodeSeek;
|
|
exinfo.decodebuffersize = 1024*8;
|
|
|
|
FMOD_RESULT result = prenderer->pDevice->createStream((const char*)psd,flags, &exinfo, psound);
|
|
return result;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
class GSwfSoundStreamer : public GSoundRenderer::AuxStreamer, public GDListNode
|
|
{
|
|
public:
|
|
GSwfSoundStreamer(GSoundRendererFMODImpl* prenderer, GAppendableSoundData* psd);
|
|
~GSwfSoundStreamer();
|
|
|
|
virtual UInt GetPCMData(UByte* pdata, UInt datasize);
|
|
virtual bool SetPosition(Float seconds);
|
|
|
|
virtual void ReleaseResource();
|
|
void ReleaseFMODObjects();
|
|
|
|
bool CreateReader();
|
|
bool GetSoundFormat(GSoundRenderer::AuxStreamer::PCMFormat* fmt, UInt32 *channels, UInt32 *samplerate);
|
|
|
|
GPtr<GAppendableSoundData> pSoundData;
|
|
FMOD::Sound* pSound;
|
|
GSoundRendererFMODImpl* pRenderer;
|
|
};
|
|
|
|
GSwfSoundStreamer::GSwfSoundStreamer(GSoundRendererFMODImpl* prenderer, GAppendableSoundData* psd)
|
|
: GDListNode(&prenderer->SampleList), pSoundData(psd), pSound(NULL), pRenderer(prenderer)
|
|
{
|
|
}
|
|
|
|
void GSwfSoundStreamer::ReleaseResource()
|
|
{
|
|
pRenderer = NULL;
|
|
if (GetRefCount() > 0)
|
|
ReleaseFMODObjects();
|
|
if (pNext)
|
|
RemoveNode();
|
|
}
|
|
void GSwfSoundStreamer::ReleaseFMODObjects()
|
|
{
|
|
if (pSound)
|
|
pSound->release();
|
|
pSound = NULL;
|
|
}
|
|
GSwfSoundStreamer::~GSwfSoundStreamer()
|
|
{
|
|
ReleaseFMODObjects();
|
|
if(pFirst)
|
|
RemoveNode();
|
|
}
|
|
|
|
UInt GSwfSoundStreamer::GetPCMData(UByte* pdata, UInt datasize)
|
|
{
|
|
UInt got_bytes = 0;
|
|
if (pSound)
|
|
pSound->readData(pdata,datasize,&got_bytes);
|
|
return got_bytes;
|
|
}
|
|
|
|
bool GSwfSoundStreamer::SetPosition(Float seconds)
|
|
{
|
|
if (pSound && pSoundData)
|
|
{
|
|
UInt latency = pSoundData ? pSoundData->GetSeekSample() : 0;
|
|
Float sample_rate;
|
|
pSound->getDefaults(&sample_rate,NULL,NULL,NULL);
|
|
UInt sample = UInt(seconds * sample_rate);
|
|
FMOD_RESULT res = pSound->seekData(sample + latency);
|
|
return res == FMOD_OK;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool GSwfSoundStreamer::CreateReader()
|
|
{
|
|
if (pRenderer)
|
|
return CreateSubSound(pRenderer, pSoundData, &pSound) == FMOD_OK;
|
|
return false;
|
|
}
|
|
|
|
bool GSwfSoundStreamer::GetSoundFormat(GSoundRenderer::AuxStreamer::PCMFormat* fmt, UInt32 *channels, UInt32 *samplerate)
|
|
{
|
|
if (!pSound)
|
|
return false;
|
|
FMOD_SOUND_FORMAT format = FMOD_SOUND_FORMAT_NONE;
|
|
int chans = 0;
|
|
FMOD_RESULT ret;
|
|
ret = pSound->getFormat(NULL, &format, &chans, NULL);
|
|
if (ret != FMOD_OK)
|
|
return false;
|
|
*channels = chans;
|
|
Float frequency = 0.0;
|
|
ret = pSound->getDefaults(&frequency, NULL, NULL, NULL);
|
|
if (ret != FMOD_OK)
|
|
return false;
|
|
*samplerate = (UInt32)frequency;
|
|
if (format == FMOD_SOUND_FORMAT_PCM16)
|
|
*fmt = PCM_SInt16;
|
|
else if (format == FMOD_SOUND_FORMAT_PCMFLOAT)
|
|
*fmt = PCM_Float;
|
|
else
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
#define AUX_SOUND_READBUFLEN_MS 300
|
|
#define AUX_SOUND_LEN_MS (AUX_SOUND_READBUFLEN_MS * 5)
|
|
|
|
class GSoundChannelFMODImplAux : public GSoundChannelFMODImpl
|
|
{
|
|
public:
|
|
GSoundChannelFMODImplAux(GSoundRendererFMODImpl* pp, GSoundSampleFMODImplAux* ps, FMOD::Channel* pc);
|
|
~GSoundChannelFMODImplAux();
|
|
|
|
virtual Float GetPosition();
|
|
virtual void SetPosition(Float seconds);
|
|
virtual void Stop();
|
|
virtual void Pause(bool pause);
|
|
|
|
Float Update();
|
|
|
|
UInt64 StartTick;
|
|
UInt64 StopTick;
|
|
UInt64 TotalTicks;
|
|
UInt64 NextFillTick;
|
|
bool Paused;
|
|
bool Starving;
|
|
GLock ChannelLock;
|
|
Float StartSecond;
|
|
|
|
};
|
|
|
|
class GSoundSampleFMODImplAux : public GSoundSampleFMODImpl
|
|
{
|
|
public:
|
|
typedef GSoundRenderer::AuxStreamer AuxStreamer;
|
|
|
|
GSoundSampleFMODImplAux(GSoundRendererFMODImpl* pp, AuxStreamer* pstreamer, AuxStreamer::PCMFormat fmt,
|
|
UInt32 channels, UInt32 samplerate);
|
|
~GSoundSampleFMODImplAux();
|
|
|
|
virtual GSoundChannelFMODImplAux* Start(bool paused);
|
|
|
|
UInt ReadAndFillSound();
|
|
void ClearSoundBuffer();
|
|
|
|
bool SeekData(Float seconds);
|
|
|
|
UInt GetTotalBytesRead() const { return TotatBytesRead; }
|
|
UInt64 GetTotalBytesReadInTicks() const { return BytesToTicks(TotatBytesRead); }
|
|
UInt GetFillPosition() const { return FillPosition; }
|
|
UInt TicksToSoundPos(UInt64 ticks) const
|
|
{
|
|
return UInt((((ticks / 1000) % AUX_SOUND_LEN_MS) * (SampleRate / 1000)) * Channels * (Bits / 8));
|
|
}
|
|
UInt64 BytesToTicks(UInt bytes) const
|
|
{
|
|
return UInt64(bytes) * 8/ Bits / Channels * 1000000 / SampleRate;
|
|
}
|
|
inline UInt DistToFillBuffPos(UInt pos) const;
|
|
|
|
GPtr<AuxStreamer> pStreamer;
|
|
UInt32 Channels;
|
|
UInt32 SampleRate;
|
|
UInt Bits;
|
|
UByte* pBlockBuffer;
|
|
UInt BlockSize;
|
|
UInt SoundLength;
|
|
FMOD_SOUND_FORMAT Format;
|
|
UInt FillPosition;
|
|
UInt TotatBytesRead;
|
|
};
|
|
|
|
GSoundSampleFMODImplAux::GSoundSampleFMODImplAux(GSoundRendererFMODImpl* pp, AuxStreamer* pstreamer, AuxStreamer::PCMFormat fmt,
|
|
UInt32 channels, UInt32 samplerate)
|
|
: GSoundSampleFMODImpl(pp), Channels(channels), SampleRate(samplerate), pBlockBuffer(0), BlockSize(0),
|
|
FillPosition(0), TotatBytesRead(0)
|
|
{
|
|
pStreamer = pstreamer;
|
|
Format = fmt == AuxStreamer::PCM_SInt16 ? FMOD_SOUND_FORMAT_PCM16 : FMOD_SOUND_FORMAT_PCMFLOAT;
|
|
Bits = fmt == GSoundRenderer::AuxStreamer::PCM_SInt16 ? 16 : 32;
|
|
SoundLength = (AUX_SOUND_LEN_MS * (SampleRate /1000)) * Channels * (Bits / 8);
|
|
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); // Required
|
|
exinfo.length = SoundLength; // Length of PCM data in bytes of whole song (for Sound::getLength)
|
|
exinfo.numchannels = Channels; // Number of channels in the sound
|
|
exinfo.defaultfrequency = SampleRate; // Default playback rate of sound
|
|
exinfo.format = Format;
|
|
|
|
FMOD_MODE flags = FMOD_OPENUSER | FMOD_LOOP_NORMAL | FMOD_SOFTWARE;
|
|
FMOD_RESULT result = pPlayer->pDevice->createSound(0, flags, &exinfo, &pSound);
|
|
if (result != FMOD_OK)
|
|
{
|
|
pSound = NULL;
|
|
pPlayer->LogError(result);
|
|
return;
|
|
}
|
|
unsigned int sl = 0;
|
|
pSound->getLength(&sl, FMOD_TIMEUNIT_PCMBYTES);
|
|
GASSERT(sl == SoundLength);
|
|
}
|
|
|
|
GSoundSampleFMODImplAux::~GSoundSampleFMODImplAux()
|
|
{
|
|
if (pBlockBuffer)
|
|
GFREE(pBlockBuffer);
|
|
}
|
|
|
|
GSoundChannelFMODImplAux* GSoundSampleFMODImplAux::Start(bool paused)
|
|
{
|
|
if (!pSound)
|
|
return NULL;
|
|
FMOD::Channel* pchannel;
|
|
|
|
FMOD_RESULT result;
|
|
result = pPlayer->pDevice->playSound(FMOD_CHANNEL_REUSE, pSound, paused, &pchannel);
|
|
if (result != FMOD_OK)
|
|
{
|
|
pPlayer->LogError(result);
|
|
return NULL;
|
|
}
|
|
BlockSize = (AUX_SOUND_READBUFLEN_MS * SampleRate/1000) * Channels * (Bits / 8);
|
|
if (pBlockBuffer)
|
|
GFREE(pBlockBuffer);
|
|
pBlockBuffer = (UByte*)GALLOC(BlockSize, GStat_Sound_Mem);
|
|
GSoundChannelFMODImplAux* pChannel = GNEW GSoundChannelFMODImplAux(pPlayer, this, pchannel);
|
|
pPlayer->AttachAuxStreamer(pChannel);
|
|
return pChannel;
|
|
}
|
|
|
|
void GSoundSampleFMODImplAux::ClearSoundBuffer()
|
|
{
|
|
FMOD_RESULT ret;
|
|
void* ptr1, *ptr2;
|
|
unsigned int len1, len2;
|
|
ret = pSound->lock(0, SoundLength, &ptr1, &ptr2, &len1, &len2);
|
|
if (ret == FMOD_OK)
|
|
{
|
|
GMemUtil::Set(ptr1, 0, len1);
|
|
ret = pSound->unlock(ptr1, ptr2, len1, len2);
|
|
GASSERT(ret == FMOD_OK);
|
|
GUNUSED(ret);
|
|
}
|
|
}
|
|
|
|
UInt GSoundSampleFMODImplAux::ReadAndFillSound()
|
|
{
|
|
FMOD_RESULT ret;
|
|
void* ptr1, *ptr2;
|
|
unsigned int len1, len2;
|
|
UInt got_bytes = pStreamer->GetPCMData(pBlockBuffer, BlockSize);
|
|
|
|
if (got_bytes < BlockSize)
|
|
GMemUtil::Set(&pBlockBuffer[got_bytes], 0, BlockSize-got_bytes );
|
|
|
|
if (Channels == 6)
|
|
{
|
|
// CRI: L,R,Ls,Rs,C,LFE
|
|
// FMOD: L,R,C,LFE,Ls,Rs
|
|
if (Format == FMOD_SOUND_FORMAT_PCMFLOAT)
|
|
{
|
|
Float tmp;
|
|
Float* psample = (Float*)pBlockBuffer;
|
|
for(size_t i = 0; i < got_bytes/4; i += 6)
|
|
{
|
|
tmp = psample[i+2]; psample[i+2] = psample[i+4]; psample[i+4] = tmp;
|
|
tmp = psample[i+3]; psample[i+3] = psample[i+5]; psample[i+5] = tmp;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UInt16 tmp;
|
|
UInt16* psample = (UInt16*)pBlockBuffer;
|
|
for(size_t i = 0; i < got_bytes/2; i += 6)
|
|
{
|
|
tmp = psample[i+2]; psample[i+2] = psample[i+4]; psample[i+4] = tmp;
|
|
tmp = psample[i+3]; psample[i+3] = psample[i+5]; psample[i+5] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = pSound->lock(FillPosition, BlockSize, &ptr1, &ptr2, &len1, &len2);
|
|
if (ret == FMOD_OK)
|
|
{
|
|
if (BlockSize > len1)
|
|
{
|
|
GMemUtil::Copy(ptr1, pBlockBuffer, len1);
|
|
if (BlockSize-len1 > len2)
|
|
{
|
|
GMemUtil::Copy(ptr2, pBlockBuffer+len1, len2);
|
|
}
|
|
else
|
|
{
|
|
GMemUtil::Copy(ptr2, pBlockBuffer+len1, BlockSize-len1);
|
|
GMemUtil::Set(((UByte*)ptr2)+(BlockSize-len1), 0, len2 - (BlockSize-len1));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GMemUtil::Copy(ptr1, pBlockBuffer, BlockSize);
|
|
GMemUtil::Set(((UByte*)ptr1)+BlockSize, 0, len1-BlockSize);
|
|
GMemUtil::Set(ptr2, 0, len2);
|
|
}
|
|
ret = pSound->unlock(ptr1, ptr2, len1, len2);
|
|
GASSERT(ret == FMOD_OK);
|
|
GUNUSED(ret);
|
|
}
|
|
|
|
TotatBytesRead += got_bytes;
|
|
FillPosition += got_bytes;
|
|
if (FillPosition >= SoundLength)
|
|
FillPosition -= SoundLength;
|
|
return got_bytes;
|
|
|
|
}
|
|
|
|
inline UInt GSoundSampleFMODImplAux::DistToFillBuffPos(UInt pos) const
|
|
{
|
|
UInt bytes_diff;
|
|
if (pos > FillPosition)
|
|
bytes_diff = (FillPosition + SoundLength) - pos;
|
|
else
|
|
bytes_diff = FillPosition - pos;
|
|
return bytes_diff;
|
|
}
|
|
|
|
bool GSoundSampleFMODImplAux::SeekData(Float seconds)
|
|
{
|
|
if (pStreamer && pStreamer->SetPosition(seconds))
|
|
{
|
|
TotatBytesRead = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
GSoundChannelFMODImplAux::GSoundChannelFMODImplAux(GSoundRendererFMODImpl* pp, GSoundSampleFMODImplAux* ps, FMOD::Channel* pc)
|
|
: GSoundChannelFMODImpl(pp, ps, pc), StartTick(GTimer::GetProfileTicks()), Starving(false), StartSecond(0.0)
|
|
{
|
|
ps->AddRef();
|
|
TotalTicks = 0;
|
|
NextFillTick = 0;
|
|
pChan->getPaused(&Paused);
|
|
if (Paused)
|
|
StopTick = StartTick;
|
|
else
|
|
{
|
|
ps->ReadAndFillSound();
|
|
StartTick = GTimer::GetProfileTicks();
|
|
NextFillTick = ps->GetTotalBytesReadInTicks();
|
|
}
|
|
}
|
|
|
|
GSoundChannelFMODImplAux::~GSoundChannelFMODImplAux()
|
|
{
|
|
pSample->Release();
|
|
}
|
|
|
|
Float GSoundChannelFMODImplAux::GetPosition()
|
|
{
|
|
GLock::Locker lock(&ChannelLock);
|
|
if (!Paused && !Starving)
|
|
{
|
|
GSoundSampleFMODImplAux* psample = (GSoundSampleFMODImplAux*)pSample;
|
|
UInt64 totalTicksRead = psample->GetTotalBytesReadInTicks();
|
|
UInt64 curtick = GTimer::GetProfileTicks();
|
|
UInt64 pos = TotalTicks + (curtick - StartTick);
|
|
if (pos > totalTicksRead)
|
|
{
|
|
Starving = true;
|
|
StopTick = curtick;
|
|
TotalTicks = totalTicksRead;
|
|
return StartSecond + TotalTicks / 1000000.f;
|
|
}
|
|
return StartSecond + pos / 1000000.f;
|
|
}
|
|
return StartSecond + TotalTicks / 1000000.f;
|
|
}
|
|
|
|
void GSoundChannelFMODImplAux::SetPosition(Float seconds)
|
|
{
|
|
GLock::Locker lock(&ChannelLock);
|
|
GSoundSampleFMODImplAux* psample = (GSoundSampleFMODImplAux*)pSample;
|
|
if (psample && psample->SeekData(seconds))
|
|
{
|
|
StartSecond = seconds;
|
|
TotalTicks = 0;
|
|
NextFillTick = 0;
|
|
StartTick = GTimer::GetProfileTicks();
|
|
UInt newPlayPos = psample->GetFillPosition();
|
|
psample->ReadAndFillSound();
|
|
pChan->setPosition(newPlayPos, FMOD_TIMEUNIT_PCMBYTES);
|
|
Starving = false;
|
|
NextFillTick = psample->GetTotalBytesReadInTicks();
|
|
}
|
|
}
|
|
|
|
void GSoundChannelFMODImplAux::Stop()
|
|
{
|
|
if (pPlayer)
|
|
pPlayer->DetachAuxStreamer(this);
|
|
StopTick = GTimer::GetProfileTicks();
|
|
}
|
|
|
|
void GSoundChannelFMODImplAux::Pause(bool pause)
|
|
{
|
|
GLock::Locker lock(&ChannelLock);
|
|
if (Paused == pause)
|
|
return;
|
|
GSoundChannelFMODImpl::Pause(pause);
|
|
Paused = pause;
|
|
if (Paused)
|
|
{
|
|
StopTick = GTimer::GetProfileTicks();
|
|
TotalTicks += StopTick - StartTick;
|
|
}
|
|
else
|
|
{
|
|
UInt64 curtick = GTimer::GetProfileTicks();
|
|
StartTick = curtick;
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
pPlayer->PulseEvent();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
Float GSoundChannelFMODImplAux::Update()
|
|
{
|
|
GLock::Locker lock(&ChannelLock);
|
|
GASSERT(pSample);
|
|
if (!IsPlaying())
|
|
return 0.5f;
|
|
if (Paused)
|
|
return 0.1f;
|
|
GSoundSampleFMODImplAux* psample = (GSoundSampleFMODImplAux*)pSample;
|
|
UInt64 totalTicksRead = psample->GetTotalBytesReadInTicks();
|
|
UInt64 curtick = GTimer::GetProfileTicks();
|
|
UInt64 pos = TotalTicks + (curtick - StartTick);
|
|
if (!Starving && pos > totalTicksRead)
|
|
{
|
|
Starving = true;
|
|
StopTick = curtick;
|
|
TotalTicks = totalTicksRead;
|
|
}
|
|
UInt dist = 0;
|
|
if (!Starving)
|
|
{
|
|
UInt fmodpos = 0;
|
|
FMOD_RESULT ret = pChan->getPosition(&fmodpos, FMOD_TIMEUNIT_PCMBYTES);
|
|
GASSERT(ret == FMOD_OK);
|
|
GUNUSED(ret);
|
|
GASSERT(fmodpos <= psample->SoundLength);
|
|
dist = psample->DistToFillBuffPos(fmodpos);
|
|
}
|
|
if (dist < psample->BlockSize / 3 )
|
|
{
|
|
UInt got_bytes = 0;
|
|
if (Starving)
|
|
{
|
|
psample->FillPosition = 0;
|
|
got_bytes = psample->ReadAndFillSound();
|
|
if (got_bytes > 0)
|
|
{
|
|
pChan->setPosition(0, FMOD_TIMEUNIT_PCMBYTES);
|
|
Starving = false;
|
|
StartTick = GTimer::GetProfileTicks();
|
|
pos = TotalTicks;
|
|
}
|
|
else
|
|
{
|
|
psample->ClearSoundBuffer();
|
|
return 0.02f;
|
|
}
|
|
}
|
|
else
|
|
got_bytes = psample->ReadAndFillSound();
|
|
|
|
if (got_bytes > 0)
|
|
{
|
|
NextFillTick = psample->GetTotalBytesReadInTicks();
|
|
Float t = (NextFillTick - pos) / 1000000.f *2/3;
|
|
return t < 0.02f ? 0.02f : t;
|
|
}
|
|
return 0.02f;
|
|
}
|
|
else
|
|
{
|
|
Float t = (NextFillTick - pos) / 1000000.f / 2;
|
|
return t < 0.02f ? 0.02f : t;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
void GSoundRendererFMODImpl::AttachAuxStreamer(GSoundChannelFMODImplAux* pchan)
|
|
{
|
|
if (!pchan || !pchan->pSample)
|
|
return;
|
|
GSoundSampleFMODImplAux* psample = (GSoundSampleFMODImplAux*)pchan->pSample;
|
|
if (!psample->pStreamer)
|
|
return;
|
|
AuxStreamsLock.Lock();
|
|
GArray<GSoundChannelFMODImplAux*>* pcont = AuxStreamers.Get(psample->pStreamer);
|
|
if (!pcont)
|
|
{
|
|
AuxStreamers.Set(psample->pStreamer, GArray<GSoundChannelFMODImplAux*>());
|
|
pcont = AuxStreamers.Get(psample->pStreamer);
|
|
}
|
|
pcont->PushBack(pchan);
|
|
pchan->AddRef();
|
|
AuxStreamsLock.Unlock();
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
if(ThreadedUpdate && !pUpdateThread)
|
|
{
|
|
StopThread = false;
|
|
GThread::CreateParams params(UpdateFunc, this, 32*1024);
|
|
#ifdef GFC_OS_WII
|
|
params.priority = GThread::AboveNormalPriority;
|
|
#else
|
|
params.priority = GThread::HighestPriority;
|
|
#endif
|
|
#ifdef GFC_OS_XBOX360
|
|
params.processor = ProcNumber;
|
|
#endif
|
|
pUpdateThread = *GNEW GThread(params);
|
|
pUpdateThread->Start();
|
|
}
|
|
Event.PulseEvent();
|
|
#endif
|
|
}
|
|
|
|
void GSoundRendererFMODImpl::DetachAuxStreamer(GSoundChannelFMODImplAux* pchan)
|
|
{
|
|
if (!pchan || !pchan->pSample)
|
|
return;
|
|
GSoundSampleFMODImplAux* psample = (GSoundSampleFMODImplAux*)pchan->pSample;
|
|
if (!psample->pStreamer)
|
|
return;
|
|
|
|
AuxStreamsLock.Lock();
|
|
GArray<GSoundChannelFMODImplAux*>* pcont = AuxStreamers.Get(psample->pStreamer);
|
|
bool found = false;
|
|
if (pcont)
|
|
{
|
|
size_t nulls = 0;
|
|
for(size_t i = 0; i < pcont->GetSize(); ++i)
|
|
{
|
|
if ((*pcont)[i] == pchan)
|
|
{
|
|
(*pcont)[i] = NULL;
|
|
found = true;
|
|
}
|
|
if ((*pcont)[i] == NULL)
|
|
nulls++;
|
|
}
|
|
if (nulls == pcont->GetSize())
|
|
AuxStreamers.Remove(psample->pStreamer);
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
if (AuxStreamers.GetSize() == 0 && ThreadedUpdate && pUpdateThread)
|
|
{
|
|
StopThread = true;
|
|
pUpdateThread = NULL;
|
|
Event.PulseEvent();
|
|
}
|
|
#endif
|
|
}
|
|
if (!found)
|
|
pchan = NULL;
|
|
|
|
AuxStreamsLock.Unlock();
|
|
if (pchan)
|
|
{
|
|
pchan->pChan->stop();
|
|
pchan->Release();
|
|
}
|
|
}
|
|
|
|
GSoundSampleFMODImpl* GSoundRendererFMODImpl::CreateSampleFromData(GSoundDataBase* psd)
|
|
{
|
|
if (!psd)
|
|
return NULL;
|
|
|
|
GLock::Locker lock(&SampleListLock);
|
|
|
|
if (psd->IsStreamSample() && !psd->IsFileSample())
|
|
{
|
|
GAppendableSoundData* pasd = (GAppendableSoundData*)psd;
|
|
GPtr<GSwfSoundStreamer> pstreamer = *GNEW GSwfSoundStreamer(this, pasd);
|
|
if (!pstreamer->CreateReader())
|
|
return NULL;
|
|
AuxStreamer::PCMFormat fmt;
|
|
UInt32 channels;
|
|
UInt32 samplerate;
|
|
if (!pstreamer->GetSoundFormat(&fmt, &channels,&samplerate))
|
|
return NULL;
|
|
return CreateSampleFromAuxStreamer(pstreamer, channels, samplerate, fmt);
|
|
}
|
|
|
|
GPtr<GSoundSampleFMODImpl> psample = *GNEW GSoundSampleFMODImpl(this);
|
|
bool created = false;
|
|
psample->pSoundData = psd;
|
|
if (psd->IsFileSample())
|
|
{
|
|
GSoundFile* ps = (GSoundFile*) psd;
|
|
created = (psample->CreateSubSound(ps, &psample->pSound) == FMOD_OK);
|
|
}
|
|
else
|
|
{
|
|
GSoundData* ps = (GSoundData*) psd;
|
|
created = (psample->CreateSubSound(ps, &psample->pSound) == FMOD_OK);
|
|
}
|
|
if (created)
|
|
{
|
|
psample->AddRef();
|
|
return psample;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
GSoundSampleFMODImpl* GSoundRendererFMODImpl::CreateSampleFromAuxStreamer(
|
|
AuxStreamer* pstreamder, UInt32 channels, UInt32 samplerate, AuxStreamer::PCMFormat fmt)
|
|
{
|
|
GLock::Locker lock(&SampleListLock);
|
|
GSoundSampleFMODImplAux* psample = new GSoundSampleFMODImplAux(this, pstreamder, fmt, channels, samplerate);
|
|
return psample;
|
|
}
|
|
|
|
GSoundSampleFMODImpl* GSoundRendererFMODImpl::CreateSampleFromFile(const char* fname, bool streaming)
|
|
{
|
|
GSoundSampleFMODImpl* psample = NULL;
|
|
{
|
|
GLock::Locker lock(&SampleListLock);
|
|
psample = new GSoundSampleFMODImpl(this);
|
|
}
|
|
FMOD_RESULT result;
|
|
if (streaming)
|
|
result = pDevice->createStream(fname,FMOD_SOFTWARE | FMOD_LOOP_OFF | FMOD_2D, NULL, &(psample->pSound));
|
|
else
|
|
result = pDevice->createSound(fname,FMOD_SOFTWARE | FMOD_LOOP_OFF | FMOD_2D, NULL, &(psample->pSound));
|
|
if (result != FMOD_OK)
|
|
{
|
|
LogError(result);
|
|
psample->pSound = NULL;
|
|
psample->Release();
|
|
return NULL;
|
|
}
|
|
return psample;
|
|
}
|
|
|
|
bool GSoundRendererFMODImpl::Initialize(FMOD::System* pd, bool call_fmod_update, bool threaded_update
|
|
#ifdef GFC_OS_XBOX360
|
|
, int processor_core
|
|
#endif
|
|
)
|
|
{
|
|
CallFMODUpdate = call_fmod_update;
|
|
ThreadedUpdate = threaded_update;
|
|
#ifdef GFC_OS_XBOX360
|
|
ProcNumber = processor_core;
|
|
#endif
|
|
|
|
pDevice = pd;
|
|
if (pDevice)
|
|
{
|
|
int rate = 0;
|
|
pDevice->getSoftwareFormat(&rate,0,0,0,0,0);
|
|
SystemBitRate = rate * 1.0f;
|
|
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
int result = FMOD::EventSystem_Create(&pEventSys);
|
|
if (result == FMOD_OK)
|
|
{
|
|
FMOD::NetEventSystem_Init(pEventSys);
|
|
result = pEventSys->init(64, FMOD_INIT_NORMAL, 0, FMOD_EVENT_INIT_NORMAL);
|
|
if (result != FMOD_OK)
|
|
{
|
|
pEventSys->release();
|
|
FMOD::NetEventSystem_Shutdown();
|
|
pEventSys = NULL;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
return pDevice != NULL;
|
|
}
|
|
|
|
void GSoundRendererFMODImpl::xFinalize()
|
|
{
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
if (pEventSys)
|
|
{
|
|
pEventSys->release();
|
|
FMOD::NetEventSystem_Shutdown();
|
|
pEventSys = NULL;
|
|
}
|
|
#endif
|
|
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
if (pUpdateThread)
|
|
{
|
|
StopThread = true;
|
|
Event.PulseEvent();
|
|
pUpdateThread->Wait();
|
|
}
|
|
#endif
|
|
{
|
|
GLock::Locker guard(&SampleListLock);
|
|
while (SampleList.pFirst != &SampleList)
|
|
SampleList.pFirst->ReleaseResource();
|
|
}
|
|
}
|
|
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
SInt GSoundRendererFMODImpl::UpdateFunc(GThread*, void* h)
|
|
{
|
|
GSoundRendererFMODImpl* pRenderer = (GSoundRendererFMODImpl*)h;
|
|
UInt wait_time = 2000; // GFC_WAIT_INFINITE;
|
|
while(1)
|
|
{
|
|
pRenderer->Event.Wait(wait_time);
|
|
if (pRenderer->StopThread)
|
|
break;
|
|
GLock::Locker lock(&pRenderer->AuxStreamsLock);
|
|
wait_time = (UInt)(pRenderer->UpdateAuxStreams() * 1000);
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
Float GSoundRendererFMODImpl::UpdateAuxStreams()
|
|
{
|
|
Float nextcall = 0.5f;
|
|
for (AuxStreamersType::Iterator it = AuxStreamers.Begin(); it != AuxStreamers.End(); ++it)
|
|
{
|
|
GArray<GSoundChannelFMODImplAux*>& cont = it->Second;
|
|
for(size_t i = 0; i < cont.GetSize(); ++i)
|
|
{
|
|
GSoundChannelFMODImplAux* pchan = cont[i];
|
|
if (pchan)
|
|
{
|
|
Float t = pchan->Update();
|
|
if (t < nextcall) nextcall = t;
|
|
}
|
|
}
|
|
}
|
|
return nextcall;
|
|
}
|
|
|
|
Float GSoundRendererFMODImpl::Update()
|
|
{
|
|
Float nextcall = 0.5f;
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
if (!pUpdateThread)
|
|
nextcall = UpdateAuxStreams();
|
|
#else
|
|
nextcall = UpdateAuxStreams();
|
|
#endif
|
|
if (CallFMODUpdate)
|
|
{
|
|
pDevice->update();
|
|
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
if (pEventSys) {
|
|
pEventSys->update();
|
|
FMOD::NetEventSystem_Update();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return nextcall;
|
|
}
|
|
|
|
void GSoundRendererFMODImpl::Mute(bool mute)
|
|
{
|
|
FMOD::ChannelGroup *pcg;
|
|
FMOD_RESULT result;
|
|
result = pDevice->getMasterChannelGroup(&pcg);
|
|
if (result == FMOD_OK)
|
|
{
|
|
pcg->setMute(mute);
|
|
}
|
|
}
|
|
|
|
void GSoundRendererFMODImpl::LogError(FMOD_RESULT result)
|
|
{
|
|
if (result != FMOD_OK && result != FMOD_ERR_INVALID_HANDLE && result != FMOD_ERR_CHANNEL_STOLEN)
|
|
fprintf(stderr,"FMOD error! (%d) %s\n", result, FMOD_ErrorString(result));
|
|
}
|
|
|
|
GSoundChannelFMODImpl* GSoundRendererFMODImpl::PlaySample(GSoundSample* ps, bool paused)
|
|
{
|
|
GSoundSampleFMODImpl* psample = (GSoundSampleFMODImpl *) ps;
|
|
if (!psample)
|
|
return NULL;
|
|
return psample->Start(paused);
|
|
}
|
|
|
|
GSoundRendererFMODImpl::GSoundRendererFMODImpl() :
|
|
pDevice(NULL),
|
|
#if defined(GFC_SOUND_FMOD_DESIGNER) && (defined(GFC_OS_WIN32) || defined(GFC_OS_MAC))
|
|
pEventSys(NULL),
|
|
#endif
|
|
CallFMODUpdate(false), SystemBitRate(0.0f)
|
|
{
|
|
#ifndef GFC_NO_THREADSUPPORT
|
|
StopThread = false;
|
|
#endif
|
|
}
|
|
|
|
Float GSoundRendererFMODImpl::GetMasterVolume()
|
|
{
|
|
FMOD_RESULT result;
|
|
float v = 1.0;
|
|
FMOD::ChannelGroup* pcgrp;
|
|
result = pDevice->getMasterChannelGroup(&pcgrp);
|
|
if (result == FMOD_OK)
|
|
{
|
|
result = pcgrp->getVolume(&v);
|
|
if (result == FMOD_OK)
|
|
return v;
|
|
else
|
|
LogError(result);
|
|
}
|
|
else
|
|
LogError(result);
|
|
return v;
|
|
}
|
|
|
|
void GSoundRendererFMODImpl::SetMasterVolume(Float volume)
|
|
{
|
|
FMOD_RESULT result;
|
|
FMOD::ChannelGroup* pcgrp;
|
|
result = pDevice->getMasterChannelGroup(&pcgrp);
|
|
if (result == FMOD_OK)
|
|
{
|
|
result = pcgrp->setVolume(volume);
|
|
LogError(result);
|
|
}
|
|
else
|
|
LogError(result);
|
|
}
|
|
|
|
bool GSoundRendererFMODImpl::GetRenderCaps(UInt32 *pcaps)
|
|
{
|
|
if (!pcaps)
|
|
return false;
|
|
*pcaps = 0;
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
GSoundSampleFMODImpl::GSoundSampleFMODImpl(GSoundRendererFMODImpl* prenderer)
|
|
: GSoundSampleImplNode(&prenderer->SampleList)
|
|
{
|
|
pPlayer = prenderer;
|
|
pSound = NULL;
|
|
}
|
|
|
|
GSoundSampleFMODImpl::~GSoundSampleFMODImpl()
|
|
{
|
|
ReleaseFMODObjects();
|
|
if (!pPlayer)
|
|
return;
|
|
GLock::Locker guard(&pPlayer->SampleListLock);
|
|
if (pFirst)
|
|
RemoveNode();
|
|
}
|
|
|
|
void GSoundSampleFMODImpl::ReleaseFMODObjects()
|
|
{
|
|
if (pSound)
|
|
pSound->release();
|
|
pSound = NULL;
|
|
}
|
|
|
|
void GSoundSampleFMODImpl::ReleaseResource()
|
|
{
|
|
pPlayer = 0;
|
|
if (AddRef_NotZero())
|
|
{
|
|
ReleaseFMODObjects();
|
|
if (pNext) // We may have been released by user
|
|
RemoveNode();
|
|
Release();
|
|
} else {
|
|
if (pNext) // We may have been released by user
|
|
RemoveNode();
|
|
}
|
|
}
|
|
|
|
Float GSoundSampleFMODImpl::GetDuration() const
|
|
{
|
|
if (pSound)
|
|
{
|
|
unsigned slen = 0;
|
|
FMOD_RESULT result = pSound->getLength(&slen, FMOD_TIMEUNIT_MS);
|
|
if (result == FMOD_OK)
|
|
return Float(slen/1000.f);
|
|
pPlayer->LogError(result);
|
|
}
|
|
return 0.0f;
|
|
}
|
|
SInt GSoundSampleFMODImpl::GetBytesTotal() const
|
|
{
|
|
if (pSound)
|
|
{
|
|
unsigned slen = 0;
|
|
FMOD_RESULT result = pSound->getLength(&slen, FMOD_TIMEUNIT_RAWBYTES);
|
|
if (result == FMOD_OK)
|
|
return slen;
|
|
pPlayer->LogError(result);
|
|
}
|
|
return 0;
|
|
}
|
|
SInt GSoundSampleFMODImpl::GetBytesLoaded() const
|
|
{
|
|
if (pSound)
|
|
{
|
|
unsigned slen = 0;
|
|
FMOD_RESULT result = pSound->getLength(&slen, FMOD_TIMEUNIT_RAWBYTES);
|
|
if (result == FMOD_OK)
|
|
return slen;
|
|
pPlayer->LogError(result);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
inline GSoundRenderer* GSoundSampleFMODImpl::GetSoundRenderer() const
|
|
{
|
|
return pPlayer;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
/*
|
|
struct FilePos : GNewOverrideBase<GStat_Sound_Mem>
|
|
{
|
|
UInt pos;
|
|
};
|
|
|
|
FMOD_RESULT F_CALLBACK OpenFile(const char *sd, int unicode, unsigned int *filesize, void **handle, void **userdata)
|
|
{
|
|
GUNUSED2(unicode, userdata);
|
|
GSoundData* psoundData = (GSoundData*)sd;
|
|
*filesize = psoundData->GetDataSize();
|
|
FilePos* fp = new FilePos;
|
|
fp->pos = 0;
|
|
*handle = fp;
|
|
*userdata = psoundData;
|
|
return FMOD_OK;
|
|
}
|
|
|
|
FMOD_RESULT F_CALLBACK CloseFile(void *handle, void *userdata)
|
|
{
|
|
GUNUSED(userdata);
|
|
if (!handle)
|
|
{
|
|
return FMOD_ERR_INVALID_PARAM;
|
|
}
|
|
FilePos* fp = (FilePos*) handle;
|
|
delete fp;
|
|
return FMOD_OK;
|
|
}
|
|
|
|
FMOD_RESULT F_CALLBACK ReadFile(void *handle, void *buffer, unsigned int sizebytes, unsigned int *bytesread, void *userdata)
|
|
{
|
|
if (!handle)
|
|
{
|
|
return FMOD_ERR_INVALID_PARAM;
|
|
}
|
|
if (bytesread)
|
|
{
|
|
FilePos* fp = (FilePos*) handle;
|
|
GSoundData* psoundData = (GSoundData*)userdata;
|
|
if ((fp->pos + sizebytes) > psoundData->GetDataSize())
|
|
sizebytes = psoundData->GetDataSize() - fp->pos;
|
|
if (sizebytes == 0)
|
|
{
|
|
*bytesread = 0;
|
|
return FMOD_ERR_FILE_EOF;
|
|
}
|
|
memcpy(buffer, psoundData->GetData() + fp->pos, sizebytes);
|
|
fp->pos += sizebytes;
|
|
*bytesread = sizebytes;
|
|
}
|
|
|
|
return FMOD_OK;
|
|
}
|
|
|
|
FMOD_RESULT F_CALLBACK SeekInFile(void *handle, unsigned int pos, void *userdata)
|
|
{
|
|
if (!handle)
|
|
{
|
|
return FMOD_ERR_INVALID_PARAM;
|
|
}
|
|
FilePos* fp = (FilePos*) handle;
|
|
GSoundData* psoundData = (GSoundData*)userdata;
|
|
|
|
if (pos >= psoundData->GetDataSize())
|
|
return FMOD_ERR_FILE_COULDNOTSEEK;
|
|
fp->pos = pos;
|
|
|
|
return FMOD_OK;
|
|
}
|
|
|
|
FMOD_RESULT GSoundSampleFMODImpl::CreateSubSound(GSoundData* psd, FMOD::Sound** psound)
|
|
{
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
GMemUtil::Set(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
|
exinfo.useropen = &OpenFile;
|
|
exinfo.userclose= &CloseFile;
|
|
exinfo.userread = &ReadFile;
|
|
exinfo.userseek = &SeekInFile;
|
|
int flags = FMOD_LOWMEM | FMOD_SOFTWARE | FMOD_IGNORETAGS;
|
|
switch (psd->GetFormat() & GSoundData::Sample_Format)
|
|
{
|
|
case GSoundData::Sample_PCM:
|
|
exinfo.format = (psd->GetFormat() & 0x7) == 2 ? FMOD_SOUND_FORMAT_PCM16 : FMOD_SOUND_FORMAT_PCM8;
|
|
exinfo.defaultfrequency = psd->GetRate();
|
|
exinfo.numchannels = (psd->GetFormat() & GSoundData::Sample_Stereo) ? 2 : 1;
|
|
flags |= FMOD_OPENRAW;
|
|
break;
|
|
case GSoundData::Sample_MP3:
|
|
exinfo.format = FMOD_SOUND_FORMAT_MPEG;
|
|
break;
|
|
default:
|
|
return FMOD_ERR_FORMAT;
|
|
}
|
|
FMOD_RESULT result = pPlayer->pDevice->createSound((const char*)psd, flags, &exinfo, psound);
|
|
return result;
|
|
}
|
|
*/
|
|
|
|
FMOD_RESULT GSoundSampleFMODImpl::CreateSubSound(GSoundData* psd, FMOD::Sound** psound)
|
|
{
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
GMemUtil::Set(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
|
int flags = FMOD_LOWMEM | FMOD_SOFTWARE | FMOD_IGNORETAGS | FMOD_OPENMEMORY_POINT;
|
|
switch (psd->GetFormat() & GSoundData::Sample_Format)
|
|
{
|
|
case GSoundData::Sample_PCM:
|
|
exinfo.format = (psd->GetFormat() & 0x7) == 2 ? FMOD_SOUND_FORMAT_PCM16 : FMOD_SOUND_FORMAT_PCM8;
|
|
exinfo.defaultfrequency = psd->GetRate();
|
|
exinfo.numchannels = (psd->GetFormat() & GSoundData::Sample_Stereo) ? 2 : 1;
|
|
flags |= FMOD_OPENRAW;
|
|
break;
|
|
case GSoundData::Sample_MP3:
|
|
flags |= FMOD_CREATECOMPRESSEDSAMPLE;
|
|
exinfo.format = FMOD_SOUND_FORMAT_MPEG;
|
|
break;
|
|
default:
|
|
return FMOD_ERR_FORMAT;
|
|
}
|
|
exinfo.length = psd->GetDataSize();
|
|
FMOD_RESULT result = pPlayer->pDevice->createSound((const char*)psd->GetData(), flags, &exinfo, psound);
|
|
return result;
|
|
}
|
|
|
|
FMOD_RESULT GSoundSampleFMODImpl::CreateSubSound(GAppendableSoundData* psd, FMOD::Sound** psound)
|
|
{
|
|
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
GMemUtil::Set(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
|
exinfo.useropen = &DecodeOpen;
|
|
exinfo.userclose= &DecodeClose;
|
|
exinfo.userread = &DecodeRead;
|
|
exinfo.userseek = &DecodeSeek;
|
|
exinfo.decodebuffersize = 1024 * 4;
|
|
int flags = FMOD_LOWMEM | FMOD_SOFTWARE | FMOD_CREATESTREAM | FMOD_IGNORETAGS;
|
|
switch (psd->GetFormat() & GSoundData::Sample_Format)
|
|
{
|
|
case GSoundData::Sample_PCM:
|
|
exinfo.format = (psd->GetFormat() & 0x7) == 2 ? FMOD_SOUND_FORMAT_PCM16 : FMOD_SOUND_FORMAT_PCM8;
|
|
exinfo.defaultfrequency = psd->GetRate();
|
|
exinfo.numchannels = (psd->GetFormat() & GSoundData::Sample_Stereo) ? 2 : 1;
|
|
flags |= FMOD_OPENRAW;
|
|
break;
|
|
case GSoundData::Sample_MP3:
|
|
exinfo.defaultfrequency = psd->GetRate();
|
|
exinfo.format = FMOD_SOUND_FORMAT_MPEG;
|
|
break;
|
|
default:
|
|
return FMOD_ERR_FORMAT;
|
|
}
|
|
exinfo.initialseekposition = psd->GetSeekSample();
|
|
exinfo.initialseekpostype = FMOD_TIMEUNIT_PCM;
|
|
FMOD_RESULT result = pPlayer->pDevice->createSound((const char*)psd, flags, &exinfo, psound);
|
|
return result;
|
|
}
|
|
|
|
FMOD_RESULT GSoundSampleFMODImpl::CreateSubSound(GSoundFile* psd, FMOD::Sound** psound)
|
|
{
|
|
int flags = FMOD_SOFTWARE;
|
|
|
|
if (psd->IsStreamSample())
|
|
flags |= FMOD_CREATESTREAM;
|
|
else
|
|
flags |= FMOD_ACCURATETIME;
|
|
|
|
FMOD_RESULT result = pPlayer->pDevice->createSound(psd->GetFileName(), flags, NULL, psound);
|
|
if (result != FMOD_OK)
|
|
{
|
|
pPlayer->LogError(result);
|
|
*psound = NULL;
|
|
return result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
GSoundChannelFMODImpl* GSoundSampleFMODImpl::Start(bool paused)
|
|
{
|
|
if (pSound)
|
|
{
|
|
UInt latency = pSoundData ? pSoundData->GetSeekSample() : 0;
|
|
UInt scount = pSoundData ? pSoundData->GetSampleCount() : 0;
|
|
if (scount == 0)
|
|
pSound->getLength(&scount, FMOD_TIMEUNIT_PCM);
|
|
|
|
Float sample_rate;
|
|
pSound->getDefaults(&sample_rate,NULL,NULL,NULL);
|
|
|
|
FMOD::Channel* pchan;
|
|
FMOD_RESULT r;
|
|
r = pPlayer->pDevice->playSound(FMOD_CHANNEL_FREE, pSound, true, &pchan);
|
|
if (r == FMOD_OK)
|
|
{
|
|
UInt hi = 0;
|
|
UInt lo = 0;
|
|
r = pPlayer->pDevice->getDSPClock(&hi, &lo);
|
|
scount = UInt(scount * pPlayer->SystemBitRate/sample_rate);
|
|
FMOD_64BIT_ADD(hi, lo, 0, scount);
|
|
r = pchan->setDelay(FMOD_DELAYTYPE_DSPCLOCK_END, hi, lo);
|
|
if (latency > 0)
|
|
{
|
|
r = pchan->setPosition(latency, FMOD_TIMEUNIT_PCM);
|
|
if (r == FMOD_OK)
|
|
r = pchan->setPaused(paused);
|
|
}
|
|
}
|
|
if (r != FMOD_OK)
|
|
{
|
|
pPlayer->LogError(r);
|
|
return NULL;
|
|
}
|
|
|
|
GSoundChannelFMODImpl* pchannel = GNEW GSoundChannelFMODImpl(pPlayer, this, pchan);
|
|
return pchannel;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
inline GSoundRenderer* GSoundChannelFMODImpl::GetSoundRenderer() const
|
|
{
|
|
return pPlayer;
|
|
}
|
|
|
|
GSoundChannelFMODImpl::GSoundChannelFMODImpl(GSoundRendererFMODImpl* pp, GSoundSampleFMODImpl* ps, FMOD::Channel* pc)
|
|
: GSoundChannelImplNode(&pp->SampleList)
|
|
{
|
|
pPlayer = pp;
|
|
pSample = ps;
|
|
pChan = pc;
|
|
|
|
pChan->setUserData(this);
|
|
pChan->setCallback(CallBackFunc);
|
|
}
|
|
GSoundChannelFMODImpl::~GSoundChannelFMODImpl()
|
|
{
|
|
ReleaseFMODObjects();
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::ReleaseResource()
|
|
{
|
|
if (GetRefCount() > 0)
|
|
ReleaseFMODObjects();
|
|
pPlayer = NULL;
|
|
if (pNext)
|
|
RemoveNode();
|
|
}
|
|
void GSoundChannelFMODImpl::ReleaseFMODObjects()
|
|
{
|
|
/*
|
|
for (GHash<FMOD_SYNCPOINT*, Transform>::ConstIterator i = Tranforms.Begin();
|
|
i != Tranforms.End(); ++i)
|
|
{
|
|
FMOD_SYNCPOINT* psync = i->First;
|
|
FMOD_RESULT r = pSample && pSample->pSound ? pSample->pSound->deleteSyncPoint(psync) : FMOD_OK;
|
|
GASSERT(r == FMOD_OK);
|
|
}
|
|
*/
|
|
if (pChan)
|
|
{
|
|
Stop();
|
|
pChan->setCallback(NULL);
|
|
pChan->setUserData(NULL);
|
|
pChan = NULL;
|
|
}
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::Stop()
|
|
{
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->stop();
|
|
pPlayer->LogError(r);
|
|
pChan = NULL;
|
|
}
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::Pause(bool pause)
|
|
{
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->setPaused(pause);
|
|
pPlayer->LogError(r);
|
|
}
|
|
}
|
|
|
|
bool GSoundChannelFMODImpl::IsPlaying() const
|
|
{
|
|
bool res = false;
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->isPlaying(&res);
|
|
pPlayer->LogError(r);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::SetPosition(Float seconds)
|
|
{
|
|
UInt ms = UInt(seconds * 1000.0f);
|
|
if (pChan)
|
|
{
|
|
UInt latency = pSample && pSample->pSoundData ?
|
|
pSample->pSoundData->GetSeekSample() * 1000 / pSample->pSoundData->GetRate():
|
|
0;
|
|
FMOD_RESULT r = pChan->setPosition(ms+latency, FMOD_TIMEUNIT_MS);
|
|
pPlayer->LogError(r);
|
|
}
|
|
}
|
|
|
|
Float GSoundChannelFMODImpl::GetPosition()
|
|
{
|
|
UInt pos = 0;
|
|
UInt latency = 0;
|
|
if (pChan)
|
|
{
|
|
latency = pSample && pSample->pSoundData ?
|
|
pSample->pSoundData->GetSeekSample() * 1000 / pSample->pSoundData->GetRate():
|
|
0;
|
|
FMOD_RESULT r;
|
|
r = pChan->getPosition(&pos, FMOD_TIMEUNIT_MS);
|
|
pPlayer->LogError(r);
|
|
UInt pcmbytes = 0;
|
|
r = pChan->getPosition(&pcmbytes, FMOD_TIMEUNIT_PCMBYTES);
|
|
pPlayer->LogError(r);
|
|
|
|
}
|
|
return Float((pos-latency)/1000.0f);
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::Loop(SInt count, Float start, Float end)
|
|
{
|
|
GASSERT(pSample);
|
|
if (!pChan || count == 0)
|
|
return;
|
|
|
|
FMOD_RESULT result;
|
|
if (count > 1)
|
|
{
|
|
pChan->setMode(FMOD_LOOP_NORMAL);
|
|
pChan->setLoopCount(count);
|
|
}
|
|
UInt seekpos = pSample->pSoundData ? pSample->pSoundData->GetSeekSample() : 0;
|
|
UInt scount = pSample->pSoundData ? pSample->pSoundData->GetSampleCount() : 0;
|
|
UInt slen = 0;
|
|
pSample->pSound->getLength(&slen, FMOD_TIMEUNIT_PCM);
|
|
Float sample_rate;
|
|
pSample->pSound->getDefaults(&sample_rate,NULL,NULL,NULL);
|
|
|
|
UInt start_pcm = start > 0.0f? UInt(start * sample_rate) : seekpos;
|
|
UInt end_pcm = UInt(end * sample_rate);
|
|
if (end_pcm == 0 || end_pcm > slen - 1)
|
|
{
|
|
if (scount > 0)
|
|
end_pcm = start_pcm + scount;
|
|
else
|
|
end_pcm = slen - 1;
|
|
}
|
|
result = pChan->setPosition(start_pcm, FMOD_TIMEUNIT_PCM);
|
|
result = pChan->setLoopPoints(start_pcm, FMOD_TIMEUNIT_PCM, end_pcm, FMOD_TIMEUNIT_PCM);
|
|
|
|
UInt hi = 0;
|
|
UInt lo = 0;
|
|
result = pPlayer->pDevice->getDSPClock(&hi, &lo);
|
|
slen = UInt((end_pcm - start_pcm) * count * (pPlayer->SystemBitRate/sample_rate));
|
|
FMOD_64BIT_ADD(hi, lo, 0, slen);
|
|
result = pChan->setDelay(FMOD_DELAYTYPE_DSPCLOCK_END, hi, lo);
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::SetVolume(Float volume)
|
|
{
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->setVolume(volume);
|
|
pPlayer->LogError(r);
|
|
}
|
|
}
|
|
Float GSoundChannelFMODImpl::GetVolume()
|
|
{
|
|
float v = 1.0f;
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->getVolume(&v);
|
|
pPlayer->LogError(r);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
Float GSoundChannelFMODImpl::GetPan()
|
|
{
|
|
float pan = 0.0f;
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->getPan(&pan);
|
|
pPlayer->LogError(r);
|
|
}
|
|
return pan;
|
|
}
|
|
void GSoundChannelFMODImpl::SetPan(Float pan)
|
|
{
|
|
if (pChan)
|
|
{
|
|
FMOD_RESULT r = pChan->setPan(pan);
|
|
pPlayer->LogError(r);
|
|
}
|
|
}
|
|
|
|
#define deltaT 0.1f
|
|
#define deltaV 0.1f
|
|
|
|
static inline void s_SetChannelPan(FMOD::Channel* pchan, const GSoundChannel::Transform& transform)
|
|
{
|
|
FMOD_RESULT r;
|
|
r = pchan->setVolume((transform.LeftVolume + transform.RightVolume)/2);
|
|
float levels[2];
|
|
levels[0] = transform.LeftVolume;
|
|
levels[1] = transform.RightVolume;
|
|
float pan = fabs(transform.LeftVolume - transform.RightVolume);
|
|
if (transform.LeftVolume > transform.RightVolume)
|
|
pan *= -1;
|
|
r = pchan->setPan(pan);
|
|
GUNUSED(r);
|
|
}
|
|
|
|
void GSoundChannelFMODImpl::SetTransforms(const GArray<Transform>& transforms)
|
|
{
|
|
UPInt size = transforms.GetSize();
|
|
if (size == 0)
|
|
return;
|
|
if (!pSample || !pSample->pSound)
|
|
return;
|
|
|
|
pChan->setCallback(CallBackFunc);
|
|
for (UInt i = 0; i < size; ++i)
|
|
{
|
|
if (i == 0)
|
|
{
|
|
s_SetChannelPan(pChan, transforms[0]);
|
|
continue;
|
|
}
|
|
Float ar = (transforms[i-1].RightVolume - transforms[i].RightVolume) /
|
|
(transforms[i-1].Position - transforms[i].Position);
|
|
Float al = (transforms[i-1].LeftVolume - transforms[i].LeftVolume) /
|
|
(transforms[i-1].Position - transforms[i].Position);
|
|
Float br = transforms[i-1].RightVolume - ar * transforms[i-1].Position;
|
|
Float bl = transforms[i-1].LeftVolume - al * transforms[i-1].Position;
|
|
|
|
bool done = false;
|
|
for(UInt j = 1; !done ; j++)
|
|
{
|
|
Transform tr;
|
|
if (transforms[i].Position < transforms[i-1].Position + deltaT * j)
|
|
{
|
|
tr = transforms[i];
|
|
done = true;
|
|
}
|
|
else
|
|
{
|
|
tr.Position = transforms[i-1].Position + deltaT * j;
|
|
tr.RightVolume = ar * tr.Position + br;
|
|
tr.LeftVolume = al * tr.Position + bl;
|
|
}
|
|
FMOD_SYNCPOINT* psync;
|
|
FMOD_RESULT r = pSample->pSound->addSyncPoint(UInt(tr.Position * 1000.0f), FMOD_TIMEUNIT_MS, "", &psync);
|
|
if (r == FMOD_OK)
|
|
{
|
|
Tranforms.Add(psync, tr);
|
|
}
|
|
else
|
|
pPlayer->LogError(r);
|
|
}
|
|
}
|
|
}
|
|
|
|
FMOD_RESULT F_CALLBACK GSoundChannelFMODImpl::CallBackFunc(
|
|
FMOD_CHANNEL* pchan, FMOD_CHANNEL_CALLBACKTYPE cb, void* cmddata1, void *cmddata2)
|
|
{
|
|
GUNUSED(cmddata2);
|
|
void *vp;
|
|
((FMOD::Channel *)pchan)->getUserData(&vp);
|
|
if (!vp)
|
|
return FMOD_OK;
|
|
|
|
GSoundChannelFMODImpl* pChan = (GSoundChannelFMODImpl *)vp;
|
|
FMOD_RESULT r;
|
|
|
|
if (cb == FMOD_CHANNEL_CALLBACKTYPE_SYNCPOINT)
|
|
{
|
|
if (pChan && pChan->pSample && pChan->pSample->pSound)
|
|
{
|
|
FMOD_SYNCPOINT* psync;
|
|
r = pChan->pSample->pSound->getSyncPoint((int)(SPInt)cmddata1, &psync);
|
|
if (r == FMOD_OK)
|
|
{
|
|
GSoundChannel::Transform* tr = pChan->Tranforms.Get(psync);
|
|
if (tr)
|
|
s_SetChannelPan(pChan->pChan, *tr);
|
|
}
|
|
else
|
|
pChan->pPlayer->LogError(r);
|
|
}
|
|
}
|
|
else if (cb == FMOD_CHANNEL_CALLBACKTYPE_END)
|
|
{
|
|
}
|
|
return FMOD_OK;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
GSoundRendererFMOD* GCDECL GSoundRendererFMOD::CreateSoundRenderer()
|
|
{
|
|
return new GSoundRendererFMODImpl;
|
|
}
|
|
|
|
#endif // GFC_NO_SOUND
|
|
|
|
#endif
|