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