1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4 Copyright (C) 1995 Ronny Wester
5 Copyright (C) 2003 Jeremy Chin
6 Copyright (C) 2003-2007 Lucas Martin-King
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22 This file incorporates work covered by the following copyright and
23 permission notice:
24
25 Copyright (c) 2013-2017, 2019-2020 Cong Xu
26 All rights reserved.
27
28 Redistribution and use in source and binary forms, with or without
29 modification, are permitted provided that the following conditions are met:
30
31 Redistributions of source code must retain the above copyright notice, this
32 list of conditions and the following disclaimer.
33 Redistributions in binary form must reproduce the above copyright notice,
34 this list of conditions and the following disclaimer in the documentation
35 and/or other materials provided with the distribution.
36
37 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
38 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
39 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
40 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
41 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
43 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
44 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
45 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
46 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
47 POSSIBILITY OF SUCH DAMAGE.
48 */
49 #include "sounds.h"
50
51 #include <ctype.h>
52 #include <math.h>
53 #include <memory.h>
54 #include <stdio.h>
55 #include <stdlib.h>
56 #include <string.h>
57 #include <sys/stat.h>
58
59 #include <SDL.h>
60
61 #include <tinydir/tinydir.h>
62
63 #include "algorithms.h"
64 #include "files.h"
65 #include "log.h"
66 #include "map.h"
67 #include "music.h"
68 #include "vector.h"
69
70 SoundDevice gSoundDevice;
71
OpenAudio(int frequency,Uint16 format,int channels,int chunkSize)72 int OpenAudio(int frequency, Uint16 format, int channels, int chunkSize)
73 {
74 int qFrequency;
75 Uint16 qFormat;
76 int qChannels;
77
78 if (Mix_OpenAudio(frequency, format, channels, chunkSize) != 0)
79 {
80 printf("Couldn't open audio!: %s\n", SDL_GetError());
81 return 1;
82 }
83
84 // Check that we got the specs we wanted
85 #ifndef __EMSCRIPTEN__
86 Mix_QuerySpec(&qFrequency, &qFormat, &qChannels);
87 if (qFrequency != frequency || qFormat != format || qChannels != channels)
88 {
89 printf("Audio not what we want.\n");
90 return 1;
91 }
92 #endif
93
94 return 0;
95 }
96
97 static Mix_Chunk *LoadSound(const char *path);
SoundLoad(map_t sounds,const char * name,const char * path)98 static void SoundLoad(map_t sounds, const char *name, const char *path)
99 {
100 // If the sound basename is a number, it is part of a group of random
101 // sounds
102 char basename[CDOGS_FILENAME_MAX];
103 PathGetBasenameWithoutExtension(basename, name);
104 char nameNoExt[CDOGS_PATH_MAX];
105 PathGetWithoutExtension(nameNoExt, name);
106 bool isNumber = true;
107 for (const char *c = basename; *c != '\0'; c++)
108 {
109 if (!isdigit(*c))
110 {
111 isNumber = false;
112 break;
113 }
114 }
115 if (isNumber)
116 {
117 // Sound is random
118 // Only add the 0 number
119 const int n = atoi(basename);
120 if (n != 0)
121 {
122 return;
123 }
124 SoundData *sound;
125 CCALLOC(sound, sizeof *sound);
126 sound->Type = SOUND_RANDOM;
127 CArrayInit(&sound->u.random.sounds, sizeof(Mix_Chunk *));
128 // Remove "0.<ext>" from path
129 const char *ext = StrGetFileExt(path);
130 const int len = (int)(ext - path - 2);
131 char fmt[CDOGS_FILENAME_MAX];
132 strncpy(fmt, path, len);
133 // Create format string path/to/sound/%d.<ext>
134 sprintf(fmt + len, "%%d.%s", ext);
135 for (int i = 0;; i++)
136 {
137 char buf[CDOGS_PATH_MAX];
138 sprintf(buf, fmt, i);
139 Mix_Chunk *data = LoadSound(buf);
140 if (data == NULL)
141 break;
142 CArrayPushBack(&sound->u.random.sounds, &data);
143 }
144 // Remove "/0" from name and add
145 *strrchr(nameNoExt, '/') = '\0';
146 SoundAdd(sounds, nameNoExt, sound);
147 }
148 else
149 {
150 Mix_Chunk *data = LoadSound(path);
151 if (data != NULL)
152 {
153 SoundData *sound;
154 CMALLOC(sound, sizeof *sound);
155 sound->Type = SOUND_NORMAL;
156 sound->u.normal = data;
157 SoundAdd(sounds, nameNoExt, sound);
158 }
159 }
160 }
LoadSound(const char * path)161 static Mix_Chunk *LoadSound(const char *path)
162 {
163 // Only load sounds from known extensions
164 const char *ext = strrchr(path, '.');
165 if (ext == NULL ||
166 !(strcmp(ext, ".ogg") == 0 || strcmp(ext, ".OGG") == 0 ||
167 strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 ||
168 strcmp(ext, ".mp3") == 0 || strcmp(ext, ".MP3") == 0))
169 {
170 return NULL;
171 }
172 LOG(LM_MAIN, LL_TRACE, "loading sound file %s", path);
173 return Mix_LoadWAV(path);
174 }
175 static void SoundDataTerminate(any_t data);
SoundAdd(map_t sounds,const char * name,SoundData * sound)176 void SoundAdd(map_t sounds, const char *name, SoundData *sound)
177 {
178 const int error = hashmap_put(sounds, name, sound);
179 if (error != MAP_OK)
180 {
181 LOG(LM_MAIN, LL_ERROR, "failed to add sound %s: %d", name, error);
182 SoundDataTerminate((any_t)sound);
183 }
184 }
185
SoundInitialize(SoundDevice * device,const char * path)186 void SoundInitialize(SoundDevice *device, const char *path)
187 {
188 memset(device, 0, sizeof *device);
189 SoundReopen(device);
190
191 device->sounds = hashmap_new();
192 device->customSounds = hashmap_new();
193 char buf[CDOGS_PATH_MAX];
194 GetDataFilePath(buf, path);
195 SoundLoadDir(device->sounds, buf, NULL);
196 MusicPlayerInit(&device->music);
197 }
SoundLoadDir(map_t sounds,const char * path,const char * prefix)198 void SoundLoadDir(map_t sounds, const char *path, const char *prefix)
199 {
200 tinydir_dir dir;
201 if (tinydir_open(&dir, path) == -1)
202 {
203 if (errno != ENOENT)
204 {
205 LOG(LM_MAIN, LL_ERROR, "Cannot open sound dir '%s': %s", path,
206 strerror(errno));
207 }
208 goto bail;
209 }
210 for (; dir.has_next; tinydir_next(&dir))
211 {
212 tinydir_file file;
213 if (tinydir_readfile(&dir, &file) == -1)
214 {
215 LOG(LM_MAIN, LL_ERROR, "Cannot read sound file '%s'", file.path);
216 continue;
217 }
218 if (file.name[0] == '.')
219 {
220 continue;
221 }
222 char buf[CDOGS_PATH_MAX];
223 if (prefix != NULL)
224 {
225 sprintf(buf, "%s/%s", prefix, file.name);
226 }
227 else
228 {
229 strcpy(buf, file.name);
230 }
231 if (file.is_reg)
232 {
233 SoundLoad(sounds, buf, file.path);
234 }
235 else if (file.is_dir)
236 {
237 SoundLoadDir(sounds, file.path, buf);
238 }
239 }
240
241 bail:
242 tinydir_close(&dir);
243 }
244
SoundClose(SoundDevice * s,const bool waitForSoundsComplete)245 static void SoundClose(SoundDevice *s, const bool waitForSoundsComplete)
246 {
247 if (!s->isInitialised)
248 {
249 return;
250 }
251
252 if (waitForSoundsComplete)
253 {
254 Uint32 waitStart = SDL_GetTicks();
255 while (Mix_Playing(-1) > 0 && SDL_GetTicks() - waitStart < 1000)
256 ;
257 // Don't stop the music unless we're reopening
258 MusicStop(&s->music);
259 }
260 while (Mix_Init(0))
261 {
262 Mix_Quit();
263 }
264 Mix_CloseAudio();
265 }
266
SoundReconfigure(SoundDevice * s)267 void SoundReconfigure(SoundDevice *s)
268 {
269 s->isInitialised = false;
270 s->music.isInitialised = false;
271
272 if (Mix_AllocateChannels(s->channels) != s->channels)
273 {
274 printf("Couldn't allocate channels!\n");
275 return;
276 }
277
278 const int sVol = ConfigGetInt(&gConfig, "Sound.SoundVolume");
279 Mix_Volume(-1, sVol);
280 const int mVol = ConfigGetInt(&gConfig, "Sound.MusicVolume");
281 Mix_VolumeMusic(mVol);
282 MusicSetPlaying(&s->music, mVol > 0);
283
284 s->isInitialised = true;
285 s->music.isInitialised = true;
286 }
287
SoundReopen(SoundDevice * s)288 void SoundReopen(SoundDevice *s)
289 {
290 SoundClose(s, false);
291 if (OpenAudio(CDOGS_SND_RATE, CDOGS_SND_FMT, CDOGS_SND_CHANNELS, 1024) !=
292 0)
293 {
294 return;
295 }
296
297 s->channels = 64;
298 SoundReconfigure(s);
299 }
300
SoundClear(map_t sounds)301 void SoundClear(map_t sounds)
302 {
303 hashmap_clear(sounds, SoundDataTerminate);
304 }
SoundTerminate(SoundDevice * device,const bool waitForSoundsComplete)305 void SoundTerminate(SoundDevice *device, const bool waitForSoundsComplete)
306 {
307 SoundClose(device, waitForSoundsComplete);
308
309 hashmap_destroy(device->sounds, SoundDataTerminate);
310 hashmap_destroy(device->customSounds, SoundDataTerminate);
311
312 MusicPlayerTerminate(&device->music);
313 }
SoundDataTerminate(any_t data)314 static void SoundDataTerminate(any_t data)
315 {
316 SoundData *s = data;
317 switch (s->Type)
318 {
319 case SOUND_NORMAL:
320 Mix_FreeChunk(s->u.normal);
321 break;
322 case SOUND_RANDOM:
323 CA_FOREACH(Mix_Chunk *, chunk, s->u.random.sounds)
324 Mix_FreeChunk(*chunk);
325 CA_FOREACH_END()
326 CArrayTerminate(&s->u.random.sounds);
327 break;
328 default:
329 CASSERT(false, "Unknown sound data type");
330 break;
331 }
332 CFREE(s);
333 }
334
335 #define OUT_OF_SIGHT_DISTANCE_PLUS 100
336 static int GetChannel(SoundDevice *s, Mix_Chunk *data);
MuffleEffect(int chan,void * stream,int len,void * udata)337 static void MuffleEffect(int chan, void *stream, int len, void *udata)
338 {
339 UNUSED(chan);
340 UNUSED(udata);
341 const int channels = 2;
342 const int chunk = channels * 2;
343 for (int i = 0; i < len / chunk - 2; i++)
344 {
345 int16_t *samples = (int16_t *)((char *)stream + i * chunk);
346 samples[0] = (samples[0] + samples[2] + samples[4]) / 3;
347 samples[1] = (samples[1] + samples[3] + samples[5]) / 3;
348 }
349 }
350 static void SetSoundEffect(
351 const int channel, const Sint16 bearingDegrees, const Uint8 distance,
352 const bool isMuffled);
SoundPlayAtPosition(SoundDevice * device,Mix_Chunk * data,const struct vec2 dp,const bool isMuffled)353 static void SoundPlayAtPosition(
354 SoundDevice *device, Mix_Chunk *data, const struct vec2 dp,
355 const bool isMuffled)
356 {
357 if (!device->isInitialised || data == NULL)
358 {
359 return;
360 }
361
362 int distance = 0;
363 Sint16 bearingDegrees = 0;
364 const float screen = (float)gGraphicsDevice.cachedConfig.Res.x;
365 const float halfScreen = screen / 2;
366 if (!svec2_is_zero(dp))
367 {
368 // Calculate distance and bearing
369 // Sound position is calculated from an imaginary camera that's half as
370 // distant from the centre of the screen as the screen width, i.e.
371 //
372 // centre-+
373 // v
374 // Screen: |------+------|
375 // |
376 // |
377 // camera---> +
378 // Calculate real distance using Pythagoras
379 const float d = svec2_length(dp);
380 // Scale so that sounds more than a full screen from centre have
381 // maximum distance (255)
382 const float maxDistance =
383 sqrtf(screen * screen + halfScreen * halfScreen);
384 distance = (int)(d * 255 / maxDistance);
385
386 // Calculate bearing
387 const double bearing = atan((double)dp.x / halfScreen);
388 bearingDegrees = (Sint16)(bearing * 180 / MPI);
389 if (bearingDegrees < 0)
390 {
391 bearingDegrees += 360;
392 }
393 }
394 if (isMuffled)
395 {
396 distance += OUT_OF_SIGHT_DISTANCE_PLUS;
397 }
398 // Don't play anything if it's too distant
399 // This means we don't waste sound channels
400 if (distance > 255)
401 {
402 return;
403 }
404
405 LOG(LM_SOUND, LL_TRACE, "distance(%d) bearing(%d)", distance,
406 bearingDegrees);
407
408 // Get sound channel to play sound
409 const int channel = GetChannel(device, data);
410 if (channel < 0)
411 {
412 return;
413 }
414
415 SetSoundEffect(channel, bearingDegrees, (Uint8)distance, isMuffled);
416 }
GetChannel(SoundDevice * s,Mix_Chunk * data)417 static int GetChannel(SoundDevice *s, Mix_Chunk *data)
418 {
419 for (;;)
420 {
421 const int channel = Mix_PlayChannel(-1, data, 0);
422 if (channel >= 0 || s->channels > 128)
423 {
424 return channel;
425 }
426 // Check if we cannot play the sound; allocate more channels
427 s->channels *= 2;
428 if (Mix_AllocateChannels(s->channels) != s->channels)
429 {
430 LOG(LM_SOUND, LL_ERROR, "Cannot allocate channels");
431 return -1;
432 }
433 // When allocating new channels, need to reset their volume
434 Mix_Volume(-1, ConfigGetInt(&gConfig, "Sound.SoundVolume"));
435 }
436 }
SetSoundEffect(const int channel,const Sint16 bearingDegrees,const Uint8 distance,const bool isMuffled)437 static void SetSoundEffect(
438 const int channel, const Sint16 bearingDegrees, const Uint8 distance,
439 const bool isMuffled)
440 {
441 #ifndef __EMSCRIPTEN__
442 Mix_SetPosition(channel, bearingDegrees, (Uint8)distance);
443 if (isMuffled)
444 {
445 if (!Mix_RegisterEffect(channel, MuffleEffect, NULL, NULL))
446 {
447 fprintf(stderr, "Mix_RegisterEffect: %s\n", Mix_GetError());
448 }
449 }
450 #else
451 // Mix_SetPosition and Mix_RegisterEffect not supported by emscripten;
452 // use plain panning instead
453
454 // Calculate left/right channel as values from 0-180
455 int left;
456 if (bearingDegrees < 90)
457 left = 90 - bearingDegrees;
458 else if (bearingDegrees < 270)
459 left = bearingDegrees - 90;
460 else
461 left = 450 - bearingDegrees;
462 const int right = 180 - left;
463 Mix_SetPanning(
464 channel, (Uint8)(left * distance / 180),
465 (Uint8)(right * distance / 180));
466 #endif
467 }
468
SoundPlay(SoundDevice * device,Mix_Chunk * data)469 void SoundPlay(SoundDevice *device, Mix_Chunk *data)
470 {
471 if (!device->isInitialised)
472 {
473 return;
474 }
475
476 SoundPlayAtPosition(device, data, svec2_zero(), false);
477 }
478
SoundSetEar(const bool isLeft,const int idx,const struct vec2 pos)479 void SoundSetEar(const bool isLeft, const int idx, const struct vec2 pos)
480 {
481 if (isLeft)
482 {
483 if (idx == 0)
484 {
485 gSoundDevice.earLeft1 = pos;
486 }
487 else
488 {
489 gSoundDevice.earLeft2 = pos;
490 }
491 }
492 else
493 {
494 if (idx == 0)
495 {
496 gSoundDevice.earRight1 = pos;
497 }
498 else
499 {
500 gSoundDevice.earRight2 = pos;
501 }
502 }
503 }
504
SoundSetEarsSide(const bool isLeft,const struct vec2 pos)505 void SoundSetEarsSide(const bool isLeft, const struct vec2 pos)
506 {
507 SoundSetEar(isLeft, 0, pos);
508 SoundSetEar(isLeft, 1, pos);
509 }
510
SoundSetEars(const struct vec2 pos)511 void SoundSetEars(const struct vec2 pos)
512 {
513 SoundSetEarsSide(true, pos);
514 SoundSetEarsSide(false, pos);
515 }
516
SoundPlayAt(SoundDevice * device,Mix_Chunk * data,const struct vec2 pos)517 void SoundPlayAt(SoundDevice *device, Mix_Chunk *data, const struct vec2 pos)
518 {
519 SoundPlayAtPlusDistance(device, data, pos, 0);
520 }
521
IsPosNoSee(void * data,struct vec2i pos)522 static bool IsPosNoSee(void *data, struct vec2i pos)
523 {
524 const Tile *t = MapGetTile(data, Vec2iToTile(pos));
525 return t != NULL && TileIsOpaque(t);
526 }
SoundPlayAtPlusDistance(SoundDevice * device,Mix_Chunk * data,const struct vec2 pos,const int plusDistance)527 void SoundPlayAtPlusDistance(
528 SoundDevice *device, Mix_Chunk *data, const struct vec2 pos,
529 const int plusDistance)
530 {
531 if (device == NULL || !device->isInitialised)
532 {
533 return;
534 }
535 struct vec2 closestLeftEar, closestRightEar;
536
537 // Find closest set of ears to the sound
538 if (svec2_distance_squared(pos, device->earLeft1) <
539 svec2_distance_squared(pos, device->earLeft2))
540 {
541 closestLeftEar = device->earLeft1;
542 }
543 else
544 {
545 closestLeftEar = device->earLeft2;
546 }
547 if (svec2_distance_squared(pos, device->earRight1) <
548 svec2_distance_squared(pos, device->earRight2))
549 {
550 closestRightEar = device->earRight1;
551 }
552 else
553 {
554 closestRightEar = device->earRight2;
555 }
556
557 const struct vec2 origin = CalcClosestPointOnLineSegmentToPoint(
558 closestLeftEar, closestRightEar, pos);
559 HasClearLineData lineData;
560 lineData.IsBlocked = IsPosNoSee;
561 lineData.data = &gMap;
562 bool isMuffled = false;
563 if (!HasClearLineJMRaytrace(
564 svec2i_assign_vec2(pos), svec2i_assign_vec2(origin), &lineData))
565 {
566 isMuffled = true;
567 }
568 const struct vec2 dp = svec2_subtract(pos, origin);
569 SoundPlayAtPosition(
570 &gSoundDevice, data, svec2(dp.x, fabsf(dp.y) + plusDistance),
571 isMuffled);
572 }
573
574 static Mix_Chunk *SoundDataGet(SoundData *s);
StrSound(const char * s)575 Mix_Chunk *StrSound(const char *s)
576 {
577 if (s == NULL || strlen(s) == 0 || !gSoundDevice.isInitialised)
578 {
579 return NULL;
580 }
581 SoundData *sound;
582 int error = hashmap_get(gSoundDevice.customSounds, s, (any_t *)&sound);
583 if (error == MAP_OK)
584 {
585 return SoundDataGet(sound);
586 }
587 error = hashmap_get(gSoundDevice.sounds, s, (any_t *)&sound);
588 if (error == MAP_OK)
589 {
590 return SoundDataGet(sound);
591 }
592 return NULL;
593 }
SoundDataGet(SoundData * s)594 static Mix_Chunk *SoundDataGet(SoundData *s)
595 {
596 switch (s->Type)
597 {
598 case SOUND_NORMAL:
599 return s->u.normal;
600 case SOUND_RANDOM:
601 if (s->u.random.sounds.size == 0)
602 {
603 return NULL;
604 }
605 else
606 {
607 // Don't get the last sound used
608 int idx = s->u.random.lastPlayed;
609 while ((int)s->u.random.sounds.size > 1 &&
610 idx == s->u.random.lastPlayed)
611 {
612 idx = rand() % s->u.random.sounds.size;
613 }
614 Mix_Chunk **sound = CArrayGet(&s->u.random.sounds, idx);
615 s->u.random.lastPlayed = idx;
616 return *sound;
617 }
618 default:
619 CASSERT(false, "Unknown sound data type");
620 return NULL;
621 }
622 }
623