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