CMusicHandler.cpp 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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 "CMusicHandler.h"
  12. #include "../CGameInfo.h"
  13. #include "../eventsSDL/InputHandler.h"
  14. #include "../gui/CGuiHandler.h"
  15. #include "../renderSDL/SDLRWwrapper.h"
  16. #include "../../lib/entities/faction/CFaction.h"
  17. #include "../../lib/entities/faction/CTown.h"
  18. #include "../../lib/entities/faction/CTownHandler.h"
  19. #include "../../lib/CRandomGenerator.h"
  20. #include "../../lib/TerrainHandler.h"
  21. #include "../../lib/filesystem/Filesystem.h"
  22. #include <SDL_mixer.h>
  23. void CMusicHandler::onVolumeChange(const JsonNode & volumeNode)
  24. {
  25. setVolume(volumeNode.Integer());
  26. }
  27. CMusicHandler::CMusicHandler():
  28. listener(settings.listen["general"]["music"])
  29. {
  30. listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
  31. auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool
  32. {
  33. if(id.getType() != EResType::SOUND)
  34. return false;
  35. if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
  36. return false;
  37. logGlobal->trace("Found music file %s", id.getName());
  38. return true;
  39. });
  40. for(const ResourcePath & file : mp3files)
  41. {
  42. if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
  43. addEntryToSet("battle", AudioPath::fromResource(file));
  44. else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
  45. addEntryToSet("enemy-turn", AudioPath::fromResource(file));
  46. }
  47. if (isInitialized())
  48. {
  49. Mix_HookMusicFinished([]()
  50. {
  51. CCS->musich->musicFinishedCallback();
  52. });
  53. }
  54. }
  55. void CMusicHandler::loadTerrainMusicThemes()
  56. {
  57. for(const auto & terrain : CGI->terrainTypeHandler->objects)
  58. {
  59. for(const auto & filename : terrain->musicFilename)
  60. addEntryToSet("terrain_" + terrain->getJsonKey(), filename);
  61. }
  62. for(const auto & faction : CGI->townh->objects)
  63. {
  64. if (!faction || !faction->hasTown())
  65. continue;
  66. for(const auto & filename : faction->town->clientInfo.musicTheme)
  67. addEntryToSet("faction_" + faction->getJsonKey(), filename);
  68. }
  69. }
  70. void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI)
  71. {
  72. musicsSet[set].push_back(musicURI);
  73. }
  74. CMusicHandler::~CMusicHandler()
  75. {
  76. if(isInitialized())
  77. {
  78. boost::mutex::scoped_lock guard(mutex);
  79. Mix_HookMusicFinished(nullptr);
  80. current->stop();
  81. current.reset();
  82. next.reset();
  83. }
  84. }
  85. void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart)
  86. {
  87. boost::mutex::scoped_lock guard(mutex);
  88. if(current && current->isPlaying() && current->isTrack(musicURI))
  89. return;
  90. queueNext(this, "", musicURI, loop, fromStart);
  91. }
  92. void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart)
  93. {
  94. playMusicFromSet(musicSet + "_" + entryID, loop, fromStart);
  95. }
  96. void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
  97. {
  98. boost::mutex::scoped_lock guard(mutex);
  99. auto selectedSet = musicsSet.find(whichSet);
  100. if(selectedSet == musicsSet.end())
  101. {
  102. logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
  103. return;
  104. }
  105. if(current && current->isPlaying() && current->isSet(whichSet))
  106. return;
  107. // in this mode - play random track from set
  108. queueNext(this, whichSet, AudioPath(), loop, fromStart);
  109. }
  110. void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
  111. {
  112. if(!isInitialized())
  113. return;
  114. next = std::move(queued);
  115. if(current == nullptr || !current->stop(1000))
  116. {
  117. current.reset(next.release());
  118. current->play();
  119. }
  120. }
  121. void CMusicHandler::queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart)
  122. {
  123. queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
  124. }
  125. void CMusicHandler::stopMusic(int fade_ms)
  126. {
  127. if(!isInitialized())
  128. return;
  129. boost::mutex::scoped_lock guard(mutex);
  130. if(current != nullptr)
  131. current->stop(fade_ms);
  132. next.reset();
  133. }
  134. ui32 CMusicHandler::getVolume() const
  135. {
  136. return volume;
  137. }
  138. void CMusicHandler::setVolume(ui32 percent)
  139. {
  140. volume = std::min(100u, percent);
  141. if(isInitialized())
  142. Mix_VolumeMusic((MIX_MAX_VOLUME * volume) / 100);
  143. }
  144. void CMusicHandler::musicFinishedCallback()
  145. {
  146. // call music restart in separate thread to avoid deadlock in some cases
  147. // It is possible for:
  148. // 1) SDL thread to call this method on end of playback
  149. // 2) VCMI code to call queueNext() method to queue new file
  150. // this leads to:
  151. // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
  152. // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
  153. GH.dispatchMainThread(
  154. [this]()
  155. {
  156. boost::unique_lock lockGuard(mutex);
  157. if(current != nullptr)
  158. {
  159. // if music is looped, play it again
  160. if(current->play())
  161. return;
  162. else
  163. current.reset();
  164. }
  165. if(current == nullptr && next != nullptr)
  166. {
  167. current.reset(next.release());
  168. current->play();
  169. }
  170. }
  171. );
  172. }
  173. MusicEntry::MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart)
  174. : owner(owner)
  175. , music(nullptr)
  176. , setName(std::move(setName))
  177. , startTime(static_cast<uint32_t>(-1))
  178. , startPosition(0)
  179. , loop(looped ? -1 : 1)
  180. , fromStart(fromStart)
  181. , playing(false)
  182. {
  183. if(!musicURI.empty())
  184. load(musicURI);
  185. }
  186. MusicEntry::~MusicEntry()
  187. {
  188. if(playing && loop > 0)
  189. {
  190. assert(0);
  191. logGlobal->error("Attempt to delete music while playing!");
  192. Mix_HaltMusic();
  193. }
  194. if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
  195. {
  196. assert(0);
  197. logGlobal->error("Attempt to delete music while fading out!");
  198. Mix_HaltMusic();
  199. }
  200. logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
  201. if(music)
  202. Mix_FreeMusic(music);
  203. }
  204. void MusicEntry::load(const AudioPath & musicURI)
  205. {
  206. if(music)
  207. {
  208. logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
  209. Mix_FreeMusic(music);
  210. music = nullptr;
  211. }
  212. if(CResourceHandler::get()->existsResource(musicURI))
  213. currentName = musicURI;
  214. else
  215. currentName = musicURI.addPrefix("MUSIC/");
  216. music = nullptr;
  217. logGlobal->trace("Loading music file %s", currentName.getOriginalName());
  218. try
  219. {
  220. std::unique_ptr<CInputStream> stream = CResourceHandler::get()->load(currentName);
  221. if(musicURI.getName() == "BLADEFWCAMPAIGN") // handle defect MP3 file - ffprobe says: Skipping 52 bytes of junk at 0.
  222. stream->seek(52);
  223. auto * musicFile = MakeSDLRWops(std::move(stream));
  224. music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
  225. }
  226. catch(std::exception & e)
  227. {
  228. logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
  229. logGlobal->error("Exception: %s", e.what());
  230. }
  231. if(!music)
  232. {
  233. logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError());
  234. return;
  235. }
  236. }
  237. bool MusicEntry::play()
  238. {
  239. if(!(loop--) && music) //already played once - return
  240. return false;
  241. if(!setName.empty())
  242. {
  243. const auto & set = owner->musicsSet[setName];
  244. const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault());
  245. load(*iter);
  246. }
  247. logGlobal->trace("Playing music file %s", currentName.getOriginalName());
  248. if(!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0)
  249. {
  250. float timeToStart = owner->trackPositions[currentName];
  251. startPosition = std::round(timeToStart * 1000);
  252. // erase stored position:
  253. // if music track will be interrupted again - new position will be written in stop() method
  254. // if music track is not interrupted and will finish by timeout/end of file - it will restart from beginning as it should
  255. owner->trackPositions.erase(owner->trackPositions.find(currentName));
  256. if(Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1)
  257. {
  258. logGlobal->error("Unable to play music (%s)", Mix_GetError());
  259. return false;
  260. }
  261. }
  262. else
  263. {
  264. startPosition = 0;
  265. if(Mix_PlayMusic(music, 1) == -1)
  266. {
  267. logGlobal->error("Unable to play music (%s)", Mix_GetError());
  268. return false;
  269. }
  270. }
  271. startTime = GH.input().getTicks();
  272. playing = true;
  273. return true;
  274. }
  275. bool MusicEntry::stop(int fade_ms)
  276. {
  277. if(Mix_PlayingMusic())
  278. {
  279. playing = false;
  280. loop = 0;
  281. uint32_t endTime = GH.input().getTicks();
  282. assert(startTime != uint32_t(-1));
  283. float playDuration = (endTime - startTime + startPosition) / 1000.f;
  284. owner->trackPositions[currentName] = playDuration;
  285. logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration);
  286. Mix_FadeOutMusic(fade_ms);
  287. return true;
  288. }
  289. return false;
  290. }
  291. bool MusicEntry::isPlaying() const
  292. {
  293. return playing;
  294. }
  295. bool MusicEntry::isSet(const std::string & set)
  296. {
  297. return !setName.empty() && set == setName;
  298. }
  299. bool MusicEntry::isTrack(const AudioPath & track)
  300. {
  301. return setName.empty() && track == currentName;
  302. }