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