/* (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 #include #include #include #include #include #include "sound.h" #if defined(CONF_VIDEORECORDER) #include #endif extern "C" { #include #include } #include 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(((m_pMixBuffer[i] * MasterVol) / 101) >> 8, std::numeric_limits::min(), std::numeric_limits::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(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(); m_pStorage = Kernel()->RequestInterface(); 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(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; } int CSound::AllocID() { // TODO: linear search, get rid of it for(unsigned SampleID = 0; SampleID < NUM_SAMPLES; SampleID++) { if(m_aSamples[SampleID].m_pData == nullptr) return SampleID; } return -1; } void CSound::RateConvert(CSample &Sample) { // make sure that we need to convert this sound if(!Sample.m_pData || 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) { OggOpusFile *pOpusFile = op_open_memory((const unsigned char *)pData, DataSize, nullptr); if(pOpusFile) { const int NumChannels = op_channel_count(pOpusFile, -1); const int NumSamples = op_pcm_total(pOpusFile, -1); // per channel! Sample.m_Channels = NumChannels; if(Sample.m_Channels > 2) { dbg_msg("sound/opus", "file is not mono or stereo."); return false; } Sample.m_pData = (short *)calloc((size_t)NumSamples * NumChannels, sizeof(short)); int Pos = 0; while(Pos < NumSamples) { const int Read = op_read(pOpusFile, Sample.m_pData + Pos * NumChannels, NumSamples * NumChannels, nullptr); if(Read < 0) { free(Sample.m_pData); dbg_msg("sound/opus", "op_read error %d at %d", Read, Pos); return false; } else if(Read == 0) // EOF break; Pos += Read; } Sample.m_NumFrames = Pos; Sample.m_Rate = 48000; Sample.m_LoopStart = -1; Sample.m_LoopEnd = -1; Sample.m_PausedAt = 0; } else { dbg_msg("sound/opus", "failed to decode sample"); 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) { 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); Sample.m_Channels = NumChannels; Sample.m_Rate = SampleRate; if(Sample.m_Channels > 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_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; const int SampleID = AllocID(); if(SampleID < 0) { 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)) { dbg_msg("sound/opus", "failed to open file. filename='%s'", pFilename); return -1; } const bool DecodeSuccess = DecodeOpus(m_aSamples[SampleID], pData, DataSize); free(pData); if(!DecodeSuccess) return -1; if(g_Config.m_Debug) dbg_msg("sound/opus", "loaded %s", pFilename); RateConvert(m_aSamples[SampleID]); return SampleID; } 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; const int SampleID = AllocID(); if(SampleID < 0) { 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)) { dbg_msg("sound/wv", "failed to open file. filename='%s'", pFilename); return -1; } const bool DecodeSuccess = DecodeWV(m_aSamples[SampleID], pData, DataSize); free(pData); if(!DecodeSuccess) return -1; if(g_Config.m_Debug) dbg_msg("sound/wv", "loaded %s", pFilename); RateConvert(m_aSamples[SampleID]); return SampleID; } 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; const int SampleID = AllocID(); if(SampleID < 0) return -1; if(!DecodeOpus(m_aSamples[SampleID], pData, DataSize)) return -1; RateConvert(m_aSamples[SampleID]); return SampleID; } 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; const int SampleID = AllocID(); if(SampleID < 0) return -1; if(!DecodeWV(m_aSamples[SampleID], pData, DataSize)) return -1; RateConvert(m_aSamples[SampleID]); return SampleID; } void CSound::UnloadSample(int SampleID) { if(SampleID == -1 || SampleID >= NUM_SAMPLES) return; Stop(SampleID); free(m_aSamples[SampleID].m_pData); m_aSamples[SampleID].m_pData = nullptr; } float CSound::GetSampleTotalTime(int SampleID) { if(SampleID == -1 || SampleID >= NUM_SAMPLES) return 0.0f; return (m_aSamples[SampleID].m_NumFrames / (float)m_aSamples[SampleID].m_Rate); } float CSound::GetSampleCurrentTime(int SampleID) { if(SampleID == -1 || SampleID >= NUM_SAMPLES) return 0.0f; CSample *pSample = &m_aSamples[SampleID]; if(IsPlaying(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; CSample *pSample = &m_aSamples[SampleID]; if(IsPlaying(SampleID)) { for(auto &Voice : m_aVoices) { if(Voice.m_pSample == pSample) { Voice.m_Tick = pSample->m_NumFrames * Time; } } } else { 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(); std::unique_lock Lock(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(); std::unique_lock Lock(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(); std::unique_lock Lock(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(); std::unique_lock Lock(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(); std::unique_lock Lock(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(); std::unique_lock Lock(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) { m_SoundLock.lock(); // 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; } m_SoundLock.unlock(); 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 std::unique_lock Lock(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 std::unique_lock Lock(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 std::unique_lock Lock(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(); std::unique_lock Lock(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) { std::unique_lock Lock(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; }