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 dbg_msg(sys: "sound", fmt: "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 dbg_msg(sys: "sound", fmt: "unable to open audio: %s", SDL_GetError());
233 return -1;
234 }
235 else
236 dbg_msg(sys: "sound", fmt: "sound init successful using audio driver '%s'", SDL_GetCurrentAudioDriver());
237
238 m_MixingRate = FormatOut.freq;
239 m_MaxFrames = FormatOut.samples * 2;
240#if defined(CONF_VIDEORECORDER)
241 m_MaxFrames = maximum<uint32_t>(a: m_MaxFrames, b: 1024 * 2); // make the buffer bigger just in case
242#endif
243 m_pMixBuffer = (int *)calloc(nmemb: m_MaxFrames * 2, size: sizeof(int));
244
245 m_SoundEnabled = true;
246 Update();
247
248 SDL_PauseAudioDevice(dev: m_Device, pause_on: 0);
249 return 0;
250}
251
252int CSound::Update()
253{
254 UpdateVolume();
255 return 0;
256}
257
258void CSound::UpdateVolume()
259{
260 int WantedVolume = g_Config.m_SndVolume;
261 if(!m_pGraphics->WindowActive() && g_Config.m_SndNonactiveMute)
262 WantedVolume = 0;
263 m_SoundVolume.store(i: WantedVolume, m: std::memory_order_relaxed);
264}
265
266void CSound::Shutdown()
267{
268 StopAll();
269
270 // Stop sound callback before freeing sample data
271 SDL_CloseAudioDevice(dev: m_Device);
272 SDL_QuitSubSystem(SDL_INIT_AUDIO);
273 m_Device = 0;
274
275 const CLockScope LockScope(m_SoundLock);
276 for(auto &Sample : m_aSamples)
277 {
278 free(ptr: Sample.m_pData);
279 Sample.m_pData = nullptr;
280 }
281
282 free(ptr: m_pMixBuffer);
283 m_pMixBuffer = nullptr;
284 m_SoundEnabled = false;
285}
286
287CSample *CSound::AllocSample()
288{
289 const CLockScope LockScope(m_SoundLock);
290 if(m_FirstFreeSampleIndex == SAMPLE_INDEX_FULL)
291 return nullptr;
292
293 CSample *pSample = &m_aSamples[m_FirstFreeSampleIndex];
294 dbg_assert(
295 pSample->m_pData == nullptr && pSample->m_NextFreeSampleIndex != SAMPLE_INDEX_USED,
296 "Sample was not unloaded (index=%d, next=%d, duration=%f, data=%p)",
297 pSample->m_Index, pSample->m_NextFreeSampleIndex, pSample->TotalTime(), pSample->m_pData);
298 m_FirstFreeSampleIndex = pSample->m_NextFreeSampleIndex;
299 pSample->m_NextFreeSampleIndex = SAMPLE_INDEX_USED;
300 return pSample;
301}
302
303void CSound::RateConvert(CSample &Sample) const
304{
305 dbg_assert(Sample.IsLoaded(), "Sample not loaded");
306 // make sure that we need to convert this sound
307 if(Sample.m_Rate == m_MixingRate)
308 return;
309
310 // allocate new data
311 const int NumFrames = (int)((Sample.m_NumFrames / (float)Sample.m_Rate) * m_MixingRate);
312 short *pNewData = (short *)calloc(nmemb: (size_t)NumFrames * Sample.m_Channels, size: sizeof(short));
313
314 for(int i = 0; i < NumFrames; i++)
315 {
316 // resample TODO: this should be done better, like linear at least
317 float a = i / (float)NumFrames;
318 int f = (int)(a * Sample.m_NumFrames);
319 if(f >= Sample.m_NumFrames)
320 f = Sample.m_NumFrames - 1;
321
322 // set new data
323 if(Sample.m_Channels == 1)
324 pNewData[i] = Sample.m_pData[f];
325 else if(Sample.m_Channels == 2)
326 {
327 pNewData[i * 2] = Sample.m_pData[f * 2];
328 pNewData[i * 2 + 1] = Sample.m_pData[f * 2 + 1];
329 }
330 }
331
332 // adjust looping position, note that this is not precise
333 const double Factor = (double)m_MixingRate / (double)Sample.m_Rate;
334 Sample.m_LoopStart = std::round(x: Sample.m_LoopStart * Factor);
335
336 // free old data and apply new
337 free(ptr: Sample.m_pData);
338 Sample.m_pData = pNewData;
339 Sample.m_NumFrames = NumFrames;
340 Sample.m_Rate = m_MixingRate;
341}
342
343bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize, const char *pContextName) const
344{
345 int OpusError = 0;
346 OggOpusFile *pOpusFile = op_open_memory(data: (const unsigned char *)pData, size: DataSize, error: &OpusError);
347 if(pOpusFile)
348 {
349 const int NumChannels = op_channel_count(of: pOpusFile, li: -1);
350 if(NumChannels > 2)
351 {
352 op_free(of: pOpusFile);
353 log_error("sound/opus", "File is not mono or stereo. Filename='%s'", pContextName);
354 return false;
355 }
356
357 const int NumSamples = op_pcm_total(of: pOpusFile, li: -1); // per channel!
358 if(NumSamples < 0)
359 {
360 op_free(of: pOpusFile);
361 log_error("sound/opus", "Failed to get number of samples, error %d. Filename='%s'", NumSamples, pContextName);
362 return false;
363 }
364
365 short *pSampleData = (short *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(short));
366
367 int Pos = 0;
368 while(Pos < NumSamples)
369 {
370 const int Read = op_read(of: pOpusFile, pcm: pSampleData + Pos * NumChannels, buf_size: (NumSamples - Pos) * NumChannels, li: nullptr);
371 if(Read < 0)
372 {
373 free(ptr: pSampleData);
374 op_free(of: pOpusFile);
375 log_error("sound/opus", "op_read error %d at %d. Filename='%s'", Read, Pos, pContextName);
376 return false;
377 }
378 else if(Read == 0) // EOF
379 break;
380 Pos += Read;
381 }
382
383 Sample.m_pData = pSampleData;
384 Sample.m_NumFrames = Pos;
385 Sample.m_Rate = 48000;
386 Sample.m_Channels = NumChannels;
387 Sample.m_LoopStart = 0;
388 Sample.m_PausedAt = 0;
389
390 const OpusTags *pTags = op_tags(of: pOpusFile, li: -1);
391 if(pTags)
392 {
393 for(int i = 0; i < pTags->comments; ++i)
394 {
395 const char *pComment = pTags->user_comments[i];
396 if(!pComment)
397 continue;
398 if(!str_startswith(str: pComment, prefix: "LOOP_START="))
399 continue;
400 int LoopStart = -1;
401 if(!str_toint(str: pComment + str_length(str: "LOOP_START="), out: &LoopStart))
402 {
403 log_error("sound/opus", "Invalid LOOP_START tag. Value='%s' Filename='%s'", pComment + str_length("LOOP_START="), pContextName);
404 break;
405 }
406 if(LoopStart < 0 || LoopStart >= Sample.m_NumFrames)
407 {
408 log_error("sound/opus", "Tag LOOP_START out of range. Value=%d Min=0 Max=%d Filename='%s'", LoopStart, Sample.m_NumFrames - 1, pContextName);
409 break;
410 }
411 Sample.m_LoopStart = LoopStart;
412 break;
413 }
414 }
415
416 op_free(of: pOpusFile);
417 }
418 else
419 {
420 log_error("sound/opus", "Failed to decode sample, error %d. Filename='%s'", OpusError, pContextName);
421 return false;
422 }
423
424 return true;
425}
426
427// TODO: Update WavPack to get rid of these global variables
428static const void *s_pWVBuffer = nullptr;
429static int s_WVBufferPosition = 0;
430static int s_WVBufferSize = 0;
431
432static int ReadDataOld(void *pBuffer, int Size)
433{
434 int ChunkSize = minimum(a: Size, b: s_WVBufferSize - s_WVBufferPosition);
435 mem_copy(dest: pBuffer, source: (const char *)s_pWVBuffer + s_WVBufferPosition, size: ChunkSize);
436 s_WVBufferPosition += ChunkSize;
437 return ChunkSize;
438}
439
440#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
441static int ReadData(void *pId, void *pBuffer, int Size)
442{
443 (void)pId;
444 return ReadDataOld(pBuffer, Size);
445}
446
447static int ReturnFalse(void *pId)
448{
449 (void)pId;
450 return 0;
451}
452
453static unsigned int GetPos(void *pId)
454{
455 (void)pId;
456 return s_WVBufferPosition;
457}
458
459static unsigned int GetLength(void *pId)
460{
461 (void)pId;
462 return s_WVBufferSize;
463}
464
465static int PushBackByte(void *pId, int Char)
466{
467 s_WVBufferPosition -= 1;
468 return 0;
469}
470#endif
471
472bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize, const char *pContextName) const
473{
474 // no need to load sound when we are running with no sound
475 if(!m_SoundEnabled)
476 return false;
477
478 dbg_assert(s_pWVBuffer == nullptr, "DecodeWV already in use");
479 s_pWVBuffer = pData;
480 s_WVBufferSize = DataSize;
481 s_WVBufferPosition = 0;
482
483 char aError[100];
484
485#if defined(CONF_WAVPACK_OPEN_FILE_INPUT_EX)
486 WavpackStreamReader Callback = {.read_bytes: 0};
487 Callback.can_seek = ReturnFalse;
488 Callback.get_length = GetLength;
489 Callback.get_pos = GetPos;
490 Callback.push_back_byte = PushBackByte;
491 Callback.read_bytes = ReadData;
492 WavpackContext *pContext = WavpackOpenFileInputEx(reader: &Callback, wv_id: (void *)1, wvc_id: 0, error: aError, flags: 0, norm_offset: 0);
493#else
494 WavpackContext *pContext = WavpackOpenFileInput(ReadDataOld, aError);
495#endif
496 if(pContext)
497 {
498 const int NumSamples = WavpackGetNumSamples(wpc: pContext);
499 const int BitsPerSample = WavpackGetBitsPerSample(wpc: pContext);
500 const unsigned int SampleRate = WavpackGetSampleRate(wpc: pContext);
501 const int NumChannels = WavpackGetNumChannels(wpc: pContext);
502
503 if(NumChannels > 2)
504 {
505 log_error("sound/wv", "File is not mono or stereo. Filename='%s'", pContextName);
506 s_pWVBuffer = nullptr;
507 return false;
508 }
509
510 if(BitsPerSample != 16)
511 {
512 log_error("sound/wv", "Bits per sample is %d, not 16. Filename='%s'", BitsPerSample, pContextName);
513 s_pWVBuffer = nullptr;
514 return false;
515 }
516
517 int *pBuffer = (int *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(int));
518 if(!WavpackUnpackSamples(wpc: pContext, buffer: pBuffer, samples: NumSamples))
519 {
520 free(ptr: pBuffer);
521 log_error("sound/wv", "WavpackUnpackSamples failed. NumSamples=%d NumChannels=%d Filename='%s'", NumSamples, NumChannels, pContextName);
522 s_pWVBuffer = nullptr;
523 return false;
524 }
525
526 Sample.m_pData = (short *)calloc(nmemb: (size_t)NumSamples * NumChannels, size: sizeof(short));
527
528 int *pSrc = pBuffer;
529 short *pDst = Sample.m_pData;
530 for(int i = 0; i < NumSamples * NumChannels; i++)
531 *pDst++ = (short)*pSrc++;
532
533 free(ptr: pBuffer);
534#ifdef CONF_WAVPACK_CLOSE_FILE
535 WavpackCloseFile(wpc: pContext);
536#endif
537
538 Sample.m_NumFrames = NumSamples;
539 Sample.m_Rate = SampleRate;
540 Sample.m_Channels = NumChannels;
541 Sample.m_LoopStart = 0;
542 Sample.m_PausedAt = 0;
543
544 s_pWVBuffer = nullptr;
545 }
546 else
547 {
548 log_error("sound/wv", "Failed to decode sample (%s). Filename='%s'", aError, pContextName);
549 s_pWVBuffer = nullptr;
550 return false;
551 }
552
553 return true;
554}
555
556int CSound::LoadOpus(const char *pFilename, int StorageType)
557{
558 // no need to load sound when we are running with no sound
559 if(!m_SoundEnabled)
560 return -1;
561
562 CSample *pSample = AllocSample();
563 if(!pSample)
564 {
565 log_error("sound/opus", "Failed to allocate sample ID. Filename='%s'", pFilename);
566 return -1;
567 }
568
569 void *pData;
570 unsigned DataSize;
571 if(!m_pStorage->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
572 {
573 UnloadSample(SampleId: pSample->m_Index);
574 log_error("sound/opus", "Failed to open file. Filename='%s'", pFilename);
575 return -1;
576 }
577
578 const bool DecodeSuccess = DecodeOpus(Sample&: *pSample, pData, DataSize, pContextName: pFilename);
579 free(ptr: pData);
580 if(!DecodeSuccess)
581 {
582 UnloadSample(SampleId: pSample->m_Index);
583 return -1;
584 }
585
586 if(g_Config.m_Debug)
587 log_trace("sound/opus", "Loaded '%s' (index %d)", pFilename, pSample->m_Index);
588
589 RateConvert(Sample&: *pSample);
590 return pSample->m_Index;
591}
592
593int CSound::LoadWV(const char *pFilename, int StorageType)
594{
595 // no need to load sound when we are running with no sound
596 if(!m_SoundEnabled)
597 return -1;
598
599 CSample *pSample = AllocSample();
600 if(!pSample)
601 {
602 log_error("sound/wv", "Failed to allocate sample ID. Filename='%s'", pFilename);
603 return -1;
604 }
605
606 void *pData;
607 unsigned DataSize;
608 if(!m_pStorage->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
609 {
610 UnloadSample(SampleId: pSample->m_Index);
611 log_error("sound/wv", "Failed to open file. Filename='%s'", pFilename);
612 return -1;
613 }
614
615 const bool DecodeSuccess = DecodeWV(Sample&: *pSample, pData, DataSize, pContextName: pFilename);
616 free(ptr: pData);
617 if(!DecodeSuccess)
618 {
619 UnloadSample(SampleId: pSample->m_Index);
620 return -1;
621 }
622
623 if(g_Config.m_Debug)
624 log_trace("sound/wv", "Loaded '%s' (index %d)", pFilename, pSample->m_Index);
625
626 RateConvert(Sample&: *pSample);
627 return pSample->m_Index;
628}
629
630int CSound::LoadOpusFromMem(const void *pData, unsigned DataSize, bool ForceLoad, const char *pContextName)
631{
632 // no need to load sound when we are running with no sound
633 if(!m_SoundEnabled && !ForceLoad)
634 return -1;
635
636 CSample *pSample = AllocSample();
637 if(!pSample)
638 return -1;
639
640 if(!DecodeOpus(Sample&: *pSample, pData, DataSize, pContextName))
641 {
642 UnloadSample(SampleId: pSample->m_Index);
643 return -1;
644 }
645
646 RateConvert(Sample&: *pSample);
647 return pSample->m_Index;
648}
649
650int CSound::LoadWVFromMem(const void *pData, unsigned DataSize, bool ForceLoad, const char *pContextName)
651{
652 // no need to load sound when we are running with no sound
653 if(!m_SoundEnabled && !ForceLoad)
654 return -1;
655
656 CSample *pSample = AllocSample();
657 if(!pSample)
658 return -1;
659
660 if(!DecodeWV(Sample&: *pSample, pData, DataSize, pContextName))
661 {
662 UnloadSample(SampleId: pSample->m_Index);
663 return -1;
664 }
665
666 RateConvert(Sample&: *pSample);
667 return pSample->m_Index;
668}
669
670void CSound::UnloadSample(int SampleId)
671{
672 if(SampleId == -1)
673 return;
674
675 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
676 const CLockScope LockScope(m_SoundLock);
677 CSample &Sample = m_aSamples[SampleId];
678
679 if(Sample.IsLoaded())
680 {
681 // Stop voices using this sample
682 for(auto &Voice : m_aVoices)
683 {
684 if(Voice.m_pSample == &Sample)
685 {
686 Voice.m_pSample = nullptr;
687 }
688 }
689
690 // Free data
691 free(ptr: Sample.m_pData);
692 Sample.m_pData = nullptr;
693 }
694
695 // Free slot
696 if(Sample.m_NextFreeSampleIndex == SAMPLE_INDEX_USED)
697 {
698 Sample.m_NextFreeSampleIndex = m_FirstFreeSampleIndex;
699 m_FirstFreeSampleIndex = Sample.m_Index;
700 }
701}
702
703float CSound::GetSampleTotalTime(int SampleId)
704{
705 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
706
707 const CLockScope LockScope(m_SoundLock);
708 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
709 return m_aSamples[SampleId].TotalTime();
710}
711
712float CSound::GetSampleCurrentTime(int SampleId)
713{
714 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
715
716 const CLockScope LockScope(m_SoundLock);
717 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
718 CSample *pSample = &m_aSamples[SampleId];
719 for(auto &Voice : m_aVoices)
720 {
721 if(Voice.m_pSample == pSample)
722 {
723 return Voice.m_Tick / (float)pSample->m_Rate;
724 }
725 }
726
727 return pSample->m_PausedAt / (float)pSample->m_Rate;
728}
729
730void CSound::SetSampleCurrentTime(int SampleId, float Time)
731{
732 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
733
734 const CLockScope LockScope(m_SoundLock);
735 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
736 CSample *pSample = &m_aSamples[SampleId];
737 for(auto &Voice : m_aVoices)
738 {
739 if(Voice.m_pSample == pSample)
740 {
741 Voice.m_Tick = pSample->m_NumFrames * Time;
742 return;
743 }
744 }
745
746 pSample->m_PausedAt = pSample->m_NumFrames * Time;
747}
748
749void CSound::SetChannel(int ChannelId, float Vol, float Pan)
750{
751 dbg_assert(ChannelId >= 0 && ChannelId < NUM_CHANNELS, "ChannelId invalid");
752
753 const CLockScope LockScope(m_SoundLock);
754 m_aChannels[ChannelId].m_Vol = (int)(Vol * 255.0f);
755 m_aChannels[ChannelId].m_Pan = (int)(Pan * 255.0f); // TODO: this is only on and off right now
756}
757
758void CSound::SetListenerPosition(vec2 Position)
759{
760 m_ListenerPositionX.store(t: Position.x, m: std::memory_order_relaxed);
761 m_ListenerPositionY.store(t: Position.y, m: std::memory_order_relaxed);
762}
763
764void CSound::SetVoiceVolume(CVoiceHandle Voice, float Volume)
765{
766 if(!Voice.IsValid())
767 return;
768
769 int VoiceId = Voice.Id();
770
771 const CLockScope LockScope(m_SoundLock);
772 if(m_aVoices[VoiceId].m_Age != Voice.Age())
773 return;
774
775 Volume = std::clamp(val: Volume, lo: 0.0f, hi: 1.0f);
776 m_aVoices[VoiceId].m_Vol = (int)(Volume * 255.0f);
777}
778
779void CSound::SetVoiceFalloff(CVoiceHandle Voice, float Falloff)
780{
781 if(!Voice.IsValid())
782 return;
783
784 int VoiceId = Voice.Id();
785
786 const CLockScope LockScope(m_SoundLock);
787 if(m_aVoices[VoiceId].m_Age != Voice.Age())
788 return;
789
790 Falloff = std::clamp(val: Falloff, lo: 0.0f, hi: 1.0f);
791 m_aVoices[VoiceId].m_Falloff = Falloff;
792}
793
794void CSound::SetVoicePosition(CVoiceHandle Voice, vec2 Position)
795{
796 if(!Voice.IsValid())
797 return;
798
799 int VoiceId = Voice.Id();
800
801 const CLockScope LockScope(m_SoundLock);
802 if(m_aVoices[VoiceId].m_Age != Voice.Age())
803 return;
804
805 m_aVoices[VoiceId].m_Position = Position;
806}
807
808void CSound::SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset)
809{
810 if(!Voice.IsValid())
811 return;
812
813 int VoiceId = Voice.Id();
814
815 const CLockScope LockScope(m_SoundLock);
816 if(m_aVoices[VoiceId].m_Age != Voice.Age())
817 return;
818
819 if(!m_aVoices[VoiceId].m_pSample)
820 return;
821
822 int Tick = 0;
823 bool IsLooping = m_aVoices[VoiceId].m_Flags & ISound::FLAG_LOOP;
824 uint64_t TickOffset = m_aVoices[VoiceId].m_pSample->m_Rate * TimeOffset;
825 if(m_aVoices[VoiceId].m_pSample->m_NumFrames > 0 && IsLooping)
826 {
827 const int LoopStart = m_aVoices[VoiceId].m_pSample->m_LoopStart;
828 const int NumFrames = m_aVoices[VoiceId].m_pSample->m_NumFrames;
829 if(TickOffset < static_cast<uint64_t>(NumFrames))
830 {
831 // Still in first playthrough
832 Tick = TickOffset;
833 }
834 else
835 {
836 // Past first playthrough, wrap within loop section only
837 const int LoopLength = NumFrames - LoopStart;
838 if(LoopLength > 0)
839 Tick = LoopStart + ((TickOffset - NumFrames) % LoopLength);
840 else
841 Tick = LoopStart;
842 }
843 }
844 else
845 {
846 Tick = std::clamp<uint64_t>(val: TickOffset, lo: 0, hi: m_aVoices[VoiceId].m_pSample->m_NumFrames);
847 }
848
849 // at least 200msec off, else depend on buffer size
850 float Threshold = maximum(a: 0.2f * m_aVoices[VoiceId].m_pSample->m_Rate, b: (float)m_MaxFrames);
851 if(absolute(a: m_aVoices[VoiceId].m_Tick - Tick) > Threshold)
852 {
853 // take care of looping (modulo!)
854 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))
855 {
856 m_aVoices[VoiceId].m_Tick = Tick;
857 }
858 }
859}
860
861void CSound::SetVoiceCircle(CVoiceHandle Voice, float Radius)
862{
863 if(!Voice.IsValid())
864 return;
865
866 int VoiceId = Voice.Id();
867
868 const CLockScope LockScope(m_SoundLock);
869 if(m_aVoices[VoiceId].m_Age != Voice.Age())
870 return;
871
872 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
873 m_aVoices[VoiceId].m_Circle.m_Radius = maximum(a: 0.0f, b: Radius);
874}
875
876void CSound::SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height)
877{
878 if(!Voice.IsValid())
879 return;
880
881 int VoiceId = Voice.Id();
882
883 const CLockScope LockScope(m_SoundLock);
884 if(m_aVoices[VoiceId].m_Age != Voice.Age())
885 return;
886
887 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_RECTANGLE;
888 m_aVoices[VoiceId].m_Rectangle.m_Width = maximum(a: 0.0f, b: Width);
889 m_aVoices[VoiceId].m_Rectangle.m_Height = maximum(a: 0.0f, b: Height);
890}
891
892ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position)
893{
894 const CLockScope LockScope(m_SoundLock);
895
896 // search for voice
897 int VoiceId = -1;
898 for(int i = 0; i < NUM_VOICES; i++)
899 {
900 int NextId = (m_NextVoice + i) % NUM_VOICES;
901 if(!m_aVoices[NextId].m_pSample)
902 {
903 VoiceId = NextId;
904 m_NextVoice = NextId + 1;
905 break;
906 }
907 }
908 if(VoiceId == -1)
909 {
910 return CreateVoiceHandle(Index: -1, Age: -1);
911 }
912
913 // voice found, use it
914 m_aVoices[VoiceId].m_pSample = &m_aSamples[SampleId];
915 m_aVoices[VoiceId].m_pChannel = &m_aChannels[ChannelId];
916 if(Flags & FLAG_LOOP)
917 {
918 m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
919 }
920 else if(Flags & FLAG_PREVIEW)
921 {
922 m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt;
923 m_aSamples[SampleId].m_PausedAt = 0;
924 }
925 else
926 {
927 m_aVoices[VoiceId].m_Tick = 0;
928 }
929 m_aVoices[VoiceId].m_Vol = (int)(std::clamp(val: Volume, lo: 0.0f, hi: 1.0f) * 255.0f);
930 m_aVoices[VoiceId].m_Flags = Flags;
931 m_aVoices[VoiceId].m_Position = Position;
932 m_aVoices[VoiceId].m_Falloff = 0.0f;
933 m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE;
934 m_aVoices[VoiceId].m_Circle.m_Radius = 1500;
935 return CreateVoiceHandle(Index: VoiceId, Age: m_aVoices[VoiceId].m_Age);
936}
937
938ISound::CVoiceHandle CSound::PlayAt(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position)
939{
940 return Play(ChannelId, SampleId, Flags: Flags | ISound::FLAG_POS, Volume, Position);
941}
942
943ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume)
944{
945 return Play(ChannelId, SampleId, Flags, Volume, Position: vec2(0.0f, 0.0f));
946}
947
948void CSound::Pause(int SampleId)
949{
950 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
951
952 // TODO: a nice fade out
953 const CLockScope LockScope(m_SoundLock);
954 CSample *pSample = &m_aSamples[SampleId];
955 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
956 for(auto &Voice : m_aVoices)
957 {
958 if(Voice.m_pSample == pSample)
959 {
960 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
961 Voice.m_pSample = nullptr;
962 }
963 }
964}
965
966void CSound::Stop(int SampleId)
967{
968 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
969
970 // TODO: a nice fade out
971 const CLockScope LockScope(m_SoundLock);
972 CSample *pSample = &m_aSamples[SampleId];
973 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
974 for(auto &Voice : m_aVoices)
975 {
976 if(Voice.m_pSample == pSample)
977 {
978 if(Voice.m_Flags & FLAG_LOOP)
979 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
980 else
981 Voice.m_pSample->m_PausedAt = 0;
982 Voice.m_pSample = nullptr;
983 }
984 }
985}
986
987void CSound::StopAll()
988{
989 // TODO: a nice fade out
990 const CLockScope LockScope(m_SoundLock);
991 for(auto &Voice : m_aVoices)
992 {
993 if(Voice.m_pSample)
994 {
995 if(Voice.m_Flags & FLAG_LOOP)
996 Voice.m_pSample->m_PausedAt = Voice.m_Tick;
997 else
998 Voice.m_pSample->m_PausedAt = 0;
999 }
1000 Voice.m_pSample = nullptr;
1001 }
1002}
1003
1004void CSound::StopVoice(CVoiceHandle Voice)
1005{
1006 if(!Voice.IsValid())
1007 return;
1008
1009 int VoiceId = Voice.Id();
1010
1011 const CLockScope LockScope(m_SoundLock);
1012 if(m_aVoices[VoiceId].m_Age != Voice.Age())
1013 return;
1014
1015 m_aVoices[VoiceId].m_pSample = nullptr;
1016 m_aVoices[VoiceId].m_Age++;
1017}
1018
1019bool CSound::IsPlaying(int SampleId)
1020{
1021 dbg_assert(SampleId >= 0 && SampleId < NUM_SAMPLES, "SampleId invalid");
1022 const CLockScope LockScope(m_SoundLock);
1023 const CSample *pSample = &m_aSamples[SampleId];
1024 dbg_assert(m_aSamples[SampleId].IsLoaded(), "Sample not loaded");
1025 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; });
1026}
1027
1028void CSound::PauseAudioDevice()
1029{
1030 SDL_PauseAudioDevice(dev: m_Device, pause_on: 1);
1031}
1032
1033void CSound::UnpauseAudioDevice()
1034{
1035 SDL_PauseAudioDevice(dev: m_Device, pause_on: 0);
1036}
1037
1038IEngineSound *CreateEngineSound() { return new CSound; }
1039