1 /*
2 * The ManaPlus Client
3 * Copyright (C) 2004-2009 The Mana World Development Team
4 * Copyright (C) 2009-2010 The Mana Developers
5 * Copyright (C) 2011-2019 The ManaPlus Developers
6 * Copyright (C) 2019-2021 Andrei Karas
7 *
8 * This file is part of The ManaPlus Client.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 #include "soundmanager.h"
25
26 #include "configuration.h"
27
28 #ifndef DYECMD
29 #include "being/localplayer.h"
30 #endif // DYECMD
31
32 #include "fs/virtfs/fs.h"
33
34 #include "resources/sdlmusic.h"
35 #include "resources/soundeffect.h"
36
37 #include "resources/loaders/musicloader.h"
38 #include "resources/loaders/soundloader.h"
39
40 #include "resources/resourcemanager/resourcemanager.h"
41
42 #ifdef DYECMD
43 #include "utils/cast.h"
44 #endif
45 #include "utils/checkutils.h"
46 #include "utils/sdlmusichelper.h"
47
48 PRAGMA48(GCC diagnostic push)
49 PRAGMA48(GCC diagnostic ignored "-Wshadow")
50 #include <SDL.h>
51 PRAGMA48(GCC diagnostic pop)
52
53 #include "debug.h"
54
55 SoundManager soundManager;
56
57 /**
58 * This will be set to true, when a music can be freed after a fade out
59 * Currently used by fadeOutCallBack()
60 */
61 static bool sFadingOutEnded = false;
62
63 /**
64 * Callback used at end of fadeout.
65 * It is called by Mix_MusicFadeFinished().
66 */
fadeOutCallBack()67 static void fadeOutCallBack()
68 {
69 sFadingOutEnded = true;
70 }
71
SoundManager()72 SoundManager::SoundManager() :
73 ConfigListener(),
74 mNextMusicFile(),
75 mInstalled(false),
76 mSfxVolume(100),
77 mMusicVolume(60),
78 mCurrentMusicFile(),
79 mMusic(nullptr),
80 mGuiChannel(-1),
81 mPlayBattle(false),
82 mPlayGui(false),
83 mPlayMusic(false),
84 mFadeoutMusic(true),
85 mCacheSounds(true)
86 {
87 // This set up our callback function used to
88 // handle fade outs endings.
89 sFadingOutEnded = false;
90 Mix_HookMusicFinished(&fadeOutCallBack);
91 }
92
~SoundManager()93 SoundManager::~SoundManager()
94 {
95 }
96
shutdown()97 void SoundManager::shutdown()
98 {
99 config.removeListeners(this);
100
101 // Unlink the callback function.
102 Mix_HookMusicFinished(nullptr);
103
104 CHECKLISTENERS
105 }
106
optionChanged(const std::string & value)107 void SoundManager::optionChanged(const std::string &value)
108 {
109 if (value == "playBattleSound")
110 mPlayBattle = config.getBoolValue("playBattleSound");
111 else if (value == "playGuiSound")
112 mPlayGui = config.getBoolValue("playGuiSound");
113 else if (value == "playMusic")
114 mPlayMusic = config.getBoolValue("playMusic");
115 else if (value == "sfxVolume")
116 setSfxVolume(config.getIntValue("sfxVolume"));
117 else if (value == "musicVolume")
118 setMusicVolume(config.getIntValue("musicVolume"));
119 else if (value == "fadeoutmusic")
120 mFadeoutMusic = (config.getIntValue("fadeoutmusic") != 0);
121 else if (value == "uselonglivesounds")
122 mCacheSounds = (config.getIntValue("uselonglivesounds") != 0);
123 else if (value == "parallelAudioChannels")
124 setChannels(config.getIntValue("parallelAudioChannels"));
125 }
126
init()127 void SoundManager::init()
128 {
129 // Don't initialize sound engine twice
130 if (mInstalled)
131 return;
132
133 logger->log1("SoundManager::init() Initializing sound...");
134
135 mPlayBattle = config.getBoolValue("playBattleSound");
136 mPlayGui = config.getBoolValue("playGuiSound");
137 mPlayMusic = config.getBoolValue("playMusic");
138 mFadeoutMusic = config.getBoolValue("fadeoutmusic");
139 mMusicVolume = config.getIntValue("musicVolume");
140 mSfxVolume = config.getIntValue("sfxVolume");
141 mCacheSounds = (config.getIntValue("uselonglivesounds") != 0);
142
143 config.addListener("playBattleSound", this);
144 config.addListener("playGuiSound", this);
145 config.addListener("playMusic", this);
146 config.addListener("sfxVolume", this);
147 config.addListener("musicVolume", this);
148 config.addListener("fadeoutmusic", this);
149 config.addListener("uselonglivesounds", this);
150 config.addListener("parallelAudioChannels", this);
151
152 if (SDL_InitSubSystem(SDL_INIT_AUDIO) == -1)
153 {
154 logger->log1("SoundManager::init() Failed to "
155 "initialize audio subsystem");
156 return;
157 }
158
159 const size_t audioBuffer = 4096;
160 int channels = config.getIntValue("audioChannels");
161 switch (channels)
162 {
163 case 3:
164 channels = 4;
165 break;
166 case 4:
167 channels = 6;
168 break;
169 default:
170 break;
171 }
172
173 const int res = SDL::MixOpenAudio(config.getIntValue("audioFrequency"),
174 MIX_DEFAULT_FORMAT, channels, audioBuffer);
175
176 if (res < 0)
177 {
178 logger->log("SoundManager::init Could not initialize audio: %s",
179 SDL_GetError());
180 if (SDL::MixOpenAudio(22010, MIX_DEFAULT_FORMAT, 2, audioBuffer) < 0)
181 return;
182 logger->log("Fallback to stereo audio");
183 }
184
185 Mix_AllocateChannels(config.getIntValue("parallelAudioChannels"));
186 Mix_VolumeMusic(mMusicVolume);
187 Mix_Volume(-1, mSfxVolume);
188
189 info();
190
191 mInstalled = true;
192
193 if (!mCurrentMusicFile.empty() && mPlayMusic)
194 playMusic(mCurrentMusicFile, SkipError_true);
195 }
196
testAudio()197 void SoundManager::testAudio()
198 {
199 mPlayBattle = config.getBoolValue("playBattleSound");
200 mPlayGui = config.getBoolValue("playGuiSound");
201 mPlayMusic = config.getBoolValue("playMusic");
202 mFadeoutMusic = config.getBoolValue("fadeoutmusic");
203 mMusicVolume = config.getIntValue("musicVolume");
204 mSfxVolume = config.getIntValue("sfxVolume");
205 mCacheSounds = (config.getIntValue("uselonglivesounds") != 0);
206
207 const size_t audioBuffer = 4096;
208 int channels = config.getIntValue("audioChannels");
209 switch (channels)
210 {
211 case 3:
212 channels = 4;
213 break;
214 case 4:
215 channels = 6;
216 break;
217 default:
218 break;
219 }
220
221 SDL_AudioSpec desired;
222 SDL_AudioSpec actual;
223
224 desired.freq = config.getIntValue("audioFrequency");
225 desired.format = MIX_DEFAULT_FORMAT;
226 desired.channels = CAST_U8(channels);
227 desired.samples = audioBuffer;
228 desired.callback = nullptr;
229 desired.userdata = nullptr;
230
231 if (SDL_OpenAudio(&desired, &actual) < 0)
232 {
233 logger->log("SoundManager::testAudio error: %s",
234 SDL_GetError());
235 return;
236 }
237 if (desired.freq != actual.freq)
238 {
239 logger->log("SoundManager::testAudio frequence: %d -> %d",
240 actual.freq, desired.freq);
241 }
242 if (desired.format != actual.format)
243 {
244 logger->log("SoundManager::testAudio format: %d -> %d",
245 actual.format, desired.format);
246 }
247 if (desired.channels != actual.channels)
248 {
249 logger->log("SoundManager::testAudio channels: %d -> %d",
250 actual.channels, desired.channels);
251 }
252 if (desired.samples != actual.samples)
253 {
254 logger->log("SoundManager::testAudio samples: %d -> %d",
255 actual.samples, desired.samples);
256 }
257 SDL_CloseAudio();
258 }
259
info()260 void SoundManager::info()
261 {
262 SDL_version compiledVersion;
263 const char *format = "Unknown";
264 int rate = 0;
265 uint16_t audioFormat = 0;
266 int channels = 0;
267
268 MIX_VERSION(&compiledVersion)
269 const SDL_version *const linkedVersion = Mix_Linked_Version();
270
271 #ifdef USE_SDL2
272 const char *driver = SDL_GetCurrentAudioDriver();
273 #else // USE_SDL2
274 char driver[40] = "Unknown";
275 SDL_AudioDriverName(driver, 40);
276 #endif // USE_SDL2
277
278 Mix_QuerySpec(&rate, &audioFormat, &channels);
279 switch (audioFormat)
280 {
281 case AUDIO_U8:
282 format = "U8";
283 break;
284 case AUDIO_S8:
285 format = "S8"; break;
286 case AUDIO_U16LSB:
287 format = "U16LSB";
288 break;
289 case AUDIO_S16LSB:
290 format = "S16LSB";
291 break;
292 case AUDIO_U16MSB:
293 format = "U16MSB";
294 break;
295 case AUDIO_S16MSB:
296 format = "S16MSB";
297 break;
298 default: break;
299 }
300
301 logger->log("SoundManager::info() SDL_mixer: %i.%i.%i (compiled)",
302 compiledVersion.major,
303 compiledVersion.minor,
304 compiledVersion.patch);
305 if (linkedVersion != nullptr)
306 {
307 logger->log("SoundManager::info() SDL_mixer: %i.%i.%i (linked)",
308 linkedVersion->major,
309 linkedVersion->minor,
310 linkedVersion->patch);
311 }
312 else
313 {
314 logger->log1("SoundManager::info() SDL_mixer: unknown");
315 }
316 logger->log("SoundManager::info() Driver: %s", driver);
317 logger->log("SoundManager::info() Format: %s", format);
318 logger->log("SoundManager::info() Rate: %i", rate);
319 logger->log("SoundManager::info() Channels: %i", channels);
320 }
321
setMusicVolume(const int volume)322 void SoundManager::setMusicVolume(const int volume)
323 {
324 mMusicVolume = volume;
325
326 if (mInstalled)
327 Mix_VolumeMusic(mMusicVolume);
328 }
329
setSfxVolume(const int volume)330 void SoundManager::setSfxVolume(const int volume)
331 {
332 mSfxVolume = volume;
333
334 if (mInstalled)
335 Mix_Volume(-1, mSfxVolume);
336 }
337
loadMusic(const std::string & fileName,const SkipError skipError)338 static SDLMusic *loadMusic(const std::string &fileName,
339 const SkipError skipError)
340 {
341 const std::string path = pathJoin(paths.getStringValue("music"),
342 fileName);
343 if (!VirtFs::exists(path))
344 {
345 if (skipError == SkipError_false)
346 reportAlways("Music file not found: %s", fileName.c_str())
347 return nullptr;
348 }
349 return Loader::getMusic(path);
350 }
351
playMusic(const std::string & fileName,const SkipError skipError)352 void SoundManager::playMusic(const std::string &fileName,
353 const SkipError skipError)
354 {
355 if (!mInstalled || !mPlayMusic)
356 return;
357
358 if (mCurrentMusicFile == fileName)
359 return;
360
361 mCurrentMusicFile = fileName;
362
363 haltMusic();
364
365 if (!fileName.empty())
366 {
367 mMusic = loadMusic(fileName,
368 skipError);
369 if (mMusic != nullptr)
370 mMusic->play(-1, 0);
371 }
372 }
373
stopMusic()374 void SoundManager::stopMusic()
375 {
376 haltMusic();
377 }
378
379 /*
380 void SoundManager::fadeInMusic(const std::string &fileName, const int ms)
381 {
382 mCurrentMusicFile = fileName;
383
384 if (!mInstalled || !mPlayMusic)
385 return;
386
387 haltMusic();
388
389 if (!fileName.empty())
390 {
391 mMusic = loadMusic(fileName);
392 if (mMusic)
393 mMusic->play(-1, ms);
394 }
395 }
396 */
397
fadeOutMusic(const int ms)398 void SoundManager::fadeOutMusic(const int ms)
399 {
400 if (!mPlayMusic)
401 return;
402
403 mCurrentMusicFile.clear();
404
405 if (!mInstalled)
406 return;
407
408 logger->log("SoundManager::fadeOutMusic() Fading-out (%i ms)", ms);
409
410 if ((mMusic != nullptr) && mFadeoutMusic)
411 {
412 Mix_FadeOutMusic(ms);
413 // Note: The fadeOutCallBack handler will take care about freeing
414 // the music file at fade out ending.
415 }
416 else
417 {
418 sFadingOutEnded = true;
419 if (!mFadeoutMusic)
420 haltMusic();
421 }
422 }
423
fadeOutAndPlayMusic(const std::string & fileName,const int ms)424 void SoundManager::fadeOutAndPlayMusic(const std::string &fileName,
425 const int ms)
426 {
427 if (!mPlayMusic)
428 return;
429
430 mNextMusicFile = fileName;
431 fadeOutMusic(ms);
432 }
433
logic()434 void SoundManager::logic()
435 {
436 BLOCK_START("SoundManager::logic")
437 if (sFadingOutEnded)
438 {
439 if (mMusic != nullptr)
440 {
441 mMusic->decRef();
442 mMusic = nullptr;
443 }
444 sFadingOutEnded = false;
445
446 if (!mNextMusicFile.empty())
447 {
448 playMusic(mNextMusicFile,
449 SkipError_false);
450 mNextMusicFile.clear();
451 }
452 }
453 BLOCK_END("SoundManager::logic")
454 }
455
456 #ifdef DYECMD
playSfx(const std::string & path A_UNUSED,const int x A_UNUSED,const int y A_UNUSED) const457 void SoundManager::playSfx(const std::string &path A_UNUSED,
458 const int x A_UNUSED,
459 const int y A_UNUSED) const
460 {
461 }
462 #else // DYECMD
playSfx(const std::string & path,const int x,const int y) const463 void SoundManager::playSfx(const std::string &path,
464 const int x, const int y) const
465 {
466 if (!mInstalled || path.empty() || !mPlayBattle)
467 return;
468
469 std::string tmpPath = pathJoin(paths.getStringValue("sfx"), path);
470 SoundEffect *const sample = Loader::getSoundEffect(tmpPath);
471 if (sample != nullptr)
472 {
473 logger->log("SoundManager::playSfx() Playing: %s", path.c_str());
474 int vol = 120;
475 if ((localPlayer != nullptr) && (x > 0 || y > 0))
476 {
477 int dx = localPlayer->getTileX() - x;
478 int dy = localPlayer->getTileY() - y;
479 if (dx < 0)
480 dx = -dx;
481 if (dy < 0)
482 dy = -dy;
483 const int dist = dx > dy ? dx : dy;
484 if (dist * 8 > vol)
485 return;
486
487 vol -= dist * 8;
488 }
489 sample->play(0, vol, -1);
490 if (!mCacheSounds)
491 sample->decRef();
492 }
493 }
494 #endif // DYECMD
495
playGuiSound(const std::string & name)496 void SoundManager::playGuiSound(const std::string &name)
497 {
498 const std::string sound = config.getStringValue(name);
499 if (sound == "(no sound)")
500 return;
501 playGuiSfx(pathJoin(branding.getStringValue("systemsounds"),
502 std::string(sound).append(".ogg")));
503 }
504
playGuiSfx(const std::string & path)505 void SoundManager::playGuiSfx(const std::string &path)
506 {
507 if (!mInstalled ||
508 !mPlayGui ||
509 path.empty())
510 {
511 return;
512 }
513
514 std::string tmpPath;
515 if (path.compare(0, 4, "sfx/") == 0)
516 tmpPath = path;
517 else
518 tmpPath = pathJoin(paths.getValue("sfx", "sfx"), path);
519 SoundEffect *const sample = Loader::getSoundEffect(tmpPath);
520 if (sample != nullptr)
521 {
522 logger->log("SoundManager::playGuiSfx() Playing: %s", path.c_str());
523 const int ret = static_cast<int>(sample->play(0, 120, mGuiChannel));
524 if (ret != -1)
525 mGuiChannel = ret;
526 if (!mCacheSounds)
527 sample->decRef();
528 }
529 }
530
close()531 void SoundManager::close()
532 {
533 if (!mInstalled)
534 return;
535
536 if (mMusic != nullptr)
537 {
538 Mix_HaltMusic();
539 ResourceManager::decRefDelete(mMusic);
540 mMusic = nullptr;
541 mCurrentMusicFile.clear();
542 }
543
544 logger->log1("SoundManager::close() Shutting down sound...");
545 Mix_CloseAudio();
546
547 mInstalled = false;
548 }
549
haltMusic()550 void SoundManager::haltMusic()
551 {
552 if (mMusic == nullptr)
553 return;
554
555 Mix_HaltMusic();
556 mMusic->decRef();
557 mMusic = nullptr;
558 mCurrentMusicFile.clear();
559 }
560
changeAudio()561 void SoundManager::changeAudio()
562 {
563 if (mInstalled)
564 close();
565 else
566 init();
567 }
568
volumeOff() const569 void SoundManager::volumeOff() const
570 {
571 if (mInstalled)
572 {
573 Mix_VolumeMusic(0);
574 Mix_Volume(-1, 0);
575 }
576 }
577
volumeRestore() const578 void SoundManager::volumeRestore() const
579 {
580 if (mInstalled)
581 {
582 Mix_VolumeMusic(mMusicVolume);
583 Mix_Volume(-1, mSfxVolume);
584 }
585 }
586
setChannels(const int channels) const587 void SoundManager::setChannels(const int channels) const
588 {
589 if (mInstalled)
590 Mix_AllocateChannels(channels);
591 }
592