CMusicHandler.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /*
  2. * CMusicHandler.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include <SDL_mixer.h>
  12. #include "CMusicHandler.h"
  13. #include "CGameInfo.h"
  14. #include "SDLRWwrapper.h"
  15. #include "../lib/JsonNode.h"
  16. #include "../lib/GameConstants.h"
  17. #include "../lib/filesystem/Filesystem.h"
  18. #include "../lib/StringConstants.h"
  19. #include "../lib/CRandomGenerator.h"
  20. #include "../lib/VCMIDirs.h"
  21. #include "../lib/Terrain.h"
  22. #define VCMI_SOUND_NAME(x)
  23. #define VCMI_SOUND_FILE(y) #y,
  24. // sounds mapped to soundBase enum
  25. static std::string sounds[] = {
  26. "", // invalid
  27. "", // todo
  28. VCMI_SOUND_LIST
  29. };
  30. #undef VCMI_SOUND_NAME
  31. #undef VCMI_SOUND_FILE
  32. // Not pretty, but there's only one music handler object in the game.
  33. static void soundFinishedCallbackC(int channel)
  34. {
  35. CCS->soundh->soundFinishedCallback(channel);
  36. }
  37. static void musicFinishedCallbackC()
  38. {
  39. CCS->musich->musicFinishedCallback();
  40. }
  41. void CAudioBase::init()
  42. {
  43. if (initialized)
  44. return;
  45. if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1)
  46. {
  47. logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError());
  48. return;
  49. }
  50. initialized = true;
  51. }
  52. void CAudioBase::release()
  53. {
  54. if(!(CCS->soundh->initialized && CCS->musich->initialized))
  55. Mix_CloseAudio();
  56. initialized = false;
  57. }
  58. void CAudioBase::setVolume(ui32 percent)
  59. {
  60. if (percent > 100)
  61. percent = 100;
  62. volume = percent;
  63. }
  64. void CSoundHandler::onVolumeChange(const JsonNode &volumeNode)
  65. {
  66. setVolume((ui32)volumeNode.Float());
  67. }
  68. CSoundHandler::CSoundHandler():
  69. listener(settings.listen["general"]["sound"]),
  70. ambientConfig(JsonNode(ResourceID("config/ambientSounds.json")))
  71. {
  72. allTilesSource = ambientConfig["allTilesSource"].Bool();
  73. listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
  74. // Vectors for helper(s)
  75. pickupSounds =
  76. {
  77. soundBase::pickup01, soundBase::pickup02, soundBase::pickup03,
  78. soundBase::pickup04, soundBase::pickup05, soundBase::pickup06, soundBase::pickup07
  79. };
  80. battleIntroSounds =
  81. {
  82. soundBase::battle00, soundBase::battle01,
  83. soundBase::battle02, soundBase::battle03, soundBase::battle04,
  84. soundBase::battle05, soundBase::battle06, soundBase::battle07
  85. };
  86. //predefine terrain set
  87. //TODO: need refactoring - support custom sounds for new terrains and load from json
  88. int h3mTerrId = 0;
  89. for(auto snd :
  90. {
  91. soundBase::horseDirt, soundBase::horseSand, soundBase::horseGrass,
  92. soundBase::horseSnow, soundBase::horseSwamp, soundBase::horseRough,
  93. soundBase::horseSubterranean, soundBase::horseLava,
  94. soundBase::horseWater, soundBase::horseRock
  95. })
  96. {
  97. horseSounds[Terrain::createTerrainTypeH3M(h3mTerrId++)] = snd;
  98. }
  99. for(auto & terrain : Terrain::Manager::terrains())
  100. {
  101. //since all sounds are hardcoded, let's keep it
  102. if(vstd::contains(horseSounds, terrain))
  103. continue;
  104. horseSounds[terrain] = horseSounds.at(Terrain::createTerrainTypeH3M(Terrain::Manager::getInfo(terrain).horseSoundId));
  105. }
  106. };
  107. void CSoundHandler::init()
  108. {
  109. CAudioBase::init();
  110. if(ambientConfig["allocateChannels"].isNumber())
  111. Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer());
  112. if (initialized)
  113. {
  114. // Load sounds
  115. Mix_ChannelFinished(soundFinishedCallbackC);
  116. }
  117. }
  118. void CSoundHandler::release()
  119. {
  120. if (initialized)
  121. {
  122. Mix_HaltChannel(-1);
  123. for (auto &chunk : soundChunks)
  124. {
  125. if (chunk.second.first)
  126. Mix_FreeChunk(chunk.second.first);
  127. }
  128. }
  129. CAudioBase::release();
  130. }
  131. // Allocate an SDL chunk and cache it.
  132. Mix_Chunk *CSoundHandler::GetSoundChunk(std::string &sound, bool cache)
  133. {
  134. try
  135. {
  136. if (cache && soundChunks.find(sound) != soundChunks.end())
  137. return soundChunks[sound].first;
  138. auto data = CResourceHandler::get()->load(ResourceID(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll();
  139. SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
  140. Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops
  141. if (cache)
  142. soundChunks.insert(std::pair<std::string, CachedChunk>(sound, std::make_pair (chunk, std::move (data.first))));
  143. return chunk;
  144. }
  145. catch(std::exception &e)
  146. {
  147. logGlobal->warn("Cannot get sound %s chunk: %s", sound, e.what());
  148. return nullptr;
  149. }
  150. }
  151. int CSoundHandler::ambientDistToVolume(int distance) const
  152. {
  153. if(distance >= ambientConfig["distances"].Vector().size())
  154. return 0;
  155. int volume = static_cast<int>(ambientConfig["distances"].Vector()[distance].Integer());
  156. return volume * (int)ambientConfig["volume"].Integer() * getVolume() / 10000;
  157. }
  158. void CSoundHandler::ambientStopSound(std::string soundId)
  159. {
  160. stopSound(ambientChannels[soundId]);
  161. setChannelVolume(ambientChannels[soundId], volume);
  162. }
  163. // Plays a sound, and return its channel so we can fade it out later
  164. int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
  165. {
  166. assert(soundID < soundBase::sound_after_last);
  167. auto sound = sounds[soundID];
  168. logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound);
  169. return playSound(sound, repeats, true);
  170. }
  171. int CSoundHandler::playSound(std::string sound, int repeats, bool cache)
  172. {
  173. if (!initialized || sound.empty())
  174. return -1;
  175. int channel;
  176. Mix_Chunk *chunk = GetSoundChunk(sound, cache);
  177. if (chunk)
  178. {
  179. channel = Mix_PlayChannel(-1, chunk, repeats);
  180. if (channel == -1)
  181. {
  182. logGlobal->error("Unable to play sound file %s , error %s", sound, Mix_GetError());
  183. if (!cache)
  184. Mix_FreeChunk(chunk);
  185. }
  186. else if (cache)
  187. callbacks[channel];
  188. else
  189. callbacks[channel] = [chunk](){ Mix_FreeChunk(chunk);};
  190. }
  191. else
  192. channel = -1;
  193. return channel;
  194. }
  195. // Helper. Randomly select a sound from an array and play it
  196. int CSoundHandler::playSoundFromSet(std::vector<soundBase::soundID> &sound_vec)
  197. {
  198. return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault()));
  199. }
  200. void CSoundHandler::stopSound( int handler )
  201. {
  202. if (initialized && handler != -1)
  203. Mix_HaltChannel(handler);
  204. }
  205. // Sets the sound volume, from 0 (mute) to 100
  206. void CSoundHandler::setVolume(ui32 percent)
  207. {
  208. CAudioBase::setVolume(percent);
  209. if (initialized)
  210. setChannelVolume(-1, volume);
  211. }
  212. // Sets the sound volume, from 0 (mute) to 100
  213. void CSoundHandler::setChannelVolume(int channel, ui32 percent)
  214. {
  215. Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100);
  216. }
  217. void CSoundHandler::setCallback(int channel, std::function<void()> function)
  218. {
  219. std::map<int, std::function<void()> >::iterator iter;
  220. iter = callbacks.find(channel);
  221. //channel not found. It may have finished so fire callback now
  222. if(iter == callbacks.end())
  223. function();
  224. else
  225. iter->second = function;
  226. }
  227. void CSoundHandler::soundFinishedCallback(int channel)
  228. {
  229. std::map<int, std::function<void()> >::iterator iter;
  230. iter = callbacks.find(channel);
  231. if (iter == callbacks.end())
  232. return;
  233. auto callback = std::move(iter->second);
  234. callbacks.erase(iter);
  235. if (callback)
  236. callback();
  237. }
  238. int CSoundHandler::ambientGetRange() const
  239. {
  240. return static_cast<int>(ambientConfig["range"].Integer());
  241. }
  242. bool CSoundHandler::ambientCheckVisitable() const
  243. {
  244. return !allTilesSource;
  245. }
  246. void CSoundHandler::ambientUpdateChannels(std::map<std::string, int> soundsArg)
  247. {
  248. boost::mutex::scoped_lock guard(mutex);
  249. std::vector<std::string> stoppedSounds;
  250. for(auto & pair : ambientChannels)
  251. {
  252. if(!vstd::contains(soundsArg, pair.first))
  253. {
  254. ambientStopSound(pair.first);
  255. stoppedSounds.push_back(pair.first);
  256. }
  257. else
  258. {
  259. CCS->soundh->setChannelVolume(pair.second, ambientDistToVolume(soundsArg[pair.first]));
  260. }
  261. }
  262. for(auto soundId : stoppedSounds)
  263. ambientChannels.erase(soundId);
  264. for(auto & pair : soundsArg)
  265. {
  266. if(!vstd::contains(ambientChannels, pair.first))
  267. {
  268. int channel = CCS->soundh->playSound(pair.first, -1);
  269. CCS->soundh->setChannelVolume(channel, ambientDistToVolume(pair.second));
  270. CCS->soundh->ambientChannels.insert(std::make_pair(pair.first, channel));
  271. }
  272. }
  273. }
  274. void CSoundHandler::ambientStopAllChannels()
  275. {
  276. boost::mutex::scoped_lock guard(mutex);
  277. for(auto ch : ambientChannels)
  278. {
  279. ambientStopSound(ch.first);
  280. }
  281. ambientChannels.clear();
  282. }
  283. void CMusicHandler::onVolumeChange(const JsonNode &volumeNode)
  284. {
  285. setVolume((ui32)volumeNode.Float());
  286. }
  287. CMusicHandler::CMusicHandler():
  288. listener(settings.listen["general"]["music"])
  289. {
  290. listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
  291. auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourceID & id) -> bool
  292. {
  293. if(id.getType() != EResType::MUSIC)
  294. return false;
  295. if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
  296. return false;
  297. logGlobal->trace("Found music file %s", id.getName());
  298. return true;
  299. });
  300. for(const ResourceID & file : mp3files)
  301. {
  302. if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
  303. addEntryToSet("battle", file.getName(), file.getName());
  304. else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
  305. addEntryToSet("enemy-turn", file.getName(), file.getName());
  306. }
  307. for(auto & terrain : Terrain::Manager::terrains())
  308. {
  309. auto & entry = Terrain::Manager::getInfo(terrain);
  310. addEntryToSet("terrain", terrain, "Music/" + entry.musicFilename);
  311. }
  312. }
  313. void CMusicHandler::addEntryToSet(const std::string & set, const std::string & musicID, const std::string & musicURI)
  314. {
  315. musicsSet[set][musicID] = musicURI;
  316. }
  317. void CMusicHandler::init()
  318. {
  319. CAudioBase::init();
  320. if (initialized)
  321. Mix_HookMusicFinished(musicFinishedCallbackC);
  322. }
  323. void CMusicHandler::release()
  324. {
  325. if (initialized)
  326. {
  327. boost::mutex::scoped_lock guard(mutex);
  328. Mix_HookMusicFinished(nullptr);
  329. current.reset();
  330. next.reset();
  331. }
  332. CAudioBase::release();
  333. }
  334. void CMusicHandler::playMusic(const std::string & musicURI, bool loop)
  335. {
  336. if (current && current->isTrack(musicURI))
  337. return;
  338. queueNext(this, "", musicURI, loop);
  339. }
  340. void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop)
  341. {
  342. auto selectedSet = musicsSet.find(whichSet);
  343. if (selectedSet == musicsSet.end())
  344. {
  345. logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
  346. return;
  347. }
  348. if (current && current->isSet(whichSet))
  349. return;
  350. // in this mode - play random track from set
  351. queueNext(this, whichSet, "", loop);
  352. }
  353. void CMusicHandler::playMusicFromSet(const std::string & whichSet, const std::string & entryID, bool loop)
  354. {
  355. auto selectedSet = musicsSet.find(whichSet);
  356. if (selectedSet == musicsSet.end())
  357. {
  358. logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
  359. return;
  360. }
  361. auto selectedEntry = selectedSet->second.find(entryID);
  362. if (selectedEntry == selectedSet->second.end())
  363. {
  364. logGlobal->error("Error: playing non-existing entry %s from set: %s", entryID, whichSet);
  365. return;
  366. }
  367. if (current && current->isTrack(selectedEntry->second))
  368. return;
  369. // in this mode - play specific track from set
  370. queueNext(this, "", selectedEntry->second, loop);
  371. }
  372. void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
  373. {
  374. if (!initialized)
  375. return;
  376. boost::mutex::scoped_lock guard(mutex);
  377. next = std::move(queued);
  378. if (current.get() == nullptr || !current->stop(1000))
  379. {
  380. current.reset(next.release());
  381. current->play();
  382. }
  383. }
  384. void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const std::string & musicURI, bool looped)
  385. {
  386. try
  387. {
  388. queueNext(make_unique<MusicEntry>(owner, setName, musicURI, looped));
  389. }
  390. catch(std::exception &e)
  391. {
  392. logGlobal->error("Failed to queue music. setName=%s\tmusicURI=%s", setName, musicURI);
  393. logGlobal->error("Exception: %s", e.what());
  394. }
  395. }
  396. void CMusicHandler::stopMusic(int fade_ms)
  397. {
  398. if (!initialized)
  399. return;
  400. boost::mutex::scoped_lock guard(mutex);
  401. if (current.get() != nullptr)
  402. current->stop(fade_ms);
  403. next.reset();
  404. }
  405. void CMusicHandler::setVolume(ui32 percent)
  406. {
  407. CAudioBase::setVolume(percent);
  408. if (initialized)
  409. Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100);
  410. }
  411. void CMusicHandler::musicFinishedCallback()
  412. {
  413. boost::mutex::scoped_lock guard(mutex);
  414. if (current.get() != nullptr)
  415. {
  416. //return if current music still not finished
  417. if (current->play())
  418. return;
  419. else
  420. current.reset();
  421. }
  422. if (current.get() == nullptr && next.get() != nullptr)
  423. {
  424. current.reset(next.release());
  425. current->play();
  426. }
  427. }
  428. MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped):
  429. owner(owner),
  430. music(nullptr),
  431. loop(looped ? -1 : 1),
  432. setName(std::move(setName))
  433. {
  434. if (!musicURI.empty())
  435. load(std::move(musicURI));
  436. }
  437. MusicEntry::~MusicEntry()
  438. {
  439. logGlobal->trace("Del-ing music file %s", currentName);
  440. if (music)
  441. Mix_FreeMusic(music);
  442. }
  443. void MusicEntry::load(std::string musicURI)
  444. {
  445. if (music)
  446. {
  447. logGlobal->trace("Del-ing music file %s", currentName);
  448. Mix_FreeMusic(music);
  449. music = nullptr;
  450. }
  451. currentName = musicURI;
  452. logGlobal->trace("Loading music file %s", musicURI);
  453. auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::MUSIC)));
  454. music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
  455. if(!music)
  456. {
  457. logGlobal->warn("Warning: Cannot open %s: %s", currentName, Mix_GetError());
  458. return;
  459. }
  460. }
  461. bool MusicEntry::play()
  462. {
  463. if (!(loop--) && music) //already played once - return
  464. return false;
  465. if (!setName.empty())
  466. {
  467. const auto & set = owner->musicsSet[setName];
  468. load(RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault())->second);
  469. }
  470. logGlobal->trace("Playing music file %s", currentName);
  471. if(Mix_PlayMusic(music, 1) == -1)
  472. {
  473. logGlobal->error("Unable to play music (%s)", Mix_GetError());
  474. return false;
  475. }
  476. return true;
  477. }
  478. bool MusicEntry::stop(int fade_ms)
  479. {
  480. if (Mix_PlayingMusic())
  481. {
  482. logGlobal->trace("Stopping music file %s", currentName);
  483. loop = 0;
  484. Mix_FadeOutMusic(fade_ms);
  485. return true;
  486. }
  487. return false;
  488. }
  489. bool MusicEntry::isSet(std::string set)
  490. {
  491. return !setName.empty() && set == setName;
  492. }
  493. bool MusicEntry::isTrack(std::string track)
  494. {
  495. return setName.empty() && track == currentName;
  496. }