1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "sound.h"
4
5#include <base/bytes.h>
6#include <base/dbg.h>
7#include <base/log.h>
8#include <base/math.h>
9#include <base/mem.h>
10#include <base/str.h>
11
12#include <engine/graphics.h>
13#include <engine/shared/config.h>
14#include <engine/storage.h>
15
16#include <SDL.h>
17
18#if defined(CONF_VIDEORECORDER)
19#include <engine/shared/video.h>
20#endif
21extern "C" {
22#include <opusfile.h>
23#include <wavpack.h>
24}
25
26#include <cmath>
27
28static constexpr int SAMPLE_INDEX_USED = -2;
29static constexpr int SAMPLE_INDEX_FULL = -1;
30
31void CSound::Mix(short *pFinalOut, unsigned Frames)
32{
33 Frames = minimum(a: Frames, b: m_MaxFrames);
34 mem_zero(block: m_pMixBuffer, size: Frames * 2 * sizeof(int));
35
36 // acquire lock while we are mixing
37 m_SoundLock.lock();
38
39 const int MasterVol = m_SoundVolume.load(m: std::memory_order_relaxed);
40
41 for(auto &Voice : m_aVoices)
42 {
43 if(!Voice.m_pSample)
44 continue;
45
46 // mix voice
47 int *pOut = m_pMixBuffer;
48
49 const int Step = Voice.m_pSample->m_Channels; // setup input sources
50 short *pInL = &Voice.m_pSample->m_pData[Voice.m_Tick * Step];
51 short *pInR = &Voice.m_pSample->m_pData[Voice.m_Tick * Step + 1];
52
53 unsigned End = Voice.m_pSample->m_NumFrames - Voice.m_Tick;
54
55 int VolumeR = round_truncate(f: Voice.m_pChannel->m_Vol * (Voice.m_Vol / 255.0f));
56 int VolumeL = VolumeR;
57
58 // make sure that we don't go outside the sound data
59 if(Frames < End)
60 End = Frames;
61
62 // check if we have a mono sound
63 if(Voice.m_pSample->m_Channels == 1)
64 pInR = pInL;
65
66 // volume calculation
67 if(Voice.m_Flags & ISound::FLAG_POS && Voice.m_pChannel->m_Pan)
68 {
69 // TODO: we should respect the channel panning value
70 const vec2 Delta = Voice.m_Position - vec2(m_ListenerPositionX.load(m: std::memory_order_relaxed), m_ListenerPositionY.load(m: std::memory_order_relaxed));
71 vec2 Falloff = vec2(0.0f, 0.0f);
72
73 float RangeX = 0.0f; // for panning
74 bool InVoiceField = false;
75
76 switch(Voice.m_Shape)
77 {
78 case ISound::SHAPE_CIRCLE:
79 {
80 const float Radius = Voice.m_Circle.m_Radius;
81 RangeX = Radius;
82
83 const float Dist = length(a: Delta);
84 if(Dist < Radius)
85 {
86 InVoiceField = true;
87
88 // falloff
89 const float FalloffDistance = Radius * Voice.m_Falloff;
90 Falloff.x = Falloff.y = Dist > FalloffDistance ? (Radius - Dist) / (Radius - FalloffDistance) : 1.0f;
91 }
92 break;
93 }
94
95 case ISound::SHAPE_RECTANGLE:
96 {
97 const vec2 AbsoluteDelta = vec2(absolute(a: Delta.x), absolute(a: Delta.y));
98 const float w = Voice.m_Rectangle.m_Width / 2.0f;
99 const float h = Voice.m_Rectangle.m_Height / 2.0f;
100 RangeX = w;
101
102 if(AbsoluteDelta.x < w && AbsoluteDelta.y < h)
103 {
104 InVoiceField = true;
105
106 // falloff
107 const vec2 FalloffDistance = vec2(w, h) * Voice.m_Falloff;
108 Falloff.x = AbsoluteDelta.x > FalloffDistance.x ? (w - AbsoluteDelta.x) / (w - FalloffDistance.x) : 1.0f;
109 Falloff.y = AbsoluteDelta.y > FalloffDistance.y ? (h - AbsoluteDelta.y) / (h - FalloffDistance.y) : 1.0f;
110 }
111 break;
112 }
113 };
114
115 if(InVoiceField)
116 {
117 // panning
118 if(!(Voice.m_Flags & ISound::FLAG_NO_PANNING))
119 {
120 if(Delta.x > 0)
121 VolumeL = ((RangeX - absolute(a: Delta.x)) * VolumeL) / RangeX;
122 else
123 VolumeR = ((RangeX - absolute(a: Delta.x)) * VolumeR) / RangeX;
124 }
125
126 {
127 VolumeL *= Falloff.x * Falloff.y;
128 VolumeR *= Falloff.x * Falloff.y;
129 }
130 }
131 else
132 {
133 VolumeL = 0;
134 VolumeR = 0;
135 }
136 }
137
138 // process all frames
139 for(unsigned s = 0; s < End; s++)
140 {
141 *pOut++ += (*pInL) * VolumeL;
142 *pOut++ += (*pInR) * VolumeR;
143 pInL += Step;
144 pInR += Step;
145 Voice.m_Tick++;
146 }
147
148 // free voice if not used any more
149 if(Voice.m_Tick == Voice.m_pSample->m_NumFrames)
150 {
151 if(Voice.m_Flags & ISound::FLAG_LOOP)
152 {
153 Voice.m_Tick = Voice.m_pSample->m_LoopStart;
154 }
155 else
156 {
157 Voice.m_pSample = nullptr;
158 Voice.m_Age++;
159 }
160 }
161 }
162
163 m_SoundLock.unlock();
164
165 // clamp accumulated values
166 for(unsigned i = 0; i < Frames * 2; i++)
167 pFinalOut[i] = std::clamp<int>(val: ((m_pMixBuffer[i] * MasterVol) / 101) >> 8, lo: std::numeric_limits<short>::min(), hi: std::numeric_limits<short>::max());
168
169#if defined(CONF_ARCH_ENDIAN_BIG)
170 swap_endian(pFinalOut, sizeof(short), Frames * 2);
171#endif
172}
173
174static void SdlCallback(void *pUser, Uint8 *pStream, int Len)
175{
176 CSound *pSound = static_cast<CSound *>(pUser);
177
178#if defined(CONF_VIDEORECORDER)
179 if(!(IVideo::Current() && g_Config.m_ClVideoSndEnable))
180 {
181 pSound->Mix(pFinalOut: (short *)pStream, Frames: Len / sizeof(short) / 2);
182 }
183 else
184 {
185 mem_zero(block: pStream, size: Len);
186 }
187#else
188 pSound->Mix((short *)pStream, Len / sizeof(short) / 2);
189#endif
190}
191
192int CSound::Init()
193{
194 m_SoundEnabled = false;
195 m_pGraphics = Kernel()->RequestInterface<IEngineGraphics>();
196 m_pStorage = Kernel()->RequestInterface<IStorage>();
197
198 // Initialize sample indices. We always need them to load sounds in
199 // the editor even if sound is disabled or failed to be enabled.
200 const CLockScope LockScope(m_SoundLock);
201 m_FirstFreeSampleIndex = 0;
202 for(size_t i = 0; i < std::size(m_aSamples) - 1; ++i)
203 {
204 m_aSamples[i].m_Index = i;
205 m_aSamples[i].m_NextFreeSampleIndex = i + 1;
206 m_aSamples[i].m_pData = nullptr;
207 }
208 m_aSamples[std::size(m_aSamples) - 1].m_Index = std::size(m_aSamples) - 1;
209 m_aSamples[std::size(m_aSamples) - 1].m_NextFreeSampleIndex = SAMPLE_INDEX_FULL;
210
211 if(!g_Config.m_SndEnable)
212 return 0;
213
214 if(SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
215 {
216 log_error("sound", "Unable to init SDL audio: %s", SDL_GetError());
217 return -1;
218 }
219
220 SDL_AudioSpec Format, FormatOut;
221 Format.freq = g_Config.m_SndRate;
222 Format.format = AUDIO_S16;
223 Format.channels = 2;
224 Format.samples = g_Config.m_SndBufferSize;
225 Format.callback = SdlCallback;
226 Format.userdata = this;
227
228 // Open the audio device and start playing sound!
229 m_Device = SDL_OpenAudioDevice(device: nullptr, iscapture: 0, desired: &Format, obtained: &FormatOut, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
230 if(m_Device == 0)
231 {
232 log_error("sound", "Unable to open audio device: %s", SDL_GetError());
233 return -1;
234 }
235 else
236 {
237 log_info("sound", "Sound init successful using audio driver '%s'", SDL_GetCurrentAudioDriver());
238 }
239
240 m_MixingRate = FormatOut.freq;
241 m_MaxFrames = FormatOut.samples * 2;
242#if defined(CONF_VIDEORECORDER)
243 m_MaxFrames = maximum<uint32_t>(a: m_MaxFrames, b: 1024 * 2); // make the buffer bigger just in case
244#endif
245 m_pMixBuffer = (int *)calloc(nmemb: m_MaxFrames * 2, size: sizeof(int));
246
247 m_SoundEnabled = true;
248 Update();
249
250 SDL_PauseAudioDevice(dev: m_Device, pause_on: 0);
251 return 0;
252}
253
254int CSound::Update()
255{
256 UpdateVolume();
257 return 0;
258}
259
260void CSound::UpdateVolume()
261{
262 int WantedVolume = g_Config.m_SndVolume;
263 if(!m_pGraphics->WindowActive() && g_Config.m_SndNonactiveMute)
264 WantedVolume = 0;
265 m_SoundVolume.store(i: WantedVolume, m: std::memory_order_relaxed);
266}
267
268void CSound::Shutdown()
269{
270 StopAll();
271
272 // Stop sound callback before freeing sample data
273 SDL_CloseAudioDevice(dev: m_Device);
274 SDL_QuitSubSystem(SDL_INIT_AUDIO);
275 m_Device = 0;
276
277 const CLockScope LockScope(m_SoundLock);
278 for(auto &Sample : m_aSamples)
279 {
280 free(ptr: Sample.m_pData);
281 Sample.m_pData = nullptr;
282 }
283
284 free(ptr: m_pMixBuffer);
285 m_pMixBuffer = nullptr;
286 m_SoundEnabled = false;
287}
288
289CSample *CSound::AllocSample()
290{
291 const CLockScope LockScope(m_SoundLock);
292 if(m_FirstFreeSampleIndex == SAMPLE_INDEX_FULL)
293 return nullptr;
294
295 CSample *pSample = &m_aSamples[m_FirstFreeSampleIndex];
296 dbg_assert(
297 pSample->m_pData == nullptr && pSample->m_NextFreeSampleIndex != SAMPLE_INDEX_USED,
298 "Sample was not unloaded (index=%d, next=%d, duration=%f, data=%p)",
299 pSample->m_Index, pSample->m_NextFreeSampleIndex, pSample->TotalTime(), pSample->m_pData);
300 m_FirstFreeSampleIndex = pSample->m_NextFreeSampleIndex;
301 pSample->m_NextFreeSampleIndex = SAMPLE_INDEX_USED;
302 return pSample;
303}
304
305void CSound::RateConvert(CSample &Sample) const
306{
307 dbg_assert(Sample.IsLoaded(), "Sample not loaded");
308 // make sure that we need to convert this sound
309 if(Sample.m_Rate == m_MixingRate)
310 return;
311
312 // allocate new data
313 const int NumFrames = (int)((Sample.m_NumFrames / (float)Sample.m_Rate) * m_MixingRate);
314 short *pNewData = (short *)calloc(nmemb: (size_t)NumFrames * Sample.m_Channels, size: sizeof(short));
315
316 for(int i = 0; i < NumFrames; i++)
317 {
318 // resample TODO: this should be done better, like linear at least
319 float a = i / (float)NumFrames;
320 int f = (int)(a * Sample.m_NumFrames);
321 if(f >= Sample.m_NumFrames)
322 f = Sample.m_NumFrames - 1;
323
324 // set new data
325 if(Sample.m_Channels == 1)
326 pNewData[i] = Sample.m_pData[f];
327 else if(Sample.m_Channels == 2)
328 {
329 pNewData[i * 2] = Sample.m_pData[f * 2];
330 pNewData[i * 2 + 1] = Sample.m_pData[f * 2 + 1];
331 }
332 }
333
334 // adjust looping position, note that this is not precise
335 const double Factor = (double)m_MixingRate / (double)Sample.m_Rate;
336 Sample.m_LoopStart = std::round(x: Sample.m_LoopStart * Factor);
337
338 // free old data and apply new
339 free(ptr: Sample.m_pData);
340 Sample.m_pData = pNewData;
341 Sample.m_NumFrames = NumFrames;
342 Sample.m_Rate = m_MixingRate;
343}
344
345bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize, const char *pContextName) const
346{
347 int OpusError = 0;
348 OggOpusFile *pOpusFile = op_open_memory(data: (const unsigned char *)pData, size: DataSize, error: &OpusError);
349 if(pOpusFile)
350 {
351 const int NumChannels = op_channel_count(of: pOpusFile, li: -1);
352 if(NumChannels > 2)
353 {
354 op_free(of: pOpusFile);
355 log_error("sound/opus", "File is not mono or stereo. Filename='%s'", pContextName);
356 return false;
357 }
358
359 const int NumSamples = op_pcm_total(of: pOpusFile, li: -1); // per channel!
360 if(NumSamples < 0)
361 {
362 op_free(of: pOpusFile);
363 log_error("sound/opus", "Failed to get number of samples, error %d. Filename='%s'", NumSamples, pContextName);
364 return false;
365 }
366
367 short *pSampleData = (short *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(short));
368
369 int Pos = 0;
370 while(Pos < NumSamples)
371 {
372 const int Read = op_read(of: pOpusFile, pcm: pSampleData + Pos * NumChannels, buf_size: (NumSamples - Pos) * NumChannels, li: nullptr);
373 if(Read < 0)
374 {
375 free(ptr: pSampleData);
376 op_free(of: pOpusFile);
377 log_error("sound/opus", "op_read error %d at %d. Filename='%s'", Read, Pos, pContextName);
378 return false;
379 }
380 else if(Read == 0) // EOF
381 break;
382 Pos += Read;
383 }
384
385 Sample.m_pData = pSampleData;
386 Sample.m_NumFrames = Pos;
387 Sample.m_Rate = 48000;
388 Sample.m_Channels = NumChannels;
389 Sample.m_LoopStart = 0;
390 Sample.m_PausedAt = 0;
391
392 const OpusTags *pTags = op_tags(of: pOpusFile, li: -1);
393 if(pTags)
394 {
395 for(int i = 0; i < pTags->comments; ++i)
396 {
397 const char *pComment = pTags->user_comments[i];
398 if(!pComment)
399 continue;
400 if(!str_startswith(str: pComment, prefix: "LOOP_START="))
401 continue;
402 int LoopStart = -1;
403 if(!str_toint(str: pComment + str_length(str: "LOOP_START="), out: &LoopStart))
404 {
405 log_error("sound/opus", "Invalid LOOP_START tag. Value='%s' Filename='%s'", pComment + str_length("LOOP_START="), pContextName);
406 break;
407 }
408 if(LoopStart < 0 || LoopStart >= Sample.m_NumFrames)
409 {
410 log_error("sound/opus", "Tag LOOP_START out of range. Value=%d Min=0 Max=%d Filename='%s'", LoopStart, Sample.m_NumFrames - 1, pContextName);
411 break;
412 }
413 Sample.m_LoopStart = LoopStart;
414 break;
415 }
416 }
417
418 op_free(of: pOpusFile);
419 }
420 else
421 {
422 log_error("sound/opus", "Failed to decode sample, error %d. Filename='%s'", OpusError, pContextName);
423 return false;
424 }
425
426 return true;
427}
428
429// TODO: Update WavPack to get rid of these global variables
430static const void *s_pWVBuffer = nullptr;
431static int s_WVBufferPosition = 0;
432static int s_WVBufferSize = 0;
433
434static int ReadDataOld(void *pBuffer, int Size)
435{
436 int ChunkSize = minimum(a: Size, b: s_WVBufferSize - s_WVBufferPosition);
437 mem_copy(dest: pBuffer, source: (const char *)s_pWVBuffer + s_WVBufferPosition, size: ChunkSize);
438 s_WVBufferPosition += ChunkSize;
439 return ChunkSize;
440}
441
442#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
443static int ReadData(void *pId, void *pBuffer, int Size)
444{
445 (void)pId;
446 return ReadDataOld(pBuffer, Size);
447}
448
449static int ReturnFalse(void *pId)
450{
451 (void)pId;
452 return 0;
453}
454
455static unsigned int GetPos(void *pId)
456{
457 (void)pId;
458 return s_WVBufferPosition;
459}
460
461static unsigned int GetLength(void *pId)
462{
463 (void)pId;
464 return s_WVBufferSize;
465}
466
467static int PushBackByte(void *pId, int Char)
468{
469 s_WVBufferPosition -= 1;
470 return 0;
471}
472#endif
473
474bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize, const char *pContextName) const
475{
476 // no need to load sound when we are running with no sound
477 if(!m_SoundEnabled)
478 return false;
479
480 dbg_assert(s_pWVBuffer == nullptr, "DecodeWV already in use");
481 s_pWVBuffer = pData;
482 s_WVBufferSize = DataSize;
483 s_WVBufferPosition = 0;
484
485 char aError[100];
486
487#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
488 WavpackStreamReader Callback = {.read_bytes: 0};
489 Callback.can_seek = ReturnFalse;
490 Callback.get_length = GetLength;
491 Callback.get_pos = GetPos;
492 Callback.push_back_byte = PushBackByte;
493 Callback.read_bytes = ReadData;
494 WavpackContext *pContext = WavpackOpenFileInputEx(reader: &Callback, wv_id: (void *)1, wvc_id: 0, error: aError, flags: 0, norm_offset: 0);
495#else
496 WavpackContext *pContext = WavpackOpenFileInput(ReadDataOld, aError);
497#endif
498 if(pContext)
499 {
500 const int NumSamples = WavpackGetNumSamples(wpc: pContext);
501 const int BitsPerSample = WavpackGetBitsPerSample(wpc: pContext);
502 const unsigned int SampleRate = WavpackGetSampleRate(wpc: pContext);
503 const int NumChannels = WavpackGetNumChannels(wpc: pContext);
504
505 if(NumChannels > 2)
506 {
507 log_error("sound/wv", "File is not mono or stereo. Filename='%s'", pContextName);
508 s_pWVBuffer = nullptr;
509 return false;
510 }
511
512 if(BitsPerSample != 16)
513 {
514 log_error("sound/wv", "Bits per sample is %d, not 16. Filename='%s'", BitsPerSample, pContextName);
515 s_pWVBuffer = nullptr;
516 return false;
517 }
518
519 int *pBuffer = (int *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(int));
520 if(!WavpackUnpackSamples(wpc: pContext, buffer: pBuffer, samples: NumSamples))
521 {
522 free(ptr: pBuffer);
523 log_error("sound/wv", "WavpackUnpackSamples failed. NumSamples=%d NumChannels=%d Filename='%s'", NumSamples, NumChannels, pContextName);
524 s_pWVBuffer = nullptr;
525 return false;
526 }
527
528 Sample.m_pData = (short *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(short));
529
530 int *pSrc = pBuffer;
531 short *pDst = Sample.m_pData;
532 for(int i = 0; i < NumSamples * NumChannels; i++)
533 *pDst++ = (short)*pSrc++;
534
535 free(ptr: pBuffer);
536#ifdef CONF_WAVPACK_CLOSE_FILE
537 WavpackCloseFile(wpc: pContext);
538#endif
539
540 Sample.m_NumFrames = NumSamples;
541 Sample.m_Rate = SampleRate;
542 Sample.m_Channels = NumChannels;
543 Sample.m_LoopStart = 0;
544 Sample.m_PausedAt = 0;
545
546 s_pWVBuffer = nullptr;
547 }
548 else
549 {
550 log_error("sound/wv", "Failed to decode sample (%s). Filename='%s'", aError, pContextName);
551 s_pWVBuffer = nullptr;
552 return false;
553 }
554
555 return true;
556}
557
558int CSound::LoadOpus(const char *pFilename, int StorageType)
559{
560 // no need to load sound when we are running with no sound
561 if(!m_SoundEnabled)
562 return -1;
563
564 CSample *pSample = AllocSample();
565 if(!pSample)
566 {
567 log_error("sound/opus", "Failed to allocate sample ID. Filename='%s'", pFilename);
568 return -1;
569 }
570
571 void *pData;
572 unsigned DataSize;
573 if(!m_pStorage->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
574 {
575 UnloadSample(SampleId: pSample->m_Index);
576 log_error("sound/opus", "Failed to open file. Filename='%s'", pFilename);
577 return -1;
578 }
579
580 const bool DecodeSuccess = DecodeOpus(Sample&: *pSample, pData, DataSize, pContextName: pFilename);
581 free(ptr: pData);
582 if(!DecodeSuccess)
583 {
584 UnloadSample(SampleId: pSample->m_Index);
585 return -1;
586 }
587
588 if(g_Config.m_Debug)
589 log_trace("sound/opus", "Loaded '%s' (index %d)", pFilename, pSample->m_Index);
590
591 RateConvert(Sample&: *pSample);
592 return pSample->m_Index;
593}
594
595int CSound::LoadWV(const char *pFilename, int StorageType)
596{
597 // no need to load sound when we are running with no sound
598 if(!m_SoundEnabled)
599 return -1;
600
601 CSample *pSample = AllocSample();
602 if(!pSample)
603 {
604 log_error("sound/wv", "Failed to allocate sample ID. Filename='%s'", pFilename);
605 return -1;
606 }
607
608 void *pData;
609 unsigned DataSize;
610 if(!m_pStorage->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
611 {
612 UnloadSample(SampleId: pSample->m_Index);
613 log_error("sound/wv", "Failed to open file. Filename='%s'", pFilename);
614 return -1;
615 }
616
617 const bool DecodeSuccess = DecodeWV(Sample&: *pSample, pData, DataSize, pContextName: pFilename);
618 free(ptr: pData);
619 if(!DecodeSuccess)
620 {
621 UnloadSample(SampleId: pSample->m_Index);
622 return -1;
623 }
624
625 if(g_Config.m_Debug)
626 log_trace("sound/wv", "Loaded '%s' (index %d)", pFilename, pSample->m_Index);
627
628 RateConvert(Sample&: *pSample);
629 return pSample->m_Index;
630}
631
632int CSound::LoadOpusFromMem(const void *pData, unsigned DataSize, bool ForceLoad, const char *pContextName)
633{
634 // no need to load sound when we are running with no sound
635 if(!m_SoundEnabled && !ForceLoad)
636 return -1;
637
638 CSample *pSample = AllocSample();
639 if(!pSample)
640 return -1;
641
642 if(!DecodeOpus(Sample&: *pSample, pData, DataSize, pContextName))
643 {
644 UnloadSample(SampleId: pSample->m_Index);
645 return -1;
646 }
647
648 RateConvert(Sample&: *pSample);
649 return pSample->m_Index;
650}
651
652int CSound::LoadWVFromMem(const void *pData, unsigned DataSize, bool ForceLoad, const char *pContextName)
653{
654 // no need to load sound when we are running with no sound
655 if(!m_SoundEnabled && !ForceLoad)
656 return -1;
657
658 CSample *pSample = AllocSample();
659 if(!pSample)
660 return -1;
661
662 if(!DecodeWV(Sample&: *pSample, pData, DataSize, pContextName))
663 {
664 UnloadSample(SampleId: pSample->m_Index);
665 return -1;
666 }
667
668 RateConvert(Sample&: *pSample);
669 return pSample->m_Index;
670}
671
672void CSound::UnloadSample(int SampleId)
673{
674 if(SampleId == -1)
675 return;
676
677 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
678 const CLockScope LockScope(m_SoundLock);
679 CSample &Sample = m_aSamples[SampleId];
680
681 if(Sample.IsLoaded())
682 {
683 // Stop voices using this sample
684 for(auto &Voice : m_aVoices)
685 {
686 if(Voice.m_pSample == &Sample)
687 {
688 Voice.m_pSample = nullptr;
689 }
690 }
691
692 // Free data
693 free(ptr: Sample.m_pData);
694 Sample.m_pData = nullptr;
695 }
696
697 // Free slot
698 if(Sample.m_NextFreeSampleIndex == SAMPLE_INDEX_USED)
699 {
700 Sample.m_NextFreeSampleIndex = m_FirstFreeSampleIndex;
701 m_FirstFreeSampleIndex = Sample.m_Index;
702 }
703}
704
705float CSound::GetSampleTotalTime(int SampleId)
706{
707 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
708
709 const CLockScope LockScope(m_SoundLock);
710 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
711 return m_aSamples[SampleId].TotalTime();
712}
713
714float CSound::GetSampleCurrentTime(int SampleId)
715{
716 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
717
718 const CLockScope LockScope(m_SoundLock);
719 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
720 CSample *pSample = &m_aSamples[SampleId];
721 for(auto &Voice : m_aVoices)
722 {
723 if(Voice.m_pSample == pSample)
724 {
725 return Voice.m_Tick / (float)pSample->m_Rate;
726 }
727 }
728
729 return pSample->m_PausedAt / (float)pSample->m_Rate;
730}
731
732void CSound::SetSampleCurrentTime(int SampleId, float Time)
733{
734 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
735
736 const CLockScope LockScope(m_SoundLock);
737 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
738 CSample *pSample = &m_aSamples[SampleId];
739 for(auto &Voice : m_aVoices)
740 {
741 if(Voice.m_pSample == pSample)
742 {
743 Voice.m_Tick = pSample->m_NumFrames * Time;
744 return;
745 }
746 }
747
748 pSample->m_PausedAt = pSample->m_NumFrames * Time;
749}
750
751void CSound::SetChannel(int ChannelId, float Vol, float Pan)
752{
753 dbg_assert(ChannelId >= 0 && ChannelId < NUM_CHANNELS, "ChannelId invalid");
754
755 const CLockScope LockScope(m_SoundLock);
756 m_aChannels[ChannelId].m_Vol = (int)(Vol * 255.0f);
757 m_aChannels[ChannelId].m_Pan = (int)(Pan * 255.0f); // TODO: this is only on and off right now
758}
759
760void CSound::SetListenerPosition(vec2 Position)
761{
762 m_ListenerPositionX.store(t: Position.x, m: std::memory_order_relaxed);
763 m_ListenerPositionY.store(t: Position.y, m: std::memory_order_relaxed);
764}
765
766void CSound::SetVoiceVolume(CVoiceHandle Voice, float Volume)
767{
768 if(!Voice.IsValid())
769 return;
770
771 int VoiceId = Voice.Id();
772
773 const CLockScope LockScope(m_SoundLock);
774 if(m_aVoices[VoiceId].m_Age != Voice.Age())
775 return;
776
777 Volume = std::clamp(val: Volume, lo: 0.0f, hi: 1.0f);
778 m_aVoices[VoiceId].m_Vol = (int)(Volume * 255.0f);
779}
780
781void CSound::SetVoiceFalloff(CVoiceHandle Voice, float Falloff)
782{
783 if(!Voice.IsValid())
784 return;
785
786 int VoiceId = Voice.Id();
787
788 const CLockScope LockScope(m_SoundLock);
789 if(m_aVoices[VoiceId].m_Age != Voice.Age())
790 return;
791
792 Falloff = std::clamp(val: Falloff, lo: 0.0f, hi: 1.0f);
793 m_aVoices[VoiceId].m_Falloff = Falloff;
794}
795
796void CSound::SetVoicePosition(CVoiceHandle Voice, vec2 Position)
797{
798 if(!Voice.IsValid())
799 return;
800
801 int VoiceId = Voice.Id();
802
803 const CLockScope LockScope(m_SoundLock);
804 if(m_aVoices[VoiceId].m_Age != Voice.Age())
805 return;
806
807 m_aVoices[VoiceId].m_Position = Position;
808}
809
810void CSound::SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset)
811{
812 if(!Voice.IsValid())
813 return;
814
815 int VoiceId = Voice.Id();
816
817 const CLockScope LockScope(m_SoundLock);
818 if(m_aVoices[VoiceId].m_Age != Voice.Age())
819 return;
820
821 if(!m_aVoices[VoiceId].m_pSample)
822 return;
823
824 int Tick = 0;
825 bool IsLooping = m_aVoices[VoiceId].m_Flags & ISound::FLAG_LOOP;
826 uint64_t TickOffset = m_aVoices[VoiceId].m_pSample->m_Rate * TimeOffset;
827 if(m_aVoices[VoiceId].m_pSample->m_NumFrames > 0 && IsLooping)
828 {
829 const int LoopStart = m_aVoices[VoiceId].m_pSample->m_LoopStart;
830 const int NumFrames = m_aVoices[VoiceId].m_pSample->m_NumFrames;
831 if(TickOffset < static_cast<uint64_t>(NumFrames))
832 {
833 // Still in first playthrough
834 Tick = TickOffset;
835 }
836 else
837 {
838 // Past first playthrough, wrap within loop section only
839 const int LoopLength = NumFrames - LoopStart;
840 if(LoopLength > 0)
841 Tick = LoopStart + ((TickOffset - NumFrames) % LoopLength);
842 else
843 Tick = LoopStart;
844 }
845 }
846 else
847 {
848 Tick = std::clamp<uint64_t>(val: TickOffset, lo: 0, hi: m_aVoices[VoiceId].m_pSample->m_NumFrames);
849 }
850
851 // at least 200msec off, else depend on buffer size
852 float Threshold = maximum(a: 0.2f * m_aVoices[VoiceId].m_pSample->m_Rate, b: (float)m_MaxFrames);
853 if(absolute(a: m_aVoices[VoiceId].m_Tick - Tick) > Threshold)
854 {
855 // take care of looping (modulo!)
856 if(!(IsLooping && (minimum(a: m_aVoices[VoiceId].m_Tick, b: Tick) + m_aVoices[VoiceId].m_pSample->m_NumFrames - maximum(a: m_aVoices[VoiceId].m_Tick, b: Tick)) <= Threshold))
857 {
858 m_aVoices[VoiceId].m_Tick = Tick;
859 }
860 }
861}
862
863void CSound::SetVoiceCircle(CVoiceHandle Voice, float Radius)
864{
865 if(!Voice.IsValid())
866 return;
867
868 int VoiceId = Voice.Id();
869
870 const CLockScope LockScope(m_SoundLock);
871 if(m_aVoices[VoiceId].m_Age != Voice.Age())
872 return;
873
874 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
875 m_aVoices[VoiceId].m_Circle.m_Radius = maximum(a: 0.0f, b: Radius);
876}
877
878void CSound::SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height)
879{
880 if(!Voice.IsValid())
881 return;
882
883 int VoiceId = Voice.Id();
884
885 const CLockScope LockScope(m_SoundLock);
886 if(m_aVoices[VoiceId].m_Age != Voice.Age())
887 return;
888
889 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_RECTANGLE;
890 m_aVoices[VoiceId].m_Rectangle.m_Width = maximum(a: 0.0f, b: Width);
891 m_aVoices[VoiceId].m_Rectangle.m_Height = maximum(a: 0.0f, b: Height);
892}
893
894ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position)
895{
896 const CLockScope LockScope(m_SoundLock);
897
898 // search for voice
899 int VoiceId = -1;
900 for(int i = 0; i < NUM_VOICES; i++)
901 {
902 int NextId = (m_NextVoice + i) % NUM_VOICES;
903 if(!m_aVoices[NextId].m_pSample)
904 {
905 VoiceId = NextId;
906 m_NextVoice = NextId + 1;
907 break;
908 }
909 }
910 if(VoiceId == -1)
911 {
912 return CreateVoiceHandle(Index: -1, Age: -1);
913 }
914
915 // voice found, use it
916 m_aVoices[VoiceId].m_pSample = &m_aSamples[SampleId];
917 m_aVoices[VoiceId].m_pChannel = &m_aChannels[ChannelId];
918 if(Flags & FLAG_LOOP)
919 {
920 m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
921 }
922 else if(Flags & FLAG_PREVIEW)
923 {
924 m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
925 m_aSamples[SampleId].m_PausedAt = 0;
926 }
927 else
928 {
929 m_aVoices[VoiceId].m_Tick = 0;
930 }
931 m_aVoices[VoiceId].m_Vol = (int)(std::clamp(val: Volume, lo: 0.0f, hi: 1.0f) * 255.0f);
932 m_aVoices[VoiceId].m_Flags = Flags;
933 m_aVoices[VoiceId].m_Position = Position;
934 m_aVoices[VoiceId].m_Falloff = 0.0f;
935 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
936 m_aVoices[VoiceId].m_Circle.m_Radius = 1500;
937 return CreateVoiceHandle(Index: VoiceId, Age: m_aVoices[VoiceId].m_Age);
938}
939
940ISound::CVoiceHandle CSound::PlayAt(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position)
941{
942 return Play(ChannelId, SampleId, Flags: Flags | ISound::FLAG_POS, Volume, Position);
943}
944
945ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume)
946{
947 return Play(ChannelId, SampleId, Flags, Volume, Position: vec2(0.0f, 0.0f));
948}
949
950void CSound::Pause(int SampleId)
951{
952 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
953
954 // TODO: a nice fade out
955 const CLockScope LockScope(m_SoundLock);
956 CSample *pSample = &m_aSamples[SampleId];
957 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
958 for(auto &Voice : m_aVoices)
959 {
960 if(Voice.m_pSample == pSample)
961 {
962 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
963 Voice.m_pSample = nullptr;
964 }
965 }
966}
967
968void CSound::Stop(int SampleId)
969{
970 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
971
972 // TODO: a nice fade out
973 const CLockScope LockScope(m_SoundLock);
974 CSample *pSample = &m_aSamples[SampleId];
975 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
976 for(auto &Voice : m_aVoices)
977 {
978 if(Voice.m_pSample == pSample)
979 {
980 if(Voice.m_Flags & FLAG_LOOP)
981 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
982 else
983 Voice.m_pSample->m_PausedAt = 0;
984 Voice.m_pSample = nullptr;
985 }
986 }
987}
988
989void CSound::StopAll()
990{
991 // TODO: a nice fade out
992 const CLockScope LockScope(m_SoundLock);
993 for(auto &Voice : m_aVoices)
994 {
995 if(Voice.m_pSample)
996 {
997 if(Voice.m_Flags & FLAG_LOOP)
998 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
999 else
1000 Voice.m_pSample->m_PausedAt = 0;
1001 }
1002 Voice.m_pSample = nullptr;
1003 }
1004}
1005
1006void CSound::StopVoice(CVoiceHandle Voice)
1007{
1008 if(!Voice.IsValid())
1009 return;
1010
1011 int VoiceId = Voice.Id();
1012
1013 const CLockScope LockScope(m_SoundLock);
1014 if(m_aVoices[VoiceId].m_Age != Voice.Age())
1015 return;
1016
1017 m_aVoices[VoiceId].m_pSample = nullptr;
1018 m_aVoices[VoiceId].m_Age++;
1019}
1020
1021bool CSound::IsPlaying(int SampleId)
1022{
1023 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
1024 const CLockScope LockScope(m_SoundLock);
1025 const CSample *pSample = &m_aSamples[SampleId];
1026 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
1027 return std::any_of(first: std::begin(arr&: m_aVoices), last: std::end(arr&: m_aVoices), pred: [pSample](const auto &Voice) { return Voice.m_pSample == pSample; });
1028}
1029
1030void CSound::PauseAudioDevice()
1031{
1032 SDL_PauseAudioDevice(dev: m_Device, pause_on: 1);
1033}
1034
1035void CSound::UnpauseAudioDevice()
1036{
1037 SDL_PauseAudioDevice(dev: m_Device, pause_on: 0);
1038}
1039
1040IEngineSound *CreateEngineSound() { return new CSound; }
1041