ddnet/src/engine/client/sound.cpp
Robert Müller efa069ef80 Ensure sample indices are initialized also with sound disabled
The assertion of #8262 can be reproduced when sound is disabled or failed to be initialized, as the sample indices where not being initialized properly in these cases. It is still necessary to initialized them so sounds can be loaded in the editor also when sound is disabled.

The potential thread-safety issues of the `CSound::AllocSample` function are not yet resolved so the issue remains open.
2024-06-21 17:38:16 +02:00

985 lines
23 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <SDL.h>
#include <base/math.h>
#include <base/system.h>
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/storage.h>
#include "sound.h"
#if defined(CONF_VIDEORECORDER)
#include <engine/shared/video.h>
#endif
extern "C" {
#include <opusfile.h>
#include <wavpack.h>
}
#include <cmath>
static constexpr int SAMPLE_INDEX_USED = -2;
static constexpr int SAMPLE_INDEX_FULL = -1;
void CSound::Mix(short *pFinalOut, unsigned Frames)
{
Frames = minimum(Frames, m_MaxFrames);
mem_zero(m_pMixBuffer, Frames * 2 * sizeof(int));
// acquire lock while we are mixing
m_SoundLock.lock();
const int MasterVol = m_SoundVolume.load(std::memory_order_relaxed);
for(auto &Voice : m_aVoices)
{
if(!Voice.m_pSample)
continue;
// mix voice
int *pOut = m_pMixBuffer;
const int Step = Voice.m_pSample->m_Channels; // setup input sources
short *pInL = &Voice.m_pSample->m_pData[Voice.m_Tick * Step];
short *pInR = &Voice.m_pSample->m_pData[Voice.m_Tick * Step + 1];
unsigned End = Voice.m_pSample->m_NumFrames - Voice.m_Tick;
int VolumeR = round_truncate(Voice.m_pChannel->m_Vol * (Voice.m_Vol / 255.0f));
int VolumeL = VolumeR;
// make sure that we don't go outside the sound data
if(Frames < End)
End = Frames;
// check if we have a mono sound
if(Voice.m_pSample->m_Channels == 1)
pInR = pInL;
// volume calculation
if(Voice.m_Flags & ISound::FLAG_POS && Voice.m_pChannel->m_Pan)
{
// TODO: we should respect the channel panning value
const int dx = Voice.m_X - m_CenterX.load(std::memory_order_relaxed);
const int dy = Voice.m_Y - m_CenterY.load(std::memory_order_relaxed);
float FalloffX = 0.0f;
float FalloffY = 0.0f;
int RangeX = 0; // for panning
bool InVoiceField = false;
switch(Voice.m_Shape)
{
case ISound::SHAPE_CIRCLE:
{
const float Radius = Voice.m_Circle.m_Radius;
RangeX = Radius;
// dx and dy can be larger than 46341 and thus the calculation would go beyond the limits of a integer,
// therefore we cast them into float
const int Dist = (int)length(vec2(dx, dy));
if(Dist < Radius)
{
InVoiceField = true;
// falloff
int FalloffDistance = Radius * Voice.m_Falloff;
if(Dist > FalloffDistance)
FalloffX = FalloffY = (Radius - Dist) / (Radius - FalloffDistance);
else
FalloffX = FalloffY = 1.0f;
}
else
InVoiceField = false;
break;
}
case ISound::SHAPE_RECTANGLE:
{
RangeX = Voice.m_Rectangle.m_Width / 2.0f;
const int abs_dx = absolute(dx);
const int abs_dy = absolute(dy);
const int w = Voice.m_Rectangle.m_Width / 2.0f;
const int h = Voice.m_Rectangle.m_Height / 2.0f;
if(abs_dx < w && abs_dy < h)
{
InVoiceField = true;
// falloff
int fx = Voice.m_Falloff * w;
int fy = Voice.m_Falloff * h;
FalloffX = abs_dx > fx ? (float)(w - abs_dx) / (w - fx) : 1.0f;
FalloffY = abs_dy > fy ? (float)(h - abs_dy) / (h - fy) : 1.0f;
}
else
InVoiceField = false;
break;
}
};
if(InVoiceField)
{
// panning
if(!(Voice.m_Flags & ISound::FLAG_NO_PANNING))
{
if(dx > 0)
VolumeL = ((RangeX - absolute(dx)) * VolumeL) / RangeX;
else
VolumeR = ((RangeX - absolute(dx)) * VolumeR) / RangeX;
}
{
VolumeL *= FalloffX * FalloffY;
VolumeR *= FalloffX * FalloffY;
}
}
else
{
VolumeL = 0;
VolumeR = 0;
}
}
// process all frames
for(unsigned s = 0; s < End; s++)
{
*pOut++ += (*pInL) * VolumeL;
*pOut++ += (*pInR) * VolumeR;
pInL += Step;
pInR += Step;
Voice.m_Tick++;
}
// free voice if not used any more
if(Voice.m_Tick == Voice.m_pSample->m_NumFrames)
{
if(Voice.m_Flags & ISound::FLAG_LOOP)
Voice.m_Tick = 0;
else
{
Voice.m_pSample = nullptr;
Voice.m_Age++;
}
}
}
m_SoundLock.unlock();
// clamp accumulated values
for(unsigned i = 0; i < Frames * 2; i++)
pFinalOut[i] = clamp<int>(((m_pMixBuffer[i] * MasterVol) / 101) >> 8, std::numeric_limits<short>::min(), std::numeric_limits<short>::max());
#if defined(CONF_ARCH_ENDIAN_BIG)
swap_endian(pFinalOut, sizeof(short), Frames * 2);
#endif
}
static void SdlCallback(void *pUser, Uint8 *pStream, int Len)
{
CSound *pSound = static_cast<CSound *>(pUser);
#if defined(CONF_VIDEORECORDER)
if(!(IVideo::Current() && g_Config.m_ClVideoSndEnable))
{
pSound->Mix((short *)pStream, Len / sizeof(short) / 2);
}
else
{
mem_zero(pStream, Len);
}
#else
pSound->Mix((short *)pStream, Len / sizeof(short) / 2);
#endif
}
int CSound::Init()
{
m_SoundEnabled = false;
m_pGraphics = Kernel()->RequestInterface<IEngineGraphics>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
// Initialize sample indices. We always need them to load sounds in
// the editor even if sound is disabled or failed to be enabled.
m_FirstFreeSampleIndex = 0;
for(size_t i = 0; i < std::size(m_aSamples) - 1; ++i)
{
m_aSamples[i].m_Index = i;
m_aSamples[i].m_NextFreeSampleIndex = i + 1;
m_aSamples[i].m_pData = nullptr;
}
m_aSamples[std::size(m_aSamples) - 1].m_Index = std::size(m_aSamples) - 1;
m_aSamples[std::size(m_aSamples) - 1].m_NextFreeSampleIndex = SAMPLE_INDEX_FULL;
if(!g_Config.m_SndEnable)
return 0;
if(SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
{
dbg_msg("sound", "unable to init SDL audio: %s", SDL_GetError());
return -1;
}
m_MixingRate = g_Config.m_SndRate;
SDL_AudioSpec Format, FormatOut;
Format.freq = m_MixingRate;
Format.format = AUDIO_S16;
Format.channels = 2;
Format.samples = g_Config.m_SndBufferSize;
Format.callback = SdlCallback;
Format.userdata = this;
// Open the audio device and start playing sound!
m_Device = SDL_OpenAudioDevice(nullptr, 0, &Format, &FormatOut, 0);
if(m_Device == 0)
{
dbg_msg("sound", "unable to open audio: %s", SDL_GetError());
return -1;
}
else
dbg_msg("sound", "sound init successful using audio driver '%s'", SDL_GetCurrentAudioDriver());
m_MaxFrames = FormatOut.samples * 2;
#if defined(CONF_VIDEORECORDER)
m_MaxFrames = maximum<uint32_t>(m_MaxFrames, 1024 * 2); // make the buffer bigger just in case
#endif
m_pMixBuffer = (int *)calloc(m_MaxFrames * 2, sizeof(int));
SDL_PauseAudioDevice(m_Device, 0);
m_SoundEnabled = true;
Update();
return 0;
}
int CSound::Update()
{
UpdateVolume();
return 0;
}
void CSound::UpdateVolume()
{
int WantedVolume = g_Config.m_SndVolume;
if(!m_pGraphics->WindowActive() && g_Config.m_SndNonactiveMute)
WantedVolume = 0;
m_SoundVolume.store(WantedVolume, std::memory_order_relaxed);
}
void CSound::Shutdown()
{
for(unsigned SampleId = 0; SampleId < NUM_SAMPLES; SampleId++)
{
UnloadSample(SampleId);
}
SDL_CloseAudioDevice(m_Device);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
free(m_pMixBuffer);
m_pMixBuffer = nullptr;
}
CSample *CSound::AllocSample()
{
if(m_FirstFreeSampleIndex == SAMPLE_INDEX_FULL)
return nullptr;
CSample *pSample = &m_aSamples[m_FirstFreeSampleIndex];
if(pSample->m_pData != nullptr || pSample->m_NextFreeSampleIndex == SAMPLE_INDEX_USED)
{
char aError[128];
str_format(aError, sizeof(aError), "Sample was not unloaded (index=%d, next=%d, duration=%f, data=%p)",
pSample->m_Index, pSample->m_NextFreeSampleIndex, pSample->TotalTime(), pSample->m_pData);
dbg_assert(false, aError);
}
m_FirstFreeSampleIndex = pSample->m_NextFreeSampleIndex;
pSample->m_NextFreeSampleIndex = SAMPLE_INDEX_USED;
return pSample;
}
void CSound::RateConvert(CSample &Sample) const
{
dbg_assert(Sample.m_pData != nullptr, "Sample is not loaded");
// make sure that we need to convert this sound
if(Sample.m_Rate == m_MixingRate)
return;
// allocate new data
const int NumFrames = (int)((Sample.m_NumFrames / (float)Sample.m_Rate) * m_MixingRate);
short *pNewData = (short *)calloc((size_t)NumFrames * Sample.m_Channels, sizeof(short));
for(int i = 0; i < NumFrames; i++)
{
// resample TODO: this should be done better, like linear at least
float a = i / (float)NumFrames;
int f = (int)(a * Sample.m_NumFrames);
if(f >= Sample.m_NumFrames)
f = Sample.m_NumFrames - 1;
// set new data
if(Sample.m_Channels == 1)
pNewData[i] = Sample.m_pData[f];
else if(Sample.m_Channels == 2)
{
pNewData[i * 2] = Sample.m_pData[f * 2];
pNewData[i * 2 + 1] = Sample.m_pData[f * 2 + 1];
}
}
// free old data and apply new
free(Sample.m_pData);
Sample.m_pData = pNewData;
Sample.m_NumFrames = NumFrames;
Sample.m_Rate = m_MixingRate;
}
bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) const
{
int OpusError = 0;
OggOpusFile *pOpusFile = op_open_memory((const unsigned char *)pData, DataSize, &OpusError);
if(pOpusFile)
{
const int NumChannels = op_channel_count(pOpusFile, -1);
if(NumChannels > 2)
{
op_free(pOpusFile);
dbg_msg("sound/opus", "file is not mono or stereo.");
return false;
}
const int NumSamples = op_pcm_total(pOpusFile, -1); // per channel!
if(NumSamples < 0)
{
op_free(pOpusFile);
dbg_msg("sound/opus", "failed to get number of samples, error %d", NumSamples);
return false;
}
short *pSampleData = (short *)calloc((size_t)NumSamples * NumChannels, sizeof(short));
int Pos = 0;
while(Pos < NumSamples)
{
const int Read = op_read(pOpusFile, pSampleData + Pos * NumChannels, (NumSamples - Pos) * NumChannels, nullptr);
if(Read < 0)
{
free(pSampleData);
op_free(pOpusFile);
dbg_msg("sound/opus", "op_read error %d at %d", Read, Pos);
return false;
}
else if(Read == 0) // EOF
break;
Pos += Read;
}
op_free(pOpusFile);
Sample.m_pData = pSampleData;
Sample.m_NumFrames = Pos;
Sample.m_Rate = 48000;
Sample.m_Channels = NumChannels;
Sample.m_LoopStart = -1;
Sample.m_LoopEnd = -1;
Sample.m_PausedAt = 0;
}
else
{
dbg_msg("sound/opus", "failed to decode sample, error %d", OpusError);
return false;
}
return true;
}
// TODO: Update WavPack to get rid of these global variables
static const void *s_pWVBuffer = nullptr;
static int s_WVBufferPosition = 0;
static int s_WVBufferSize = 0;
static int ReadDataOld(void *pBuffer, int Size)
{
int ChunkSize = minimum(Size, s_WVBufferSize - s_WVBufferPosition);
mem_copy(pBuffer, (const char *)s_pWVBuffer + s_WVBufferPosition, ChunkSize);
s_WVBufferPosition += ChunkSize;
return ChunkSize;
}
#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
static int ReadData(void *pId, void *pBuffer, int Size)
{
(void)pId;
return ReadDataOld(pBuffer, Size);
}
static int ReturnFalse(void *pId)
{
(void)pId;
return 0;
}
static unsigned int GetPos(void *pId)
{
(void)pId;
return s_WVBufferPosition;
}
static unsigned int GetLength(void *pId)
{
(void)pId;
return s_WVBufferSize;
}
static int PushBackByte(void *pId, int Char)
{
s_WVBufferPosition -= 1;
return 0;
}
#endif
bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) const
{
char aError[100];
dbg_assert(s_pWVBuffer == nullptr, "DecodeWV already in use");
s_pWVBuffer = pData;
s_WVBufferSize = DataSize;
s_WVBufferPosition = 0;
#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
WavpackStreamReader Callback = {0};
Callback.can_seek = ReturnFalse;
Callback.get_length = GetLength;
Callback.get_pos = GetPos;
Callback.push_back_byte = PushBackByte;
Callback.read_bytes = ReadData;
WavpackContext *pContext = WavpackOpenFileInputEx(&Callback, (void *)1, 0, aError, 0, 0);
#else
WavpackContext *pContext = WavpackOpenFileInput(ReadDataOld, aError);
#endif
if(pContext)
{
const int NumSamples = WavpackGetNumSamples(pContext);
const int BitsPerSample = WavpackGetBitsPerSample(pContext);
const unsigned int SampleRate = WavpackGetSampleRate(pContext);
const int NumChannels = WavpackGetNumChannels(pContext);
if(NumChannels > 2)
{
dbg_msg("sound/wv", "file is not mono or stereo.");
s_pWVBuffer = nullptr;
return false;
}
if(BitsPerSample != 16)
{
dbg_msg("sound/wv", "bps is %d, not 16", BitsPerSample);
s_pWVBuffer = nullptr;
return false;
}
int *pBuffer = (int *)calloc((size_t)NumSamples * NumChannels, sizeof(int));
if(!WavpackUnpackSamples(pContext, pBuffer, NumSamples))
{
free(pBuffer);
dbg_msg("sound/wv", "WavpackUnpackSamples failed. NumSamples=%d, NumChannels=%d", NumSamples, NumChannels);
s_pWVBuffer = nullptr;
return false;
}
Sample.m_pData = (short *)calloc((size_t)NumSamples * NumChannels, sizeof(short));
int *pSrc = pBuffer;
short *pDst = Sample.m_pData;
for(int i = 0; i < NumSamples * NumChannels; i++)
*pDst++ = (short)*pSrc++;
free(pBuffer);
#ifdef CONF_WAVPACK_CLOSE_FILE
WavpackCloseFile(pContext);
#endif
Sample.m_NumFrames = NumSamples;
Sample.m_Rate = SampleRate;
Sample.m_Channels = NumChannels;
Sample.m_LoopStart = -1;
Sample.m_LoopEnd = -1;
Sample.m_PausedAt = 0;
s_pWVBuffer = nullptr;
}
else
{
dbg_msg("sound/wv", "failed to decode sample (%s)", aError);
s_pWVBuffer = nullptr;
return false;
}
return true;
}
int CSound::LoadOpus(const char *pFilename, int StorageType)
{
// no need to load sound when we are running with no sound
if(!m_SoundEnabled)
return -1;
if(!m_pStorage)
return -1;
CSample *pSample = AllocSample();
if(!pSample)
{
dbg_msg("sound/opus", "failed to allocate sample ID. filename='%s'", pFilename);
return -1;
}
void *pData;
unsigned DataSize;
if(!m_pStorage->ReadFile(pFilename, StorageType, &pData, &DataSize))
{
UnloadSample(pSample->m_Index);
dbg_msg("sound/opus", "failed to open file. filename='%s'", pFilename);
return -1;
}
const bool DecodeSuccess = DecodeOpus(*pSample, pData, DataSize);
free(pData);
if(!DecodeSuccess)
{
UnloadSample(pSample->m_Index);
return -1;
}
if(g_Config.m_Debug)
dbg_msg("sound/opus", "loaded %s", pFilename);
RateConvert(*pSample);
return pSample->m_Index;
}
int CSound::LoadWV(const char *pFilename, int StorageType)
{
// no need to load sound when we are running with no sound
if(!m_SoundEnabled)
return -1;
if(!m_pStorage)
return -1;
CSample *pSample = AllocSample();
if(!pSample)
{
dbg_msg("sound/wv", "failed to allocate sample ID. filename='%s'", pFilename);
return -1;
}
void *pData;
unsigned DataSize;
if(!m_pStorage->ReadFile(pFilename, StorageType, &pData, &DataSize))
{
UnloadSample(pSample->m_Index);
dbg_msg("sound/wv", "failed to open file. filename='%s'", pFilename);
return -1;
}
const bool DecodeSuccess = DecodeWV(*pSample, pData, DataSize);
free(pData);
if(!DecodeSuccess)
{
UnloadSample(pSample->m_Index);
return -1;
}
if(g_Config.m_Debug)
dbg_msg("sound/wv", "loaded %s", pFilename);
RateConvert(*pSample);
return pSample->m_Index;
}
int CSound::LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEditor = false)
{
// no need to load sound when we are running with no sound
if(!m_SoundEnabled && !FromEditor)
return -1;
if(!pData)
return -1;
CSample *pSample = AllocSample();
if(!pSample)
return -1;
if(!DecodeOpus(*pSample, pData, DataSize))
{
UnloadSample(pSample->m_Index);
return -1;
}
RateConvert(*pSample);
return pSample->m_Index;
}
int CSound::LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor = false)
{
// no need to load sound when we are running with no sound
if(!m_SoundEnabled && !FromEditor)
return -1;
if(!pData)
return -1;
CSample *pSample = AllocSample();
if(!pSample)
return -1;
if(!DecodeWV(*pSample, pData, DataSize))
{
UnloadSample(pSample->m_Index);
return -1;
}
RateConvert(*pSample);
return pSample->m_Index;
}
void CSound::UnloadSample(int SampleId)
{
if(SampleId == -1 || SampleId >= NUM_SAMPLES)
return;
Stop(SampleId);
// Free data
CSample &Sample = m_aSamples[SampleId];
free(Sample.m_pData);
Sample.m_pData = nullptr;
// Free slot
if(Sample.m_NextFreeSampleIndex == SAMPLE_INDEX_USED)
{
Sample.m_NextFreeSampleIndex = m_FirstFreeSampleIndex;
m_FirstFreeSampleIndex = Sample.m_Index;
}
}
float CSound::GetSampleTotalTime(int SampleId)
{
if(SampleId == -1 || SampleId >= NUM_SAMPLES)
return 0.0f;
return m_aSamples[SampleId].TotalTime();
}
float CSound::GetSampleCurrentTime(int SampleId)
{
if(SampleId == -1 || SampleId >= NUM_SAMPLES)
return 0.0f;
const CLockScope LockScope(m_SoundLock);
CSample *pSample = &m_aSamples[SampleId];
for(auto &Voice : m_aVoices)
{
if(Voice.m_pSample == pSample)
{
return Voice.m_Tick / (float)pSample->m_Rate;
}
}
return pSample->m_PausedAt / (float)pSample->m_Rate;
}
void CSound::SetSampleCurrentTime(int SampleId, float Time)
{
if(SampleId == -1 || SampleId >= NUM_SAMPLES)
return;
const CLockScope LockScope(m_SoundLock);
CSample *pSample = &m_aSamples[SampleId];
for(auto &Voice : m_aVoices)
{
if(Voice.m_pSample == pSample)
{
Voice.m_Tick = pSample->m_NumFrames * Time;
return;
}
}
pSample->m_PausedAt = pSample->m_NumFrames * Time;
}
void CSound::SetChannel(int ChannelId, float Vol, float Pan)
{
m_aChannels[ChannelId].m_Vol = (int)(Vol * 255.0f);
m_aChannels[ChannelId].m_Pan = (int)(Pan * 255.0f); // TODO: this is only on and off right now
}
void CSound::SetListenerPos(float x, float y)
{
m_CenterX.store((int)x, std::memory_order_relaxed);
m_CenterY.store((int)y, std::memory_order_relaxed);
}
void CSound::SetVoiceVolume(CVoiceHandle Voice, float Volume)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
Volume = clamp(Volume, 0.0f, 1.0f);
m_aVoices[VoiceId].m_Vol = (int)(Volume * 255.0f);
}
void CSound::SetVoiceFalloff(CVoiceHandle Voice, float Falloff)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
Falloff = clamp(Falloff, 0.0f, 1.0f);
m_aVoices[VoiceId].m_Falloff = Falloff;
}
void CSound::SetVoiceLocation(CVoiceHandle Voice, float x, float y)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
m_aVoices[VoiceId].m_X = x;
m_aVoices[VoiceId].m_Y = y;
}
void CSound::SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
if(!m_aVoices[VoiceId].m_pSample)
return;
int Tick = 0;
bool IsLooping = m_aVoices[VoiceId].m_Flags & ISound::FLAG_LOOP;
uint64_t TickOffset = m_aVoices[VoiceId].m_pSample->m_Rate * TimeOffset;
if(m_aVoices[VoiceId].m_pSample->m_NumFrames > 0 && IsLooping)
Tick = TickOffset % m_aVoices[VoiceId].m_pSample->m_NumFrames;
else
Tick = clamp(TickOffset, (uint64_t)0, (uint64_t)m_aVoices[VoiceId].m_pSample->m_NumFrames);
// at least 200msec off, else depend on buffer size
float Threshold = maximum(0.2f * m_aVoices[VoiceId].m_pSample->m_Rate, (float)m_MaxFrames);
if(absolute(m_aVoices[VoiceId].m_Tick - Tick) > Threshold)
{
// take care of looping (modulo!)
if(!(IsLooping && (minimum(m_aVoices[VoiceId].m_Tick, Tick) + m_aVoices[VoiceId].m_pSample->m_NumFrames - maximum(m_aVoices[VoiceId].m_Tick, Tick)) <= Threshold))
{
m_aVoices[VoiceId].m_Tick = Tick;
}
}
}
void CSound::SetVoiceCircle(CVoiceHandle Voice, float Radius)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
m_aVoices[VoiceId].m_Circle.m_Radius = maximum(0.0f, Radius);
}
void CSound::SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
m_aVoices[VoiceId].m_Shape = ISound::SHAPE_RECTANGLE;
m_aVoices[VoiceId].m_Rectangle.m_Width = maximum(0.0f, Width);
m_aVoices[VoiceId].m_Rectangle.m_Height = maximum(0.0f, Height);
}
ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float x, float y)
{
const CLockScope LockScope(m_SoundLock);
// search for voice
int VoiceId = -1;
for(int i = 0; i < NUM_VOICES; i++)
{
int NextId = (m_NextVoice + i) % NUM_VOICES;
if(!m_aVoices[NextId].m_pSample)
{
VoiceId = NextId;
m_NextVoice = NextId + 1;
break;
}
}
// voice found, use it
int Age = -1;
if(VoiceId != -1)
{
m_aVoices[VoiceId].m_pSample = &m_aSamples[SampleId];
m_aVoices[VoiceId].m_pChannel = &m_aChannels[ChannelId];
if(Flags & FLAG_LOOP)
{
m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
}
else if(Flags & FLAG_PREVIEW)
{
m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
m_aSamples[SampleId].m_PausedAt = 0;
}
else
{
m_aVoices[VoiceId].m_Tick = 0;
}
m_aVoices[VoiceId].m_Vol = 255;
m_aVoices[VoiceId].m_Flags = Flags;
m_aVoices[VoiceId].m_X = (int)x;
m_aVoices[VoiceId].m_Y = (int)y;
m_aVoices[VoiceId].m_Falloff = 0.0f;
m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
m_aVoices[VoiceId].m_Circle.m_Radius = 1500;
Age = m_aVoices[VoiceId].m_Age;
}
return CreateVoiceHandle(VoiceId, Age);
}
ISound::CVoiceHandle CSound::PlayAt(int ChannelId, int SampleId, int Flags, float x, float y)
{
return Play(ChannelId, SampleId, Flags | ISound::FLAG_POS, x, y);
}
ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags)
{
return Play(ChannelId, SampleId, Flags, 0, 0);
}
void CSound::Pause(int SampleId)
{
// TODO: a nice fade out
const CLockScope LockScope(m_SoundLock);
CSample *pSample = &m_aSamples[SampleId];
for(auto &Voice : m_aVoices)
{
if(Voice.m_pSample == pSample)
{
Voice.m_pSample->m_PausedAt = Voice.m_Tick;
Voice.m_pSample = nullptr;
}
}
}
void CSound::Stop(int SampleId)
{
// TODO: a nice fade out
const CLockScope LockScope(m_SoundLock);
CSample *pSample = &m_aSamples[SampleId];
for(auto &Voice : m_aVoices)
{
if(Voice.m_pSample == pSample)
{
if(Voice.m_Flags & FLAG_LOOP)
Voice.m_pSample->m_PausedAt = Voice.m_Tick;
else
Voice.m_pSample->m_PausedAt = 0;
Voice.m_pSample = nullptr;
}
}
}
void CSound::StopAll()
{
// TODO: a nice fade out
const CLockScope LockScope(m_SoundLock);
for(auto &Voice : m_aVoices)
{
if(Voice.m_pSample)
{
if(Voice.m_Flags & FLAG_LOOP)
Voice.m_pSample->m_PausedAt = Voice.m_Tick;
else
Voice.m_pSample->m_PausedAt = 0;
}
Voice.m_pSample = nullptr;
}
}
void CSound::StopVoice(CVoiceHandle Voice)
{
if(!Voice.IsValid())
return;
int VoiceId = Voice.Id();
const CLockScope LockScope(m_SoundLock);
if(m_aVoices[VoiceId].m_Age != Voice.Age())
return;
m_aVoices[VoiceId].m_pSample = nullptr;
m_aVoices[VoiceId].m_Age++;
}
bool CSound::IsPlaying(int SampleId)
{
const CLockScope LockScope(m_SoundLock);
const CSample *pSample = &m_aSamples[SampleId];
return std::any_of(std::begin(m_aVoices), std::end(m_aVoices), [pSample](const auto &Voice) { return Voice.m_pSample == pSample; });
}
void CSound::PauseAudioDevice()
{
SDL_PauseAudioDevice(m_Device, 1);
}
void CSound::UnpauseAudioDevice()
{
SDL_PauseAudioDevice(m_Device, 0);
}
IEngineSound *CreateEngineSound() { return new CSound; }