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