diff --git a/client/CGameInfo.h b/client/CGameInfo.h index edc8f46f3..08e649825 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -36,21 +36,21 @@ class CMap; VCMI_LIB_NAMESPACE_END class CMapHandler; -class CSoundHandler; -class CMusicHandler; +class ISoundPlayer; +class IMusicPlayer; class CursorHandler; -class IMainVideoPlayer; +class IVideoPlayer; class CServerHandler; //a class for non-mechanical client GUI classes class CClientState { public: - CSoundHandler * soundh; - CMusicHandler * musich; + ISoundPlayer * soundh; + IMusicPlayer * musich; CConsoleHandler * consoleh; CursorHandler * curh; - IMainVideoPlayer * videoh; + IVideoPlayer * videoh; }; extern CClientState * CCS; diff --git a/client/CMT.cpp b/client/CMT.cpp index be8e262d5..adcf29da9 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -14,11 +14,13 @@ #include "CGameInfo.h" #include "mainmenu/CMainMenu.h" +#include "media/CEmptyVideoPlayer.h" +#include "media/CMusicHandler.h" +#include "media/CSoundHandler.h" +#include "media/CVideoHandler.h" #include "gui/CursorHandler.h" #include "eventsSDL/InputHandler.h" #include "CPlayerInterface.h" -#include "CVideoHandler.h" -#include "CMusicHandler.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "CServerHandler.h" @@ -292,10 +294,8 @@ int main(int argc, char * argv[]) { //initializing audio CCS->soundh = new CSoundHandler(); - CCS->soundh->init(); CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float()); CCS->musich = new CMusicHandler(); - CCS->musich->init(); CCS->musich->setVolume((ui32)settings["general"]["music"].Float()); logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); } @@ -396,20 +396,13 @@ int main(int argc, char * argv[]) //plays intro, ends when intro is over or button has been pressed (handles events) void playIntro() { - auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); - int sound = CCS->soundh->playSound(audioData); - if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); - sound = CCS->soundh->playSound(audioData); - if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); - sound = CCS->soundh->playSound(audioData); - CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO); - } - } - CCS->soundh->stopSound(sound); + if(!CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK"))) + return; + + if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) + return; + + CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); } static void mainLoop() @@ -457,9 +450,6 @@ static void mainLoop() // cleanup, mostly to remove false leaks from analyzer if(CCS) { - CCS->musich->release(); - CCS->soundh->release(); - delete CCS->consoleh; delete CCS->curh; delete CCS->videoh; diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0e8b0d562..9c3d4ef54 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -75,6 +75,11 @@ set(client_SRCS mapView/MapViewModel.cpp mapView/mapHandler.cpp + media/CAudioBase.cpp + media/CMusicHandler.cpp + media/CSoundHandler.cpp + media/CVideoHandler.cpp + render/CAnimation.cpp render/CBitmapHandler.cpp render/CDefFile.cpp @@ -126,6 +131,7 @@ set(client_SRCS widgets/CArtifactsOfHeroMarket.cpp widgets/CArtifactsOfHeroBackpack.cpp widgets/RadialMenu.cpp + widgets/VideoWidget.cpp widgets/markets/CAltarArtifacts.cpp widgets/markets/CAltarCreatures.cpp widgets/markets/CArtifactsBuying.cpp @@ -163,11 +169,9 @@ set(client_SRCS CGameInfo.cpp CMT.cpp - CMusicHandler.cpp CPlayerInterface.cpp PlayerLocalState.cpp CServerHandler.cpp - CVideoHandler.cpp Client.cpp ClientCommandManager.cpp GameChatHandler.cpp @@ -260,6 +264,15 @@ set(client_HEADERS mapView/MapViewModel.h mapView/mapHandler.h + media/CAudioBase.h + media/CEmptyVideoPlayer.h + media/CMusicHandler.h + media/CSoundHandler.h + media/CVideoHandler.h + media/IMusicPlayer.h + media/ISoundPlayer.h + media/IVideoPlayer.h + render/CAnimation.h render/CBitmapHandler.h render/CDefFile.h @@ -320,6 +333,7 @@ set(client_HEADERS widgets/CArtifactsOfHeroMarket.h widgets/CArtifactsOfHeroBackpack.h widgets/RadialMenu.h + widgets/VideoWidget.h widgets/markets/CAltarArtifacts.h widgets/markets/CAltarCreatures.h widgets/markets/CArtifactsBuying.h @@ -357,11 +371,9 @@ set(client_HEADERS CGameInfo.h CMT.h - CMusicHandler.h CPlayerInterface.h PlayerLocalState.h CServerHandler.h - CVideoHandler.h Client.h ClientCommandManager.h ClientNetPackVisitors.h diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp deleted file mode 100644 index 00edf149c..000000000 --- a/client/CMusicHandler.cpp +++ /dev/null @@ -1,753 +0,0 @@ -/* - * CMusicHandler.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include -#include - -#include "CMusicHandler.h" -#include "CGameInfo.h" -#include "renderSDL/SDLRWwrapper.h" -#include "eventsSDL/InputHandler.h" -#include "gui/CGuiHandler.h" - -#include "../lib/GameConstants.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/constants/StringConstants.h" -#include "../lib/CRandomGenerator.h" -#include "../lib/VCMIDirs.h" -#include "../lib/TerrainHandler.h" - - -#define VCMI_SOUND_NAME(x) -#define VCMI_SOUND_FILE(y) #y, - -// sounds mapped to soundBase enum -static const std::string sounds[] = { - "", // invalid - "", // todo - VCMI_SOUND_LIST -}; -#undef VCMI_SOUND_NAME -#undef VCMI_SOUND_FILE - -void CAudioBase::init() -{ - if (initialized) - return; - - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) - { - logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); - return; - } - - initialized = true; -} - -void CAudioBase::release() -{ - if(!(CCS->soundh->initialized && CCS->musich->initialized)) - Mix_CloseAudio(); - - initialized = false; -} - -void CAudioBase::setVolume(ui32 percent) -{ - if (percent > 100) - percent = 100; - - volume = percent; -} - -void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CSoundHandler::CSoundHandler(): - listener(settings.listen["general"]["sound"]), - ambientConfig(JsonPath::builtin("config/ambientSounds.json")) -{ - listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); - - battleIntroSounds = - { - soundBase::battle00, soundBase::battle01, - soundBase::battle02, soundBase::battle03, soundBase::battle04, - soundBase::battle05, soundBase::battle06, soundBase::battle07 - }; -} - -void CSoundHandler::init() -{ - CAudioBase::init(); - if(ambientConfig["allocateChannels"].isNumber()) - Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer()); - - if (initialized) - { - Mix_ChannelFinished([](int channel) - { - CCS->soundh->soundFinishedCallback(channel); - }); - } -} - -void CSoundHandler::release() -{ - if (initialized) - { - Mix_HaltChannel(-1); - - for (auto &chunk : soundChunks) - { - if (chunk.second.first) - Mix_FreeChunk(chunk.second.first); - } - } - - CAudioBase::release(); -} - -// Allocate an SDL chunk and cache it. -Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) -{ - try - { - if (cache && soundChunks.find(sound) != soundChunks.end()) - return soundChunks[sound].first; - - auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); - return nullptr; - } -} - -Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) -{ - try - { - std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); - - if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) - return soundChunksRaw[startBytes].first; - - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound chunk: %s", e.what()); - return nullptr; - } -} - -int CSoundHandler::ambientDistToVolume(int distance) const -{ - const auto & distancesVector = ambientConfig["distances"].Vector(); - - if(distance >= distancesVector.size()) - return 0; - - int volume = static_cast(distancesVector[distance].Integer()); - return volume * (int)ambientConfig["volume"].Integer() / 100; -} - -void CSoundHandler::ambientStopSound(const AudioPath & soundId) -{ - stopSound(ambientChannels[soundId]); - setChannelVolume(ambientChannels[soundId], volume); -} - -uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) -{ - if (!initialized || sound.empty()) - return 0; - - auto resourcePath = sound.addPrefix("SOUNDS/"); - - if (!CResourceHandler::get()->existsResource(resourcePath)) - return 0; - - auto data = CResourceHandler::get()->load(resourcePath)->readAll(); - - SDL_AudioSpec spec; - uint32_t audioLen; - uint8_t *audioBuf; - uint32_t miliseconds = 0; - - if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) - { - SDL_FreeWAV(audioBuf); - uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8; - uint32_t sampleCount = audioLen / sampleSize; - uint32_t sampleLen = sampleCount / spec.channels; - miliseconds = 1000 * sampleLen / spec.freq; - } - - return miliseconds ; -} - -// Plays a sound, and return its channel so we can fade it out later -int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) -{ - assert(soundID < soundBase::sound_after_last); - auto sound = AudioPath::builtin(sounds[soundID]); - logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); - - return playSound(sound, repeats, true); -} - -int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) -{ - if (!initialized || sound.empty()) - return -1; - - int channel; - Mix_Chunk *chunk = GetSoundChunk(sound, cache); - - if (chunk) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - else - channel = -1; - - return channel; -} - -int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) -{ - int channel = -1; - if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound, error %s", Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - return channel; -} - -// Helper. Randomly select a sound from an array and play it -int CSoundHandler::playSoundFromSet(std::vector &sound_vec) -{ - return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); -} - -void CSoundHandler::stopSound(int handler) -{ - if (initialized && handler != -1) - Mix_HaltChannel(handler); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - { - setChannelVolume(-1, volume); - - for (auto const & channel : channelVolumes) - updateChannelVolume(channel.first); - } -} - -void CSoundHandler::updateChannelVolume(int channel) -{ - if (channelVolumes.count(channel)) - setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); - else - setChannelVolume(channel, getVolume()); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setChannelVolume(int channel, ui32 percent) -{ - Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); -} - -void CSoundHandler::setCallback(int channel, std::function function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - auto iter = callbacks.find(channel); - - //channel not found. It may have finished so fire callback now - if(iter == callbacks.end()) - function(); - else - iter->second.push_back(function); -} - -void CSoundHandler::resetCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - callbacks.erase(channel); -} - -void CSoundHandler::soundFinishedCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - if (callbacks.count(channel) == 0) - return; - - // store callbacks from container locally - SDL might reuse this channel for another sound - // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own - auto callback = callbacks.at(channel); - callbacks.erase(channel); - - if (!callback.empty()) - { - GH.dispatchMainThread([callback](){ - for (auto entry : callback) - entry(); - }); - } -} - -void CSoundHandler::initCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel] = {}; -} - -void CSoundHandler::initCallback(int channel, const std::function & function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel].push_back(function); -} - -int CSoundHandler::ambientGetRange() const -{ - return static_cast(ambientConfig["range"].Integer()); -} - -void CSoundHandler::ambientUpdateChannels(std::map soundsArg) -{ - boost::mutex::scoped_lock guard(mutex); - - std::vector stoppedSounds; - for(auto & pair : ambientChannels) - { - const auto & soundId = pair.first; - const int channel = pair.second; - - if(!vstd::contains(soundsArg, soundId)) - { - ambientStopSound(soundId); - stoppedSounds.push_back(soundId); - } - else - { - int volume = ambientDistToVolume(soundsArg[soundId]); - channelVolumes[channel] = volume; - updateChannelVolume(channel); - } - } - for(auto soundId : stoppedSounds) - { - channelVolumes.erase(ambientChannels[soundId]); - ambientChannels.erase(soundId); - } - - for(auto & pair : soundsArg) - { - const auto & soundId = pair.first; - const int distance = pair.second; - - if(!vstd::contains(ambientChannels, soundId)) - { - int channel = playSound(soundId, -1); - int volume = ambientDistToVolume(distance); - channelVolumes[channel] = volume; - - updateChannelVolume(channel); - ambientChannels[soundId] = channel; - } - } -} - -void CSoundHandler::ambientStopAllChannels() -{ - boost::mutex::scoped_lock guard(mutex); - - for(auto ch : ambientChannels) - { - ambientStopSound(ch.first); - } - channelVolumes.clear(); - ambientChannels.clear(); -} - -void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CMusicHandler::CMusicHandler(): - listener(settings.listen["general"]["music"]) -{ - listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); - - auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool - { - if(id.getType() != EResType::SOUND) - return false; - - if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) - return false; - - logGlobal->trace("Found music file %s", id.getName()); - return true; - }); - - for(const ResourcePath & file : mp3files) - { - if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) - addEntryToSet("battle", AudioPath::fromResource(file)); - else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) - addEntryToSet("enemy-turn", AudioPath::fromResource(file)); - } - -} - -void CMusicHandler::loadTerrainMusicThemes() -{ - for (const auto & terrain : CGI->terrainTypeHandler->objects) - { - addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); - } -} - -void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) -{ - musicsSet[set].push_back(musicURI); -} - -void CMusicHandler::init() -{ - CAudioBase::init(); - - if (initialized) - { - Mix_HookMusicFinished([]() - { - CCS->musich->musicFinishedCallback(); - }); - } -} - -void CMusicHandler::release() -{ - if (initialized) - { - boost::mutex::scoped_lock guard(mutex); - - Mix_HookMusicFinished(nullptr); - current->stop(); - - current.reset(); - next.reset(); - } - - CAudioBase::release(); -} - -void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - if (current && current->isPlaying() && current->isTrack(musicURI)) - return; - - queueNext(this, "", musicURI, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) -{ - playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - auto selectedSet = musicsSet.find(whichSet); - if (selectedSet == musicsSet.end()) - { - logGlobal->error("Error: playing music from non-existing set: %s", whichSet); - return; - } - - if (current && current->isPlaying() && current->isSet(whichSet)) - return; - - // in this mode - play random track from set - queueNext(this, whichSet, AudioPath(), loop, fromStart); -} - -void CMusicHandler::queueNext(std::unique_ptr queued) -{ - if (!initialized) - return; - - next = std::move(queued); - - if (current.get() == nullptr || !current->stop(1000)) - { - current.reset(next.release()); - current->play(); - } -} - -void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) -{ - queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); -} - -void CMusicHandler::stopMusic(int fade_ms) -{ - if (!initialized) - return; - - boost::mutex::scoped_lock guard(mutex); - - if (current.get() != nullptr) - current->stop(fade_ms); - next.reset(); -} - -void CMusicHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); -} - -void CMusicHandler::musicFinishedCallback() -{ - // call music restart in separate thread to avoid deadlock in some cases - // It is possible for: - // 1) SDL thread to call this method on end of playback - // 2) VCMI code to call queueNext() method to queue new file - // this leads to: - // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) - // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) - - GH.dispatchMainThread([this]() - { - boost::unique_lock lockGuard(mutex); - if (current.get() != nullptr) - { - // if music is looped, play it again - if (current->play()) - return; - else - current.reset(); - } - - if (current.get() == nullptr && next.get() != nullptr) - { - current.reset(next.release()); - current->play(); - } - }); -} - -MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart): - owner(owner), - music(nullptr), - playing(false), - startTime(uint32_t(-1)), - startPosition(0), - loop(looped ? -1 : 1), - fromStart(fromStart), - setName(std::move(setName)) -{ - if (!musicURI.empty()) - load(std::move(musicURI)); -} -MusicEntry::~MusicEntry() -{ - if (playing && loop > 0) - { - assert(0); - logGlobal->error("Attempt to delete music while playing!"); - Mix_HaltMusic(); - } - - if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) - { - assert(0); - logGlobal->error("Attempt to delete music while fading out!"); - Mix_HaltMusic(); - } - - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - if (music) - Mix_FreeMusic(music); -} - -void MusicEntry::load(const AudioPath & musicURI) -{ - if (music) - { - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - Mix_FreeMusic(music); - music = nullptr; - } - - if (CResourceHandler::get()->existsResource(musicURI)) - currentName = musicURI; - else - currentName = musicURI.addPrefix("MUSIC/"); - - music = nullptr; - - logGlobal->trace("Loading music file %s", currentName.getOriginalName()); - - try - { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); - music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); - } - catch(std::exception &e) - { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); - logGlobal->error("Exception: %s", e.what()); - } - - if(!music) - { - logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); - return; - } -} - -bool MusicEntry::play() -{ - if (!(loop--) && music) //already played once - return - return false; - - if (!setName.empty()) - { - const auto & set = owner->musicsSet[setName]; - const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); - load(*iter); - } - - logGlobal->trace("Playing music file %s", currentName.getOriginalName()); - - if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) - { - float timeToStart = owner->trackPositions[currentName]; - startPosition = std::round(timeToStart * 1000); - - // erase stored position: - // if music track will be interrupted again - new position will be written in stop() method - // if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should - owner->trackPositions.erase(owner->trackPositions.find(currentName)); - - if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - else - { - startPosition = 0; - - if(Mix_PlayMusic(music, 1) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - - startTime = GH.input().getTicks(); - - playing = true; - return true; -} - -bool MusicEntry::stop(int fade_ms) -{ - if (Mix_PlayingMusic()) - { - playing = false; - loop = 0; - uint32_t endTime = GH.input().getTicks(); - assert(startTime != uint32_t(-1)); - float playDuration = (endTime - startTime + startPosition) / 1000.f; - owner->trackPositions[currentName] = playDuration; - logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); - - Mix_FadeOutMusic(fade_ms); - return true; - } - return false; -} - -bool MusicEntry::isPlaying() -{ - return playing; -} - -bool MusicEntry::isSet(std::string set) -{ - return !setName.empty() && set == setName; -} - -bool MusicEntry::isTrack(const AudioPath & track) -{ - return setName.empty() && track == currentName; -} diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h deleted file mode 100644 index 4a024c450..000000000 --- a/client/CMusicHandler.h +++ /dev/null @@ -1,170 +0,0 @@ -/* - * CMusicHandler.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "../lib/CConfigHandler.h" -#include "../lib/CSoundBase.h" - -struct _Mix_Music; -struct SDL_RWops; -using Mix_Music = struct _Mix_Music; -struct Mix_Chunk; - -class CAudioBase { -protected: - boost::mutex mutex; - bool initialized; - int volume; // from 0 (mute) to 100 - - CAudioBase(): initialized(false), volume(0) {}; - ~CAudioBase() = default; -public: - virtual void init() = 0; - virtual void release() = 0; - - virtual void setVolume(ui32 percent); - ui32 getVolume() const { return volume; }; -}; - -class CSoundHandler final : public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - using CachedChunk = std::pair>; - std::map soundChunks; - std::map, CachedChunk> soundChunksRaw; - - Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); - Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); - - /// have entry for every currently active channel - /// vector will be empty if callback was not set - std::map> > callbacks; - - /// Protects access to callbacks member to avoid data races: - /// SDL calls sound finished callbacks from audio thread - boost::mutex mutexCallbacks; - - int ambientDistToVolume(int distance) const; - void ambientStopSound(const AudioPath & soundId); - void updateChannelVolume(int channel); - - const JsonNode ambientConfig; - - std::map ambientChannels; - std::map channelVolumes; - - void initCallback(int channel, const std::function & function); - void initCallback(int channel); - -public: - CSoundHandler(); - - void init() override; - void release() override; - - void setVolume(ui32 percent) override; - void setChannelVolume(int channel, ui32 percent); - - // Sounds - uint32_t getSoundDurationMilliseconds(const AudioPath & sound); - int playSound(soundBase::soundID soundID, int repeats=0); - int playSound(const AudioPath & sound, int repeats=0, bool cache=false); - int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); - int playSoundFromSet(std::vector &sound_vec); - void stopSound(int handler); - - void setCallback(int channel, std::function function); - void resetCallback(int channel); - void soundFinishedCallback(int channel); - - int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); - void ambientStopAllChannels(); - - // Sets - std::vector battleIntroSounds; -}; - -class CMusicHandler; - -//Class for handling one music file -class MusicEntry -{ - CMusicHandler *owner; - Mix_Music *music; - - int loop; // -1 = indefinite - bool fromStart; - bool playing; - uint32_t startTime; - uint32_t startPosition; - //if not null - set from which music will be randomly selected - std::string setName; - AudioPath currentName; - - void load(const AudioPath & musicURI); - -public: - MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); - ~MusicEntry(); - - bool isSet(std::string setName); - bool isTrack(const AudioPath & trackName); - bool isPlaying(); - - bool play(); - bool stop(int fade_ms=0); -}; - -class CMusicHandler final: public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - std::unique_ptr current; - std::unique_ptr next; - - void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); - void queueNext(std::unique_ptr queued); - void musicFinishedCallback(); - - /// map -> - std::map> musicsSet; - /// stored position, in seconds at which music player should resume playing this track - std::map trackPositions; - -public: - CMusicHandler(); - - /// add entry with URI musicURI in set. Track will have ID musicID - void addEntryToSet(const std::string & set, const AudioPath & musicURI); - - void init() override; - void loadTerrainMusicThemes(); - void release() override; - void setVolume(ui32 percent) override; - - /// play track by URI, if loop = true music will be looped - void playMusic(const AudioPath & musicURI, bool loop, bool fromStart); - /// play random track from this set - void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart); - /// play random track from set (musicSet, entryID) - void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart); - /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any - void stopMusic(int fade_ms=1000); - - friend class MusicEntry; -}; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 497a1a24b..2be8587d9 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -14,7 +14,6 @@ #include "CGameInfo.h" #include "CMT.h" -#include "CMusicHandler.h" #include "CServerHandler.h" #include "HeroMovementController.h" #include "PlayerLocalState.h" @@ -41,6 +40,9 @@ #include "mapView/mapHandler.h" +#include "media/IMusicPlayer.h" +#include "media/ISoundPlayer.h" + #include "render/CAnimation.h" #include "render/IImage.h" #include "render/IRenderHandler.h" diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp deleted file mode 100644 index 4595e5977..000000000 --- a/client/CVideoHandler.cpp +++ /dev/null @@ -1,713 +0,0 @@ -/* - * CVideoHandler.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "CVideoHandler.h" - -#include "CMT.h" -#include "gui/CGuiHandler.h" -#include "eventsSDL/InputHandler.h" -#include "gui/FramerateManager.h" -#include "renderSDL/SDL_Extensions.h" -#include "CPlayerInterface.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CInputStream.h" - -#include - -#ifndef DISABLE_VIDEO - -extern "C" { -#include -#include -#include -#include -} - -#ifdef _MSC_VER -#pragma comment(lib, "avcodec.lib") -#pragma comment(lib, "avutil.lib") -#pragma comment(lib, "avformat.lib") -#pragma comment(lib, "swscale.lib") -#endif // _MSC_VER - -// Define a set of functions to read data -static int lodRead(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->data->read(buf, size)); - if(bytes == 0) - return AVERROR_EOF; - - return bytes; -} - -static si64 lodSeek(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->data->getSize(); - - return video->data->seek(pos); -} - -// Define a set of functions to read data -static int lodReadAudio(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->dataAudio->read(buf, size)); - if(bytes == 0) - return AVERROR_EOF; - - return bytes; -} - -static si64 lodSeekAudio(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->dataAudio->getSize(); - - return video->dataAudio->seek(pos); -} - -CVideoPlayer::CVideoPlayer() - : stream(-1) - , format (nullptr) - , codecContext(nullptr) - , codec(nullptr) - , frame(nullptr) - , sws(nullptr) - , context(nullptr) - , texture(nullptr) - , dest(nullptr) - , destRect(0,0,0,0) - , pos(0,0,0,0) - , frameTime(0) - , doLoop(false) -{} - -bool CVideoPlayer::open(const VideoPath & fname, bool scale) -{ - return open(fname, true, false); -} - -// loop = to loop through the video -// overlay = directly write to the screen. -bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale) -{ - close(); - - doLoop = loop; - frameTime = 0; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fname = videoToOpen; - else - fname = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fname)) - { - logGlobal->error("Error: video %s was not found", fname.getName()); - return false; - } - - data = CResourceHandler::get()->load(fname); - - static const int BUFFER_SIZE = 4096; - - unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek); - - format = avformat_alloc_context(); - format->pb = context; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return false; - } - // Retrieve stream information - if (avformat_find_stream_info(format, nullptr) < 0) - return false; - - // Find the first video stream - stream = -1; - for(ui32 i=0; inb_streams; i++) - { - if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) - { - stream = i; - break; - } - } - - if (stream < 0) - // No video stream in that file - return false; - - // Find the decoder for the video stream - codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); - - if (codec == nullptr) - { - // Unsupported codec - return false; - } - - codecContext = avcodec_alloc_context3(codec); - if(!codecContext) - return false; - // Get a pointer to the codec context for the video stream - int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContext); - return false; - } - - // Open codec - if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) - { - // Could not open codec - codec = nullptr; - return false; - } - // Allocate video frame - frame = av_frame_alloc(); - - //setup scaling - if(scale) - { - pos.w = screen->w; - pos.h = screen->h; - } - else - { - pos.w = codecContext->width; - pos.h = codecContext->height; - } - - // Allocate a place to put our YUV image on that screen - if (overlay) - { - texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); - } - else - { - dest = CSDL_Ext::newSurface(pos.w, pos.h); - destRect.x = destRect.y = 0; - destRect.w = pos.w; - destRect.h = pos.h; - } - - if (texture == nullptr && dest == nullptr) - return false; - - if (texture) - { // Convert the image into YUV format that SDL uses - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, - AV_PIX_FMT_YUV420P, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - else - { - AVPixelFormat screenFormat = AV_PIX_FMT_NONE; - if (screen->format->Bshift > screen->format->Rshift) - { - // this a BGR surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_BGR565; break; - case 3: screenFormat = AV_PIX_FMT_BGR24; break; - case 4: screenFormat = AV_PIX_FMT_BGR32; break; - default: return false; - } - } - else - { - // this a RGB surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_RGB565; break; - case 3: screenFormat = AV_PIX_FMT_RGB24; break; - case 4: screenFormat = AV_PIX_FMT_RGB32; break; - default: return false; - } - } - - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, screenFormat, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - - if (sws == nullptr) - return false; - - return true; -} - -// Read the next frame. Return false on error/end of file. -bool CVideoPlayer::nextFrame() -{ - AVPacket packet; - int frameFinished = 0; - bool gotError = false; - - if (sws == nullptr) - return false; - - while(!frameFinished) - { - int ret = av_read_frame(format, &packet); - if (ret < 0) - { - // Error. It's probably an end of file. - if (doLoop && !gotError) - { - // Rewind - frameTime = 0; - if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) - break; - gotError = true; - } - else - { - break; - } - } - else - { - // Is this a packet from the video stream? - if (packet.stream_index == stream) - { - // Decode video frame - int rc = avcodec_send_packet(codecContext, &packet); - if (rc >=0) - packet.size = 0; - rc = avcodec_receive_frame(codecContext, frame); - if (rc >= 0) - frameFinished = 1; - // Did we get a video frame? - if (frameFinished) - { - uint8_t *data[4]; - int linesize[4]; - - if (texture) { - av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1); - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - - SDL_UpdateYUVTexture(texture, nullptr, data[0], linesize[0], - data[1], linesize[1], - data[2], linesize[2]); - av_freep(&data[0]); - } - else - { - /* Avoid buffer overflow caused by sws_scale(): - * http://trac.ffmpeg.org/ticket/9254 - * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() - * has a few requirements for target data buffers on rescaling: - * 1. buffer has to be aligned to be usable for SIMD instructions - * 2. buffer has to be padded to allow small overflow by SIMD instructions - * Unfortunately SDL_Surface does not provide these guarantees. - * This means that atempt to rescale directly into SDL surface causes - * memory corruption. Usually it happens on campaign selection screen - * where short video moves start spinning on mouse hover. - * - * To fix [1.] we use av_malloc() for memory allocation. - * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. - * We have to use intermdiate buffer and then use memcpy() to land it - * to SDL_Surface. - */ - size_t pic_bytes = dest->pitch * dest->h; - size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ - void * for_sws = av_malloc (pic_bytes + ffmped_pad); - data[0] = (ui8 *)for_sws; - linesize[0] = dest->pitch; - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - memcpy(dest->pixels, for_sws, pic_bytes); - av_free(for_sws); - } - } - } - - av_packet_unref(&packet); - } - } - - return frameFinished != 0; -} - -void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) -{ - if (sws == nullptr) - return; - - pos.x = x; - pos.y = y; - CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft()); - - if (update) - CSDL_Ext::updateRect(dst, pos); -} - -void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) -{ - show(x, y, dst, update); -} - -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) -{ - if (sws == nullptr) - return; - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base); - frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0; - - if (frameTime >= frameEndTime ) - { - if (nextFrame()) - show(x,y,dst,update); - else - { - if(onVideoRestart) - onVideoRestart(); - VideoPath filenameToReopen = fname; // create copy to backup this->fname - open(filenameToReopen); - nextFrame(); - - // The y position is wrong at the first frame. - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - show(x, y--, dst, update); - } - } - else - { - redraw(x, y, dst, update); - } -} - -void CVideoPlayer::close() -{ - fname = VideoPath(); - - if (sws) - { - sws_freeContext(sws); - sws = nullptr; - } - - if (texture) - { - SDL_DestroyTexture(texture); - texture = nullptr; - } - - if (dest) - { - SDL_FreeSurface(dest); - dest = nullptr; - } - - if (frame) - { - av_frame_free(&frame);//will be set to null - } - - if (codec) - { - avcodec_close(codecContext); - codec = nullptr; - } - if (codecContext) - { - avcodec_free_context(&codecContext); - } - - if (format) - { - avformat_close_input(&format); - } - - if (context) - { - av_free(context); - context = nullptr; - } -} - -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) -{ - std::pair, si64> dat(std::make_pair(nullptr, 0)); - - VideoPath fnameAudio; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fnameAudio = videoToOpen; - else - fnameAudio = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fnameAudio)) - { - logGlobal->error("Error: video %s was not found", fnameAudio.getName()); - return dat; - } - - dataAudio = CResourceHandler::get()->load(fnameAudio); - - static const int BUFFER_SIZE = 4096; - - unsigned char * bufferAudio = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio); - - AVFormatContext * formatAudio = avformat_alloc_context(); - formatAudio->pb = contextAudio; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return dat; - } - // Retrieve stream information - if (avformat_find_stream_info(formatAudio, nullptr) < 0) - return dat; - - // Find the first audio stream - int streamAudio = -1; - for(ui32 i = 0; i < formatAudio->nb_streams; i++) - { - if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - streamAudio = i; - break; - } - } - - if(streamAudio < 0) - return dat; - - const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id); - - AVCodecContext *codecContextAudio; - if (codecAudio != nullptr) - codecContextAudio = avcodec_alloc_context3(codecAudio); - - // Get a pointer to the codec context for the audio stream - if (streamAudio > -1) - { - int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContextAudio); - } - } - - // Open codec - AVFrame *frameAudio; - if (codecAudio != nullptr) - { - if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) - { - // Could not open codec - codecAudio = nullptr; - } - // Allocate audio frame - frameAudio = av_frame_alloc(); - } - - AVPacket packet; - - std::vector samples; - - while (av_read_frame(formatAudio, &packet) >= 0) - { - if(packet.stream_index == streamAudio) - { - int rc = avcodec_send_packet(codecContextAudio, &packet); - if (rc >= 0) - packet.size = 0; - rc = avcodec_receive_frame(codecContextAudio, frameAudio); - int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); - if (rc >= 0) - for (int s = 0; s < bytesToRead; s += sizeof(ui8)) - { - ui8 value; - memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); - samples.push_back(value); - } - } - - av_packet_unref(&packet); - } - - typedef struct WAV_HEADER { - ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; - ui32 ChunkSize; - ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; - ui8 fmt[4] = {'f', 'm', 't', ' '}; - ui32 Subchunk1Size = 16; - ui16 AudioFormat = 1; - ui16 NumOfChan = 2; - ui32 SamplesPerSec = 22050; - ui32 bytesPerSec = 22050 * 2; - ui16 blockAlign = 2; - ui16 bitsPerSample = 16; - ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; - ui32 Subchunk2Size; - } wav_hdr; - - wav_hdr wav; - wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; - wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; - wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate; - wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; - auto wavPtr = reinterpret_cast(&wav); - - dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); - std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); - std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); - - if (frameAudio) - av_frame_free(&frameAudio); - - if (codecAudio) - { - avcodec_close(codecContextAudio); - codecAudio = nullptr; - } - if (codecContextAudio) - avcodec_free_context(&codecContextAudio); - - if (formatAudio) - avformat_close_input(&formatAudio); - - if (contextAudio) - { - av_free(contextAudio); - contextAudio = nullptr; - } - - return dat; -} - -Point CVideoPlayer::size() -{ - if(frame) - return Point(frame->width, frame->height); - else - return Point(0, 0); -} - -// Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) -{ - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - y--; - - pos.x = x; - pos.y = y; - frameTime = 0.0; - - auto lastTimePoint = boost::chrono::steady_clock::now(); - - while(nextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - return false; - } - - SDL_Rect rect = CSDL_Ext::toSDL(pos); - - if(overlay) - { - SDL_RenderFillRect(mainRenderer, &rect); - } - else - { - SDL_RenderClear(mainRenderer); - } - SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); - SDL_RenderPresent(mainRenderer); - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - // Framerate delay - double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base); - auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); - - auto timePointAfterPresent = boost::chrono::steady_clock::now(); - auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); - - if (targetFrameTime > timeSpentBusy) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); - - lastTimePoint = boost::chrono::steady_clock::now(); - } - - return true; -} - -bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) -{ - bool scale; - bool stopOnKey; - bool overlay; - - switch(videoType) - { - case EVideoType::INTRO: - stopOnKey = true; - scale = true; - overlay = false; - break; - case EVideoType::SPELLBOOK: - default: - stopOnKey = false; - scale = false; - overlay = true; - } - open(name, false, true, scale); - bool ret = playVideo(x, y, stopOnKey, overlay); - close(); - return ret; -} - -CVideoPlayer::~CVideoPlayer() -{ - close(); -} - -#endif - diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h deleted file mode 100644 index 89f8016a2..000000000 --- a/client/CVideoHandler.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - * CVideoHandler.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "../lib/Rect.h" -#include "../lib/filesystem/ResourcePath.h" - -struct SDL_Surface; -struct SDL_Texture; - -enum class EVideoType : ui8 -{ - INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false - SPELLBOOK // overlay video: stopOnKey = false, scale = false, overlay = true -}; - -class IVideoPlayer : boost::noncopyable -{ -public: - virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes - virtual void close()=0; - virtual bool nextFrame()=0; - virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; - virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer - virtual bool wait()=0; - virtual int curFrame() const =0; - virtual int frameCount() const =0; -}; - -class IMainVideoPlayer : public IVideoPlayer -{ -public: - virtual ~IMainVideoPlayer() = default; - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr){} - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) - { - return false; - } - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; - virtual Point size() { return Point(0, 0); }; -}; - -class CEmptyVideoPlayer final : public IMainVideoPlayer -{ -public: - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - bool nextFrame() override {return false;}; - void close() override {}; - bool wait() override {return false;}; - bool open(const VideoPath & name, bool scale = false) override {return false;}; -}; - -#ifndef DISABLE_VIDEO - -struct AVFormatContext; -struct AVCodecContext; -struct AVCodec; -struct AVFrame; -struct AVIOContext; - -VCMI_LIB_NAMESPACE_BEGIN -class CInputStream; -VCMI_LIB_NAMESPACE_END - -class CVideoPlayer final : public IMainVideoPlayer -{ - int stream; // stream index in video - AVFormatContext *format; - AVCodecContext *codecContext; // codec context for stream - const AVCodec *codec; - AVFrame *frame; - struct SwsContext *sws; - - AVIOContext * context; - - VideoPath fname; //name of current video file (empty if idle) - - // Destination. Either overlay or dest. - - SDL_Texture *texture; - SDL_Surface *dest; - Rect destRect; // valid when dest is used - Rect pos; // destination on screen - - /// video playback currnet progress, in seconds - double frameTime; - bool doLoop; // loop through video - - bool playVideo(int x, int y, bool stopOnKey, bool overlay); - bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); -public: - CVideoPlayer(); - ~CVideoPlayer(); - - bool init(); - bool open(const VideoPath & fname, bool scale = false) override; - void close() override; - bool nextFrame() override; // display next frame - - void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame - void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true - - // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) - bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override; - - std::pair, si64> getAudio(const VideoPath & videoToOpen) override; - - Point size() override; - - //TODO: - bool wait() override {return false;}; - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - - // public to allow access from ffmpeg IO functions - std::unique_ptr data; - std::unique_ptr dataAudio; -}; - -#endif diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index d69075661..4c415704a 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -11,7 +11,6 @@ #include "HeroMovementController.h" #include "CGameInfo.h" -#include "CMusicHandler.h" #include "CPlayerInterface.h" #include "PlayerLocalState.h" #include "adventureMap/AdventureMapInterface.h" @@ -19,10 +18,12 @@ #include "gui/CGuiHandler.h" #include "gui/CursorHandler.h" #include "mapView/mapHandler.h" +#include "media/ISoundPlayer.h" #include "../CCallback.h" #include "../lib/CondSh.h" +#include "../lib/CConfigHandler.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/networkPacks/PacksForClient.h" diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 5a8a05cc0..54b07b7e9 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -12,7 +12,6 @@ #include "CInGameConsole.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../GameChatHandler.h" @@ -21,6 +20,7 @@ #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" #include "../gui/TextAlignment.h" +#include "../media/ISoundPlayer.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IScreenHandler.h" diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 68809752a..70b6484bb 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -20,11 +20,11 @@ #include "../widgets/MiscWidgets.h" #include "../windows/InfoWindows.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/IScreenHandler.h" #include "../../CCallback.h" diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 81a7cf002..bca944448 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -12,9 +12,10 @@ #include "../CCallback.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../mapView/mapHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjects/CArmedInstance.h" diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index c97d67f94..ac7cb7602 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -11,11 +11,11 @@ #include "TurnTimerWidget.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" #include "../battle/BattleStacksController.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Graphics.h" #include "../widgets/Images.h" #include "../widgets/GraphicalPrimitiveCanvas.h" diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 2bff6e8ce..78bfcadfa 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -20,10 +20,10 @@ #include "CreatureAnimation.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/IRenderHandler.h" #include "../../CCallback.h" diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index f1db77c16..7a2d936b7 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -18,9 +18,9 @@ #include "BattleStacksController.h" #include "BattleRenderer.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../media/ISoundPlayer.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" #include "../render/Graphics.h" diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index dd02ca497..e7248738d 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -24,11 +24,12 @@ #include "BattleRenderer.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../windows/CTutorialWindow.h" #include "../render/Canvas.h" #include "../adventureMap/AdventureMapInterface.h" @@ -113,7 +114,14 @@ void BattleInterface::playIntroSoundAndUnlockInterface() onIntroSoundPlayed(); }; - int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); + std::vector battleIntroSounds = + { + soundBase::battle00, soundBase::battle01, + soundBase::battle02, soundBase::battle03, soundBase::battle04, + soundBase::battle05, soundBase::battle06, soundBase::battle07 + }; + + int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(battleIntroSounds); if (battleIntroSoundChannel != -1) { CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 1891f3f08..b5416e7bf 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -19,14 +19,13 @@ #include "BattleWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CVideoHandler.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IFont.h" @@ -35,6 +34,7 @@ #include "../widgets/Images.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/CMessage.h" #include "../windows/CCreatureWindow.h" @@ -603,7 +603,7 @@ HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) } BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) - : owner(_owner), currentVideo(BattleResultVideo::NONE) + : owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -705,68 +705,98 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface } } } + + auto resources = getResources(br); + + description = std::make_shared(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + videoPlayer = std::make_shared(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false); + + CCS->musich->playMusic(resources.musicName, false, true); +} + +BattleResultResources BattleResultWindow::getResources(const BattleResult & br) +{ //printing result description bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); - if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won + bool weAreDefender = !weAreAttacker; + bool weWon = (br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker); + bool isSiege = owner.cb->getBattle(br.battleID)->battleGetDefendedTown() != nullptr; + + BattleResultResources resources; + + if(weWon) { - int text = 304; - currentVideo = BattleResultVideo::WIN; + if(isSiege && weAreDefender) + { + resources.musicName = AudioPath::builtin("Music/Defend Castle"); + resources.prologueVideo = VideoPath::builtin("DEFENDALL.BIK"); + resources.loopedVideo = VideoPath::builtin("defendloop.bik"); + } + else + { + resources.musicName = AudioPath::builtin("Music/Win Battle"); + resources.prologueVideo = VideoPath(); + resources.loopedVideo = VideoPath::builtin("WIN3.BIK"); + } + switch(br.result) { case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::WIN_SIEGE; + resources.resultText.appendTextID("core.genrltxt.304"); break; case EBattleResult::ESCAPE: - text = 303; + resources.resultText.appendTextID("core.genrltxt.303"); break; case EBattleResult::SURRENDER: - text = 302; + resources.resultText.appendTextID("core.genrltxt.302"); break; default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; + throw std::runtime_error("Invalid battle result!"); } - playVideo(); - - std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); if (ourHero) { - str += CGI->generaltexth->allTexts[305]; - boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated()); - boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1])); + resources.resultText.appendTextID("core.genrltxt.305"); + resources.resultText.replaceTextID(ourHero->getNameTranslated()); + resources.resultText.replaceNumber(br.exp[weAreAttacker ? 0 : 1]); } - - description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } else // we lose { - int text = 311; - currentVideo = BattleResultVideo::DEFEAT; switch(br.result) { case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::DEFEAT_SIEGE; + resources.resultText.appendTextID("core.genrltxt.311"); + resources.musicName = AudioPath::builtin("Music/LoseCombat"); + resources.prologueVideo = VideoPath::builtin("LBSTART.BIK"); + resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK"); break; case EBattleResult::ESCAPE: - currentVideo = BattleResultVideo::RETREAT; - text = 310; + resources.resultText.appendTextID("core.genrltxt.310"); + resources.musicName = AudioPath::builtin("Music/Retreat Battle"); + resources.prologueVideo = VideoPath::builtin("RTSTART.BIK"); + resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK"); break; case EBattleResult::SURRENDER: - currentVideo = BattleResultVideo::SURRENDER; - text = 309; + resources.resultText.appendTextID("core.genrltxt.309"); + resources.musicName = AudioPath::builtin("Music/Surrender Battle"); + resources.prologueVideo = VideoPath(); + resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK"); break; default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; + throw std::runtime_error("Invalid battle result!"); } - playVideo(); - labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); + if(isSiege && weAreDefender) + { + resources.musicName = AudioPath::builtin("Music/LoseCastle"); + resources.prologueVideo = VideoPath::builtin("LOSECSTL.BIK"); + resources.loopedVideo = VideoPath::builtin("LOSECSLP.BIK"); + } } + + return resources; } void BattleResultWindow::activate() @@ -775,81 +805,6 @@ void BattleResultWindow::activate() CIntObject::activate(); } -void BattleResultWindow::show(Canvas & to) -{ - CIntObject::show(to); - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, - [&]() - { - playVideo(true); - }); -} - -void BattleResultWindow::playVideo(bool startLoop) -{ - AudioPath musicName = AudioPath(); - VideoPath videoName = VideoPath(); - - if(!startLoop) - { - switch(currentVideo) - { - case BattleResultVideo::WIN: - musicName = AudioPath::builtin("Music/Win Battle"); - videoName = VideoPath::builtin("WIN3.BIK"); - break; - case BattleResultVideo::SURRENDER: - musicName = AudioPath::builtin("Music/Surrender Battle"); - videoName = VideoPath::builtin("SURRENDER.BIK"); - break; - case BattleResultVideo::RETREAT: - musicName = AudioPath::builtin("Music/Retreat Battle"); - videoName = VideoPath::builtin("RTSTART.BIK"); - break; - case BattleResultVideo::DEFEAT: - musicName = AudioPath::builtin("Music/LoseCombat"); - videoName = VideoPath::builtin("LBSTART.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - musicName = AudioPath::builtin("Music/LoseCastle"); - videoName = VideoPath::builtin("LOSECSTL.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - musicName = AudioPath::builtin("Music/Defend Castle"); - videoName = VideoPath::builtin("DEFENDALL.BIK"); - break; - } - } - else - { - switch(currentVideo) - { - case BattleResultVideo::RETREAT: - currentVideo = BattleResultVideo::RETREAT_LOOP; - videoName = VideoPath::builtin("RTLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT: - currentVideo = BattleResultVideo::DEFEAT_LOOP; - videoName = VideoPath::builtin("LBLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP; - videoName = VideoPath::builtin("LOSECSLP.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - currentVideo = BattleResultVideo::WIN_SIEGE_LOOP; - videoName = VideoPath::builtin("DEFENDLOOP.BIK"); - break; - } - } - - if(musicName != AudioPath()) - CCS->musich->playMusic(musicName, false, true); - - if(videoName != VideoPath()) - CCS->videoh->open(videoName); -} - void BattleResultWindow::buttonPressed(int button) { if (resultCallback) @@ -865,7 +820,6 @@ void BattleResultWindow::buttonPressed(int button) //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, //so we can be sure that there is no dialogs left on GUI stack. intTmp.showingDialog->setn(false); - CCS->videoh->close(); } void BattleResultWindow::bExitf() diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index d50a9e41e..6b9762d62 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -14,6 +14,7 @@ #include "../../lib/FunctionList.h" #include "../../lib/battle/BattleHex.h" #include "../windows/CWindowObject.h" +#include "../../lib/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,6 +43,7 @@ class CAnimImage; class TransparentFilledRectangle; class CPlayerInterface; class BattleRenderer; +class VideoWidget; /// Class which shows the console at the bottom of the battle screen and manages the text of the console class BattleConsole : public CIntObject, public IStatusBar @@ -185,6 +187,14 @@ public: HeroInfoWindow(const InfoAboutHero & hero, Point * position); }; +struct BattleResultResources +{ + VideoPath prologueVideo; + VideoPath loopedVideo; + AudioPath musicName; + MetaString resultText; +}; + /// Class which is responsible for showing the battle result window class BattleResultWindow : public WindowBase { @@ -195,25 +205,10 @@ private: std::shared_ptr repeat; std::vector> icons; std::shared_ptr description; + std::shared_ptr videoPlayer; CPlayerInterface & owner; - enum BattleResultVideo - { - NONE, - WIN, - SURRENDER, - RETREAT, - RETREAT_LOOP, - DEFEAT, - DEFEAT_LOOP, - DEFEAT_SIEGE, - DEFEAT_SIEGE_LOOP, - WIN_SIEGE, - WIN_SIEGE_LOOP, - }; - BattleResultVideo currentVideo; - - void playVideo(bool startLoop = false); + BattleResultResources getResources(const BattleResult & br); void buttonPressed(int button); //internal function for button callbacks public: @@ -224,7 +219,6 @@ public: std::function resultCallback; //callback receiving which button was pressed void activate() override; - void show(Canvas & to) override; }; /// Shows the stack queue diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 0c6e7e809..2d7fbdbb7 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -17,10 +17,10 @@ #include "BattleRenderer.h" #include "CreatureAnimation.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 6d27a5945..6e3acb82b 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -17,10 +17,10 @@ #include "BattleFieldController.h" #include "BattleRenderer.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 5528b0bb4..bbc14298d 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -23,10 +23,10 @@ #include "CreatureAnimation.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index c750b620d..648e3c4ff 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -18,7 +18,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index bbf3dcd4f..105af193c 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -22,10 +22,11 @@ #include "../gui/CursorHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/MouseButton.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../CMT.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../../lib/CConfigHandler.h" diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp index 6b7be1513..9b58d782f 100644 --- a/client/globalLobby/GlobalLobbyClient.cpp +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -17,11 +17,11 @@ #include "GlobalLobbyWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../mainmenu/CMainMenu.h" +#include "../media/ISoundPlayer.h" #include "../windows/InfoWindows.h" #include "../../lib/CConfigHandler.h" diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index 95803ecf0..b3ee03288 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -16,10 +16,10 @@ #include "GlobalLobbyRoomWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Colors.h" #include "../widgets/Buttons.h" #include "../widgets/CTextInput.h" diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index f105b6efb..3406f30a5 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -18,12 +18,11 @@ #include "ExtraOptionsTab.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../mainmenu/CMainMenu.h" #include "../mainmenu/CPrologEpilogVideo.h" +#include "../media/IMusicPlayer.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -41,9 +40,9 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/CGeneralTextHandler.h" - +#include "../../lib/CConfigHandler.h" #include "../../lib/CBuildingHandler.h" - +#include "../../lib/CConfigHandler.h" #include "../../lib/CSkillHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/CHeroHandler.h" diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 2f5ba1ce6..9030da027 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -20,14 +20,13 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../globalLobby/GlobalLobbyClient.h" #include "../mainmenu/CMainMenu.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/CComponent.h" #include "../widgets/CTextInput.h" diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 3f87eb5e1..83060f09a 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -14,12 +14,12 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" -#include "../CMusicHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../media/ISoundPlayer.h" #include "../widgets/CComponent.h" #include "../widgets/ComboBox.h" #include "../widgets/CTextInput.h" @@ -38,6 +38,7 @@ #include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapping/CMapInfo.h" diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 9db6cf79c..16ae03a8a 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -14,18 +14,18 @@ #include "CMainMenu.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../media/IMusicPlayer.h" #include "../render/Canvas.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../windows/CWindowObject.h" @@ -36,7 +36,7 @@ #include "../../lib/CArtHandler.h" #include "../../lib/CBuildingHandler.h" #include "../../lib/spells/CSpellHandler.h" - +#include "../../lib/CConfigHandler.h" #include "../../lib/CSkillHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/CHeroHandler.h" @@ -100,7 +100,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const pos.h = 116; campFile = config["file"].String(); - video = VideoPath::fromJson(config["video"]); + videoPath = VideoPath::fromJson(config["video"]); status = CCampaignScreen::ENABLED; @@ -127,7 +127,6 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const { addUsedEvents(LCLICK | HOVER); graphicsImage = std::make_shared(ImagePath::fromJson(config["image"])); - hoverLabel = std::make_shared(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, ""); parent->addChild(hoverLabel.get()); } @@ -136,30 +135,19 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const graphicsCompleted = std::make_shared(ImagePath::builtin("CAMPCHK")); } -void CCampaignScreen::CCampaignButton::show(Canvas & to) -{ - if(status == CCampaignScreen::DISABLED) - return; - - CIntObject::show(to); - - // Play the campaign button video when the mouse cursor is placed over the button - if(isHovered()) - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); // plays sequentially frame by frame, starts at the beginning when the video is over -} - void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) { - CCS->videoh->close(); CMainMenu::openCampaignLobby(campFile, campaignSet); } void CCampaignScreen::CCampaignButton::hover(bool on) { - if (on) - CCS->videoh->open(video); + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if (on && !videoPath.empty()) + videoPlayer = std::make_shared(Point(), videoPath, false); else - CCS->videoh->close(); + videoPlayer.reset(); if(hoverLabel) { diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index e19395974..87b69bc5e 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_END class CLabel; class CPicture; class CButton; +class VideoWidget; class CCampaignScreen : public CWindowObject { @@ -34,10 +35,11 @@ private: std::shared_ptr hoverLabel; std::shared_ptr graphicsImage; std::shared_ptr graphicsCompleted; + std::shared_ptr videoPlayer; CampaignStatus status; + VideoPath videoPath; std::string campFile; // the filename/resourcename of the campaign - VideoPath video; // the resource name of the video std::string hoverText; std::string campaignSet; @@ -47,7 +49,6 @@ private: public: CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet); - void show(Canvas & to) override; }; std::string campaignSet; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index c099b5335..25c846f5d 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -14,17 +14,18 @@ #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/CTextInput.h" #include "../widgets/Images.h" #include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../widgets/TextControls.h" #include "../render/Canvas.h" #include "../CGameInfo.h" -#include "../CVideoHandler.h" -#include "../CMusicHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" @@ -216,7 +217,7 @@ void CHighScoreScreen::buttonExitClick() } CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) - : CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1) + : CWindowObject(BORDERED), won(won), calc(calc) { addUsedEvents(LCLICK | KEYBOARD); @@ -228,6 +229,9 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc if(won) { + + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK"), true); + int border = 100; int textareaW = ((pos.w - 2 * border) / 4); std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank @@ -242,9 +246,10 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); } else + { + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();}); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); - - video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; + } } int CHighScoreInputScreen::addEntry(std::string text) { @@ -289,43 +294,7 @@ int CHighScoreInputScreen::addEntry(std::string text) { void CHighScoreInputScreen::show(Canvas & to) { - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, - [&]() - { - if(won) - { - CCS->videoh->close(); - video = "HSLOOP.SMK"; - auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - videoSoundHandle = CCS->soundh->playSound(audioData); - CCS->videoh->open(VideoPath::builtin(video)); - } - else - close(); - }); - redraw(); - - CIntObject::show(to); -} - -void CHighScoreInputScreen::activate() -{ - auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - videoSoundHandle = CCS->soundh->playSound(audioData); - if(!CCS->videoh->open(VideoPath::builtin(video))) - { - if(!won) - close(); - } - else - background = nullptr; - CIntObject::activate(); -} - -void CHighScoreInputScreen::deactivate() -{ - CCS->videoh->close(); - CCS->soundh->stopSound(videoSoundHandle); + CWindowObject::showAll(to); } void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 2261dbe60..852ddc26f 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -15,6 +15,7 @@ class CLabel; class CMultiLineLabel; class CAnimImage; class CTextInput; +class VideoWidgetBase; class TransparentFilledRectangle; @@ -93,9 +94,8 @@ class CHighScoreInputScreen : public CWindowObject std::vector> texts; std::shared_ptr input; std::shared_ptr background; + std::shared_ptr videoPlayer; - std::string video; - int videoSoundHandle; bool won; HighScoreCalculation calc; public: @@ -103,9 +103,7 @@ public: int addEntry(std::string text); - void show(Canvas & to) override; - void activate() override; - void deactivate() override; void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; -}; \ No newline at end of file + void show(Canvas & to) override; +}; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 12b44298f..5a0314aa1 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -17,6 +17,7 @@ #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" #include "../lobby/CLobbyScreen.h" +#include "../media/IMusicPlayer.h" #include "../gui/CursorHandler.h" #include "../windows/GUIClasses.h" #include "../gui/CGuiHandler.h" @@ -33,12 +34,11 @@ #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../CServerHandler.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../Client.h" #include "../CMT.h" @@ -92,8 +92,14 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) menuNameToEntry.push_back("credits"); tabs = std::make_shared(std::bind(&CMenuScreen::createTab, this, _1)); - if(config["video"].isNull()) + if(!config["video"].isNull()) + { + Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer()); + videoPlayer = std::make_shared(videoPosition, VideoPath::fromJson(config["video"]["name"]), false); + } + else tabs->setRedrawParent(true); + } std::shared_ptr CMenuScreen::createTab(size_t index) @@ -106,32 +112,16 @@ std::shared_ptr CMenuScreen::createTab(size_t index) void CMenuScreen::show(Canvas & to) { - if(!config["video"].isNull()) - { - // redraw order: background -> video -> buttons and pictures - background->redraw(); - CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false); - tabs->redraw(); - } - CIntObject::show(to); + // TODO: avoid excessive redraws + CIntObject::showAll(to); } void CMenuScreen::activate() { CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); - if(!config["video"].isNull()) - CCS->videoh->open(VideoPath::fromJson(config["video"]["name"])); CIntObject::activate(); } -void CMenuScreen::deactivate() -{ - if(!config["video"].isNull()) - CCS->videoh->close(); - - CIntObject::deactivate(); -} - void CMenuScreen::switchToTab(size_t index) { tabs->setActive(index); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index e4d62aef1..442f92afb 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -28,7 +28,7 @@ class CAnimation; class CButton; class CFilledTexture; class CLabel; - +class VideoWidget; // TODO: Find new location for these enums enum class ESelectionScreen : ui8 { @@ -48,6 +48,7 @@ class CMenuScreen : public CWindowObject std::shared_ptr tabs; std::shared_ptr background; + std::shared_ptr videoPlayer; std::vector> images; std::shared_ptr createTab(size_t index); @@ -57,9 +58,8 @@ public: CMenuScreen(const JsonNode & configNode); - void show(Canvas & to) override; void activate() override; - void deactivate() override; + void show(Canvas & to) override; void switchToTab(size_t index); void switchToTab(std::string name); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index af1d77a72..8d4a6d93e 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -12,15 +12,15 @@ #include "CPrologEpilogVideo.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" -#include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" +//#include "../gui/WindowHandler.h" #include "../gui/CGuiHandler.h" -#include "../gui/FramerateManager.h" +//#include "../gui/FramerateManager.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../render/Canvas.h" - CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0) { @@ -29,9 +29,23 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f pos = center(Rect(0, 0, 800, 600)); updateShadow(); - auto audioData = CCS->videoh->getAudio(spe.prologVideo); - videoSoundHandle = CCS->soundh->playSound(audioData, -1); - CCS->videoh->open(spe.prologVideo); + //TODO: remove hardcoded paths. Some of campaigns video actually consist from 2 parts + // however, currently our campaigns format expects only a single video file + static const std::map pairedVideoFiles = { + { VideoPath::builtin("EVIL2AP1"), VideoPath::builtin("EVIL2AP2") }, + { VideoPath::builtin("H3ABdb4"), VideoPath::builtin("H3ABdb4b") }, + { VideoPath::builtin("H3x2_RNe1"), VideoPath::builtin("H3x2_RNe2") }, + }; + + if (pairedVideoFiles.count(spe.prologVideo)) + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, pairedVideoFiles.at(spe.prologVideo), true); + else + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, true); + + //some videos are 800x600 in size while some are 800x400 + if (videoPlayer->pos.h == 400) + videoPlayer->moveBy(Point(0, 100)); + CCS->musich->playMusic(spe.prologMusic, true, true); voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); @@ -65,9 +79,8 @@ void CPrologEpilogVideo::tick(uint32_t msPassed) void CPrologEpilogVideo::show(Canvas & to) { to.drawColor(pos, Colors::BLACK); - //some videos are 800x600 in size while some are 800x400 - CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false); + videoPlayer->show(to); text->showAll(to); // blit text over video, if needed } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 5923791d5..b2c1fd9e3 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -13,6 +13,7 @@ #include "../../lib/campaign/CampaignScenarioPrologEpilog.h" class CMultiLineLabel; +class VideoWidget; class CPrologEpilogVideo : public CWindowObject { @@ -25,6 +26,7 @@ class CPrologEpilogVideo : public CWindowObject std::function exitCb; std::shared_ptr text; + std::shared_ptr videoPlayer; bool voiceStopped = false; diff --git a/client/media/CAudioBase.cpp b/client/media/CAudioBase.cpp new file mode 100644 index 000000000..24bbc1eff --- /dev/null +++ b/client/media/CAudioBase.cpp @@ -0,0 +1,43 @@ +/* + * CAudioBase.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CAudioBase.h" + +#include + +int CAudioBase::initializationCounter = 0; +bool CAudioBase::initializeSuccess = false; + +CAudioBase::CAudioBase() +{ + if(initializationCounter == 0) + { + if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1) + logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); + else + initializeSuccess = true; + } + ++initializationCounter; +} + +bool CAudioBase::isInitialized() const +{ + return initializeSuccess; +} + +CAudioBase::~CAudioBase() +{ + --initializationCounter; + + if(initializationCounter == 0 && initializeSuccess) + Mix_CloseAudio(); + + initializeSuccess = false; +} diff --git a/client/media/CAudioBase.h b/client/media/CAudioBase.h new file mode 100644 index 000000000..94437db51 --- /dev/null +++ b/client/media/CAudioBase.h @@ -0,0 +1,22 @@ +/* + * CAudioBase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CAudioBase : boost::noncopyable +{ + static int initializationCounter; + static bool initializeSuccess; + +protected: + bool isInitialized() const; + + CAudioBase(); + ~CAudioBase(); +}; diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h new file mode 100644 index 000000000..6dc91abaf --- /dev/null +++ b/client/media/CEmptyVideoPlayer.h @@ -0,0 +1,38 @@ +/* + * CEmptyVideoPlayer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "IVideoPlayer.h" + +class CEmptyVideoPlayer final : public IVideoPlayer +{ +public: + /// Plays video on top of the screen, returns only after playback is over + bool playIntroVideo(const VideoPath & name) override + { + return false; + }; + + void playSpellbookAnimation(const VideoPath & name, const Point & position) override + { + } + + /// Load video from specified path + std::unique_ptr open(const VideoPath & name, bool scaleToScreen) override + { + return nullptr; + }; + + /// Extracts audio data from provided video in wav format + std::pair, si64> getAudio(const VideoPath & videoToOpen) override + { + return {nullptr, 0}; + }; +}; diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp new file mode 100644 index 000000000..b608bbab9 --- /dev/null +++ b/client/media/CMusicHandler.cpp @@ -0,0 +1,349 @@ +/* + * CMusicHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CMusicHandler.h" + +#include "../CGameInfo.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" +#include "../renderSDL/SDLRWwrapper.h" + +#include "../../lib/CRandomGenerator.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/filesystem/Filesystem.h" + +#include + +void CMusicHandler::onVolumeChange(const JsonNode & volumeNode) +{ + setVolume(volumeNode.Integer()); +} + +CMusicHandler::CMusicHandler(): + listener(settings.listen["general"]["music"]) +{ + listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); + + auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool + { + if(id.getType() != EResType::SOUND) + return false; + + if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) + return false; + + logGlobal->trace("Found music file %s", id.getName()); + return true; + }); + + for(const ResourcePath & file : mp3files) + { + if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) + addEntryToSet("battle", AudioPath::fromResource(file)); + else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) + addEntryToSet("enemy-turn", AudioPath::fromResource(file)); + } + + if (isInitialized()) + { + Mix_HookMusicFinished([]() + { + CCS->musich->musicFinishedCallback(); + }); + } +} + +void CMusicHandler::loadTerrainMusicThemes() +{ + for(const auto & terrain : CGI->terrainTypeHandler->objects) + { + addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); + } +} + +void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) +{ + musicsSet[set].push_back(musicURI); +} + +CMusicHandler::~CMusicHandler() +{ + if(isInitialized()) + { + boost::mutex::scoped_lock guard(mutex); + + Mix_HookMusicFinished(nullptr); + current->stop(); + + current.reset(); + next.reset(); + } +} + +void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + if(current && current->isPlaying() && current->isTrack(musicURI)) + return; + + queueNext(this, "", musicURI, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) +{ + playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + auto selectedSet = musicsSet.find(whichSet); + if(selectedSet == musicsSet.end()) + { + logGlobal->error("Error: playing music from non-existing set: %s", whichSet); + return; + } + + if(current && current->isPlaying() && current->isSet(whichSet)) + return; + + // in this mode - play random track from set + queueNext(this, whichSet, AudioPath(), loop, fromStart); +} + +void CMusicHandler::queueNext(std::unique_ptr queued) +{ + if(!isInitialized()) + return; + + next = std::move(queued); + + if(current == nullptr || !current->stop(1000)) + { + current.reset(next.release()); + current->play(); + } +} + +void CMusicHandler::queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) +{ + queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); +} + +void CMusicHandler::stopMusic(int fade_ms) +{ + if(!isInitialized()) + return; + + boost::mutex::scoped_lock guard(mutex); + + if(current != nullptr) + current->stop(fade_ms); + next.reset(); +} + +ui32 CMusicHandler::getVolume() const +{ + return volume; +} + +void CMusicHandler::setVolume(ui32 percent) +{ + volume = std::min(100u, percent); + + if(isInitialized()) + Mix_VolumeMusic((MIX_MAX_VOLUME * volume) / 100); +} + +void CMusicHandler::musicFinishedCallback() +{ + // call music restart in separate thread to avoid deadlock in some cases + // It is possible for: + // 1) SDL thread to call this method on end of playback + // 2) VCMI code to call queueNext() method to queue new file + // this leads to: + // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) + // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) + + GH.dispatchMainThread( + [this]() + { + boost::unique_lock lockGuard(mutex); + if(current != nullptr) + { + // if music is looped, play it again + if(current->play()) + return; + else + current.reset(); + } + + if(current == nullptr && next != nullptr) + { + current.reset(next.release()); + current->play(); + } + } + ); +} + +MusicEntry::MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart) + : owner(owner) + , music(nullptr) + , setName(std::move(setName)) + , startTime(static_cast(-1)) + , startPosition(0) + , loop(looped ? -1 : 1) + , fromStart(fromStart) + , playing(false) + +{ + if(!musicURI.empty()) + load(musicURI); +} + +MusicEntry::~MusicEntry() +{ + if(playing && loop > 0) + { + assert(0); + logGlobal->error("Attempt to delete music while playing!"); + Mix_HaltMusic(); + } + + if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) + { + assert(0); + logGlobal->error("Attempt to delete music while fading out!"); + Mix_HaltMusic(); + } + + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + if(music) + Mix_FreeMusic(music); +} + +void MusicEntry::load(const AudioPath & musicURI) +{ + if(music) + { + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + Mix_FreeMusic(music); + music = nullptr; + } + + if(CResourceHandler::get()->existsResource(musicURI)) + currentName = musicURI; + else + currentName = musicURI.addPrefix("MUSIC/"); + + music = nullptr; + + logGlobal->trace("Loading music file %s", currentName.getOriginalName()); + + try + { + auto * musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); + music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); + } + catch(std::exception & e) + { + logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); + logGlobal->error("Exception: %s", e.what()); + } + + if(!music) + { + logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); + return; + } +} + +bool MusicEntry::play() +{ + if(!(loop--) && music) //already played once - return + return false; + + if(!setName.empty()) + { + const auto & set = owner->musicsSet[setName]; + const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); + load(*iter); + } + + logGlobal->trace("Playing music file %s", currentName.getOriginalName()); + + if(!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) + { + float timeToStart = owner->trackPositions[currentName]; + startPosition = std::round(timeToStart * 1000); + + // erase stored position: + // if music track will be interrupted again - new position will be written in stop() method + // if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should + owner->trackPositions.erase(owner->trackPositions.find(currentName)); + + if(Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + else + { + startPosition = 0; + + if(Mix_PlayMusic(music, 1) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + + startTime = GH.input().getTicks(); + + playing = true; + return true; +} + +bool MusicEntry::stop(int fade_ms) +{ + if(Mix_PlayingMusic()) + { + playing = false; + loop = 0; + uint32_t endTime = GH.input().getTicks(); + assert(startTime != uint32_t(-1)); + float playDuration = (endTime - startTime + startPosition) / 1000.f; + owner->trackPositions[currentName] = playDuration; + logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); + + Mix_FadeOutMusic(fade_ms); + return true; + } + return false; +} + +bool MusicEntry::isPlaying() const +{ + return playing; +} + +bool MusicEntry::isSet(const std::string & set) +{ + return !setName.empty() && set == setName; +} + +bool MusicEntry::isTrack(const AudioPath & track) +{ + return setName.empty() && track == currentName; +} diff --git a/client/media/CMusicHandler.h b/client/media/CMusicHandler.h new file mode 100644 index 000000000..b85463d4a --- /dev/null +++ b/client/media/CMusicHandler.h @@ -0,0 +1,95 @@ +/* + * CMusicHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CAudioBase.h" +#include "IMusicPlayer.h" + +#include "../lib/CConfigHandler.h" + +struct _Mix_Music; +using Mix_Music = struct _Mix_Music; + +class CMusicHandler; + +//Class for handling one music file +class MusicEntry : boost::noncopyable +{ + CMusicHandler * owner; + Mix_Music * music; + + //if not null - set from which music will be randomly selected + std::string setName; + AudioPath currentName; + + uint32_t startTime; + uint32_t startPosition; + int loop; // -1 = indefinite + bool fromStart; + bool playing; + + void load(const AudioPath & musicURI); + +public: + MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); + ~MusicEntry(); + + bool isSet(const std::string & setName); + bool isTrack(const AudioPath & trackName); + bool isPlaying() const; + + bool play(); + bool stop(int fade_ms = 0); +}; + +class CMusicHandler final : public CAudioBase, public IMusicPlayer +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode & volumeNode); + + std::unique_ptr current; + std::unique_ptr next; + + boost::mutex mutex; + int volume = 0; // from 0 (mute) to 100 + + void queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); + void queueNext(std::unique_ptr queued); + void musicFinishedCallback() final; + + /// map -> + std::map> musicsSet; + /// stored position, in seconds at which music player should resume playing this track + std::map trackPositions; + +public: + CMusicHandler(); + ~CMusicHandler(); + + /// add entry with URI musicURI in set. Track will have ID musicID + void addEntryToSet(const std::string & set, const AudioPath & musicURI); + + void loadTerrainMusicThemes() final; + void setVolume(ui32 percent) final; + ui32 getVolume() const final; + + /// play track by URI, if loop = true music will be looped + void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) final; + /// play random track from this set + void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) final; + /// play random track from set (musicSet, entryID) + void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) final; + /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any + void stopMusic(int fade_ms) final; + + friend class MusicEntry; +}; diff --git a/client/media/CSoundHandler.cpp b/client/media/CSoundHandler.cpp new file mode 100644 index 000000000..532cdc617 --- /dev/null +++ b/client/media/CSoundHandler.cpp @@ -0,0 +1,385 @@ +/* + * CMusicHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CSoundHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../CGameInfo.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/CRandomGenerator.h" + +#include + +#define VCMI_SOUND_NAME(x) +#define VCMI_SOUND_FILE(y) #y, + +// sounds mapped to soundBase enum +static const std::string soundsList[] = { + "", // invalid + "", // todo + VCMI_SOUND_LIST +}; +#undef VCMI_SOUND_NAME +#undef VCMI_SOUND_FILE + +void CSoundHandler::onVolumeChange(const JsonNode & volumeNode) +{ + setVolume(volumeNode.Integer()); +} + +CSoundHandler::CSoundHandler(): + listener(settings.listen["general"]["sound"]), + ambientConfig(JsonPath::builtin("config/ambientSounds.json")) +{ + listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); + + if(ambientConfig["allocateChannels"].isNumber()) + Mix_AllocateChannels(ambientConfig["allocateChannels"].Integer()); + + if(isInitialized()) + { + Mix_ChannelFinished([](int channel) + { + CCS->soundh->soundFinishedCallback(channel); + }); + } +} + +CSoundHandler::~CSoundHandler() +{ + if(isInitialized()) + { + Mix_HaltChannel(-1); + + for(auto & chunk : soundChunks) + { + if(chunk.second.first) + Mix_FreeChunk(chunk.second.first); + } + } +} + +// Allocate an SDL chunk and cache it. +Mix_Chunk * CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) +{ + try + { + if(cache && soundChunks.find(sound) != soundChunks.end()) + return soundChunks[sound].first; + + auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); + SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second); + Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if(cache) + soundChunks.insert({sound, std::make_pair(chunk, std::move(data.first))}); + + return chunk; + } + catch(std::exception & e) + { + logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); + return nullptr; + } +} + +Mix_Chunk * CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) +{ + try + { + std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min(static_cast(100), data.second)); + + if(cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) + return soundChunksRaw[startBytes].first; + + SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second); + Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if(cache) + soundChunksRaw.insert({startBytes, std::make_pair(chunk, std::move(data.first))}); + + return chunk; + } + catch(std::exception & e) + { + logGlobal->warn("Cannot get sound chunk: %s", e.what()); + return nullptr; + } +} + +int CSoundHandler::ambientDistToVolume(int distance) const +{ + const auto & distancesVector = ambientConfig["distances"].Vector(); + + if(distance >= distancesVector.size()) + return 0; + + int volumeByDistance = static_cast(distancesVector[distance].Integer()); + return volumeByDistance * ambientConfig["volume"].Integer() / 100; +} + +void CSoundHandler::ambientStopSound(const AudioPath & soundId) +{ + stopSound(ambientChannels[soundId]); + setChannelVolume(ambientChannels[soundId], volume); +} + +uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) +{ + if(!isInitialized() || sound.empty()) + return 0; + + auto resourcePath = sound.addPrefix("SOUNDS/"); + + if(!CResourceHandler::get()->existsResource(resourcePath)) + return 0; + + auto data = CResourceHandler::get()->load(resourcePath)->readAll(); + + SDL_AudioSpec spec; + uint32_t audioLen; + uint8_t * audioBuf; + uint32_t miliseconds = 0; + + if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) + { + SDL_FreeWAV(audioBuf); + uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8; + uint32_t sampleCount = audioLen / sampleSize; + uint32_t sampleLen = sampleCount / spec.channels; + miliseconds = 1000 * sampleLen / spec.freq; + } + + return miliseconds; +} + +// Plays a sound, and return its channel so we can fade it out later +int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) +{ + assert(soundID < soundBase::sound_after_last); + auto sound = AudioPath::builtin(soundsList[soundID]); + logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); + + return playSound(sound, repeats, true); +} + +int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) +{ + if(!isInitialized() || sound.empty()) + return -1; + + int channel; + Mix_Chunk * chunk = GetSoundChunk(sound, cache); + + if(chunk) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if(channel == -1) + { + logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); + if(!cache) + Mix_FreeChunk(chunk); + } + else if(cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + else + channel = -1; + + return channel; +} + +int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) +{ + int channel = -1; + if(Mix_Chunk * chunk = GetSoundChunk(data, cache)) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if(channel == -1) + { + logGlobal->error("Unable to play sound, error %s", Mix_GetError()); + if(!cache) + Mix_FreeChunk(chunk); + } + else if(cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + return channel; +} + +// Helper. Randomly select a sound from an array and play it +int CSoundHandler::playSoundFromSet(std::vector & sound_vec) +{ + return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); +} + +void CSoundHandler::stopSound(int handler) +{ + if(isInitialized() && handler != -1) + Mix_HaltChannel(handler); +} + +ui32 CSoundHandler::getVolume() const +{ + return volume; +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setVolume(ui32 percent) +{ + volume = std::min(100u, percent); + + if(isInitialized()) + { + setChannelVolume(-1, volume); + + for(const auto & channel : channelVolumes) + updateChannelVolume(channel.first); + } +} + +void CSoundHandler::updateChannelVolume(int channel) +{ + if(channelVolumes.count(channel)) + setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); + else + setChannelVolume(channel, getVolume()); +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setChannelVolume(int channel, ui32 percent) +{ + Mix_Volume(channel, (MIX_MAX_VOLUME * percent) / 100); +} + +void CSoundHandler::setCallback(int channel, std::function function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + auto iter = callbacks.find(channel); + + //channel not found. It may have finished so fire callback now + if(iter == callbacks.end()) + function(); + else + iter->second.push_back(function); +} + +void CSoundHandler::resetCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + callbacks.erase(channel); +} + +void CSoundHandler::soundFinishedCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + if(callbacks.count(channel) == 0) + return; + + // store callbacks from container locally - SDL might reuse this channel for another sound + // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own + auto callback = callbacks.at(channel); + callbacks.erase(channel); + + if(!callback.empty()) + { + GH.dispatchMainThread( + [callback]() + { + for(const auto & entry : callback) + entry(); + } + ); + } +} + +void CSoundHandler::initCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel] = {}; +} + +void CSoundHandler::initCallback(int channel, const std::function & function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel].push_back(function); +} + +int CSoundHandler::ambientGetRange() const +{ + return ambientConfig["range"].Integer(); +} + +void CSoundHandler::ambientUpdateChannels(std::map soundsArg) +{ + boost::mutex::scoped_lock guard(mutex); + + std::vector stoppedSounds; + for(const auto & pair : ambientChannels) + { + const auto & soundId = pair.first; + const int channel = pair.second; + + if(!vstd::contains(soundsArg, soundId)) + { + ambientStopSound(soundId); + stoppedSounds.push_back(soundId); + } + else + { + int channelVolume = ambientDistToVolume(soundsArg[soundId]); + channelVolumes[channel] = channelVolume; + updateChannelVolume(channel); + } + } + for(const auto & soundId : stoppedSounds) + { + channelVolumes.erase(ambientChannels[soundId]); + ambientChannels.erase(soundId); + } + + for(const auto & pair : soundsArg) + { + const auto & soundId = pair.first; + const int distance = pair.second; + + if(!vstd::contains(ambientChannels, soundId)) + { + int channel = playSound(soundId, -1); + int channelVolume = ambientDistToVolume(distance); + channelVolumes[channel] = channelVolume; + + updateChannelVolume(channel); + ambientChannels[soundId] = channel; + } + } +} + +void CSoundHandler::ambientStopAllChannels() +{ + boost::mutex::scoped_lock guard(mutex); + + for(const auto & ch : ambientChannels) + { + ambientStopSound(ch.first); + } + channelVolumes.clear(); + ambientChannels.clear(); +} diff --git a/client/media/CSoundHandler.h b/client/media/CSoundHandler.h new file mode 100644 index 000000000..5a10a5493 --- /dev/null +++ b/client/media/CSoundHandler.h @@ -0,0 +1,78 @@ +/* + * CSoundHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CAudioBase.h" +#include "ISoundPlayer.h" + +#include "../lib/CConfigHandler.h" + +struct Mix_Chunk; + +class CSoundHandler final : public CAudioBase, public ISoundPlayer +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode & volumeNode); + + using CachedChunk = std::pair>; + std::map soundChunks; + std::map, CachedChunk> soundChunksRaw; + + Mix_Chunk * GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk * GetSoundChunk(std::pair, si64> & data, bool cache); + + /// have entry for every currently active channel + /// vector will be empty if callback was not set + std::map>> callbacks; + + /// Protects access to callbacks member to avoid data races: + /// SDL calls sound finished callbacks from audio thread + boost::mutex mutexCallbacks; + + int ambientDistToVolume(int distance) const; + void ambientStopSound(const AudioPath & soundId); + void updateChannelVolume(int channel); + + const JsonNode ambientConfig; + + boost::mutex mutex; + std::map ambientChannels; + std::map channelVolumes; + int volume = 0; + + void initCallback(int channel, const std::function & function); + void initCallback(int channel); + +public: + CSoundHandler(); + ~CSoundHandler(); + + ui32 getVolume() const final; + void setVolume(ui32 percent) final; + void setChannelVolume(int channel, ui32 percent); + + // Sounds + uint32_t getSoundDurationMilliseconds(const AudioPath & sound) final; + int playSound(soundBase::soundID soundID, int repeats = 0) final; + int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) final; + int playSound(std::pair, si64> & data, int repeats = 0, bool cache = false) final; + int playSoundFromSet(std::vector & sound_vec) final; + void stopSound(int handler) final; + + void setCallback(int channel, std::function function) final; + void resetCallback(int channel) final; + void soundFinishedCallback(int channel) final; + + int ambientGetRange() const final; + void ambientUpdateChannels(std::map currentSounds) final; + void ambientStopAllChannels() final; +}; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp new file mode 100644 index 000000000..3775942ad --- /dev/null +++ b/client/media/CVideoHandler.cpp @@ -0,0 +1,658 @@ +/* + * CVideoHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CVideoHandler.h" + +#ifndef DISABLE_VIDEO + +#include "ISoundPlayer.h" + +#include "../CGameInfo.h" +#include "../CMT.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../renderSDL/SDL_Extensions.h" + +#include "../../lib/filesystem/CInputStream.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/Languages.h" + +#include + +extern "C" { +#include +#include +#include +#include +} + +// Define a set of functions to read data +static int lodRead(void * opaque, uint8_t * buf, int size) +{ + auto * data = static_cast(opaque); + auto bytesRead = data->read(buf, size); + if(bytesRead == 0) + return AVERROR_EOF; + + return bytesRead; +} + +static si64 lodSeek(void * opaque, si64 pos, int whence) +{ + auto * data = static_cast(opaque); + + if(whence & AVSEEK_SIZE) + return data->getSize(); + + return data->seek(pos); +} + +[[noreturn]] static void throwFFmpegError(int errorCode) +{ + std::array errorMessage{}; + av_strerror(errorCode, errorMessage.data(), errorMessage.size()); + + throw std::runtime_error(errorMessage.data()); +} + +static std::unique_ptr findVideoData(const VideoPath & videoToOpen) +{ + if(CResourceHandler::get()->existsResource(videoToOpen)) + return CResourceHandler::get()->load(videoToOpen); + + auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/"); + auto lowQualityVideo = videoToOpen.toType(); + auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType(); + + if(CResourceHandler::get()->existsResource(highQualityVideoToOpenWithDir)) + return CResourceHandler::get()->load(highQualityVideoToOpenWithDir); + + if(CResourceHandler::get()->existsResource(lowQualityVideo)) + return CResourceHandler::get()->load(lowQualityVideo); + + if(CResourceHandler::get()->existsResource(lowQualityVideoWithDir)) + return CResourceHandler::get()->load(lowQualityVideoWithDir); + + return nullptr; +} + +bool FFMpegStream::openInput(const VideoPath & videoToOpen) +{ + input = findVideoData(videoToOpen); + + return input != nullptr; +} + +void FFMpegStream::openContext() +{ + static const int BUFFER_SIZE = 4096; + input->seek(0); + + auto * buffer = static_cast(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg + context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek); + + formatContext = avformat_alloc_context(); + formatContext->pb = context; + // filename is not needed - file was already open and stored in this->data; + int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr); + + if(avfopen != 0) + throwFFmpegError(avfopen); + + // Retrieve stream information + int findStreamInfo = avformat_find_stream_info(formatContext, nullptr); + + if(avfopen < 0) + throwFFmpegError(findStreamInfo); +} + +void FFMpegStream::openCodec(int desiredStreamIndex) +{ + streamIndex = desiredStreamIndex; + + // Find the decoder for the stream + codec = avcodec_find_decoder(formatContext->streams[streamIndex]->codecpar->codec_id); + + if(codec == nullptr) + throw std::runtime_error("Unsupported codec"); + + codecContext = avcodec_alloc_context3(codec); + if(codecContext == nullptr) + throw std::runtime_error("Failed to create codec context"); + + // Get a pointer to the codec context for the video stream + int ret = avcodec_parameters_to_context(codecContext, formatContext->streams[streamIndex]->codecpar); + if(ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContext); + throwFFmpegError(ret); + } + + // Open codec + ret = avcodec_open2(codecContext, codec, nullptr); + if(ret < 0) + { + // Could not open codec + codec = nullptr; + throwFFmpegError(ret); + } + + // Allocate video frame + frame = av_frame_alloc(); +} + +const AVCodecParameters * FFMpegStream::getCodecParameters() const +{ + return formatContext->streams[streamIndex]->codecpar; +} + +const AVCodecContext * FFMpegStream::getCodecContext() const +{ + return codecContext; +} + +const AVFrame * FFMpegStream::getCurrentFrame() const +{ + return frame; +} + +void CVideoInstance::openVideo() +{ + openContext(); + openCodec(findVideoStream()); +} + +void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput) +{ + //setup scaling + if(scaleToScreenSize) + { + dimensions.x = screen->w; + dimensions.y = screen->h; + } + else + { + dimensions.x = getCodecContext()->width; + dimensions.y = getCodecContext()->height; + } + + // Allocate a place to put our YUV image on that screen + if (useTextureOutput) + { + std::array potentialFormats = { + AV_PIX_FMT_YUV420P, // -> SDL_PIXELFORMAT_IYUV - most of H3 videos use YUV format, so it is preferred to save some space & conversion time + AV_PIX_FMT_RGB32, // -> SDL_PIXELFORMAT_ARGB8888 - some .smk videos actually use palette, so RGB > YUV. This is also our screen texture format + AV_PIX_FMT_NONE + }; + + auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), getCodecContext()->pix_fmt, false, nullptr); + + if (preferredFormat == AV_PIX_FMT_YUV420P) + textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + else + textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt, + dimensions.x, dimensions.y, preferredFormat, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + else + { + surface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); + sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt, + dimensions.x, dimensions.y, AV_PIX_FMT_RGB32, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + + if (sws == nullptr) + throw std::runtime_error("Failed to create sws"); +} + +void FFMpegStream::decodeNextFrame() +{ + AVPacket packet; + + for(;;) + { + int rc = avcodec_receive_frame(codecContext, frame); + if(rc == AVERROR(EAGAIN)) + break; + + if(rc < 0) + throwFFmpegError(rc); + + return; + } + + for(;;) + { + int ret = av_read_frame(formatContext, &packet); + if(ret < 0) + { + if(ret == AVERROR_EOF) + { + av_packet_unref(&packet); + av_frame_free(&frame); + frame = nullptr; + return; + } + throwFFmpegError(ret); + } + + // Is this a packet from the video stream? + if(packet.stream_index == streamIndex) + { + // Decode video frame + int rc = avcodec_send_packet(codecContext, &packet); + if(rc < 0 && rc != AVERROR(EAGAIN)) + throwFFmpegError(rc); + + rc = avcodec_receive_frame(codecContext, frame); + if(rc == AVERROR(EAGAIN)) + { + av_packet_unref(&packet); + continue; + } + if(rc < 0) + throwFFmpegError(rc); + + av_packet_unref(&packet); + return; + } + av_packet_unref(&packet); + } +} + +bool CVideoInstance::loadNextFrame() +{ + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); + + if(!frame) + return false; + + uint8_t * data[4] = {}; + int linesize[4] = {}; + + if(textureYUV) + { + av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_YUV420P, 1); + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + SDL_UpdateYUVTexture(textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); + av_freep(&data[0]); + } + if(textureRGB) + { + av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_RGB32, 1); + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + SDL_UpdateTexture(textureRGB, nullptr, data[0], linesize[0]); + av_freep(&data[0]); + } + if(surface) + { + // Avoid buffer overflow caused by sws_scale(): + // http://trac.ffmpeg.org/ticket/9254 + + size_t pic_bytes = surface->pitch * surface->h; + size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ + void * for_sws = av_malloc(pic_bytes + ffmped_pad); + data[0] = (ui8 *)for_sws; + linesize[0] = surface->pitch; + + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + memcpy(surface->pixels, for_sws, pic_bytes); + av_free(for_sws); + } + return true; +} + +bool CVideoInstance::videoEnded() +{ + return getCurrentFrame() == nullptr; +} + +CVideoInstance::~CVideoInstance() +{ + sws_freeContext(sws); + SDL_DestroyTexture(textureYUV); + SDL_DestroyTexture(textureRGB); + SDL_FreeSurface(surface); +} + +FFMpegStream::~FFMpegStream() +{ + av_frame_free(&frame); + + avcodec_close(codecContext); + avcodec_free_context(&codecContext); + + avcodec_close(codecContext); + avcodec_free_context(&codecContext); + + avformat_close_input(&formatContext); + av_free(context); +} + +Point CVideoInstance::size() +{ + if(!getCurrentFrame()) + throw std::runtime_error("Invalid video frame!"); + + return Point(getCurrentFrame()->width, getCurrentFrame()->height); +} + +void CVideoInstance::show(const Point & position, Canvas & canvas) +{ + if(sws == nullptr) + throw std::runtime_error("No video to show!"); + + CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position); +} + +double FFMpegStream::getCurrentFrameEndTime() const +{ +#if(LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + return (frame->pts + packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base); +} + +double FFMpegStream::getCurrentFrameDuration() const +{ +#if(LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + return packet_duration * av_q2d(formatContext->streams[streamIndex]->time_base); +} + +void CVideoInstance::tick(uint32_t msPassed) +{ + if(sws == nullptr) + throw std::runtime_error("No video to show!"); + + if(videoEnded()) + throw std::runtime_error("Video already ended!"); + + frameTime += msPassed / 1000.0; + + if(frameTime >= getCurrentFrameEndTime()) + loadNextFrame(); +} + +struct FFMpegFormatDescription +{ + uint8_t sampleSizeBytes; + uint8_t wavFormatID; + bool isPlanar; +}; + +static FFMpegFormatDescription getAudioFormatProperties(int audioFormat) +{ + switch (audioFormat) + { + case AV_SAMPLE_FMT_U8: return { 1, 1, false}; + case AV_SAMPLE_FMT_U8P: return { 1, 1, true}; + case AV_SAMPLE_FMT_S16: return { 2, 1, false}; + case AV_SAMPLE_FMT_S16P: return { 2, 1, true}; + case AV_SAMPLE_FMT_S32: return { 4, 1, false}; + case AV_SAMPLE_FMT_S32P: return { 4, 1, true}; + case AV_SAMPLE_FMT_S64: return { 8, 1, false}; + case AV_SAMPLE_FMT_S64P: return { 8, 1, true}; + case AV_SAMPLE_FMT_FLT: return { 4, 3, false}; + case AV_SAMPLE_FMT_FLTP: return { 4, 3, true}; + case AV_SAMPLE_FMT_DBL: return { 8, 3, false}; + case AV_SAMPLE_FMT_DBLP: return { 8, 3, true}; + } + throw std::runtime_error("Invalid audio format"); +} + +int FFMpegStream::findAudioStream() const +{ + std::vector audioStreamIndices; + + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + audioStreamIndices.push_back(i); + + if (audioStreamIndices.empty()) + return -1; + + if (audioStreamIndices.size() == 1) + return audioStreamIndices.front(); + + // multiple audio streams - try to pick best one based on language settings + std::map streamToLanguage; + + // Approach 1 - check if stream has language set in metadata + for (auto const & index : audioStreamIndices) + { + const AVDictionaryEntry *e = av_dict_get(formatContext->streams[index]->metadata, "language", nullptr, 0); + if (e) + streamToLanguage[index] = e->value; + } + + // Approach 2 - no metadata found. This may be video from Chronicles which have predefined (presumably hardcoded) list of languages + if (streamToLanguage.empty()) + { + if (audioStreamIndices.size() == 2) + { + streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2; + streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2; + } + + if (audioStreamIndices.size() == 5) + { + streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2; + streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::FRENCH).tagISO2; + streamToLanguage[audioStreamIndices[2]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2; + streamToLanguage[audioStreamIndices[3]] = Languages::getLanguageOptions(Languages::ELanguages::ITALIAN).tagISO2; + streamToLanguage[audioStreamIndices[4]] = Languages::getLanguageOptions(Languages::ELanguages::SPANISH).tagISO2; + } + } + + std::string preferredLanguageName = CGI->generaltexth->getPreferredLanguage(); + std::string preferredTag = Languages::getLanguageOptions(preferredLanguageName).tagISO2; + + for (auto const & entry : streamToLanguage) + if (entry.second == preferredTag) + return entry.first; + + return audioStreamIndices.front(); +} + +int FFMpegStream::findVideoStream() const +{ + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + return i; + + return -1; +} + +std::pair, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen) +{ + if (!openInput(videoToOpen)) + return { nullptr, 0}; + openContext(); + openCodec(findAudioStream()); + + const auto * codecpar = getCodecParameters(); + + std::vector samples; + + auto formatProperties = getAudioFormatProperties(codecpar->format); +#if(LIBAVUTIL_VERSION_MAJOR < 58) + int numChannels = codecpar->channels; +#else + int numChannels = codecpar->ch_layout.nb_channels; +#endif + + samples.reserve(44100 * 5); // arbitrary 5-second buffer + + for (;;) + { + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); + + if (!frame) + break; + + int samplesToRead = frame->nb_samples * numChannels; + int bytesToRead = samplesToRead * formatProperties.sampleSizeBytes; + + if (formatProperties.isPlanar && numChannels > 1) + { + // Workaround for lack of resampler + // Currently, ffmpeg on conan systems is built without sws resampler + // Because of that, and because wav format does not supports 'planar' formats from ffmpeg + // we need to de-planarize it and convert to "normal" (non-planar / interleaved) stream + samples.reserve(samples.size() + bytesToRead); + for (int sm = 0; sm < frame->nb_samples; ++sm) + for (int ch = 0; ch < numChannels; ++ch) + samples.insert(samples.end(), frame->data[ch] + sm * formatProperties.sampleSizeBytes, frame->data[ch] + (sm+1) * formatProperties.sampleSizeBytes ); + } + else + { + samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead); + } + } + + struct WavHeader { + ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; + ui32 ChunkSize; + ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; + ui8 fmt[4] = {'f', 'm', 't', ' '}; + ui32 Subchunk1Size = 16; + ui16 AudioFormat = 1; + ui16 NumOfChan = 2; + ui32 SamplesPerSec = 22050; + ui32 bytesPerSec = 22050 * 2; + ui16 blockAlign = 2; + ui16 bitsPerSample = 32; + ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; + ui32 Subchunk2Size; + }; + + WavHeader wav; + wav.ChunkSize = samples.size() + sizeof(WavHeader) - 8; + wav.AudioFormat = formatProperties.wavFormatID; // 1 = PCM, 3 = IEEE float + wav.NumOfChan = numChannels; + wav.SamplesPerSec = codecpar->sample_rate; + wav.bytesPerSec = codecpar->sample_rate * formatProperties.sampleSizeBytes; + wav.bitsPerSample = formatProperties.sampleSizeBytes * 8; + wav.Subchunk2Size = samples.size() + sizeof(WavHeader) - 44; + auto * wavPtr = reinterpret_cast(&wav); + + auto dat = std::make_pair(std::make_unique(samples.size() + sizeof(WavHeader)), samples.size() + sizeof(WavHeader)); + std::copy(wavPtr, wavPtr + sizeof(WavHeader), dat.first.get()); + std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(WavHeader)); + + return dat; +} + +bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) +{ + CVideoInstance instance; + CAudioInstance audio; + + auto extractedAudio = audio.extractAudio(name); + int audioHandle = CCS->soundh->playSound(extractedAudio); + + if (!instance.openInput(name)) + return true; + + instance.openVideo(); + instance.prepareOutput(scale, useOverlay); + + auto lastTimePoint = boost::chrono::steady_clock::now(); + + while(instance.loadNextFrame()) + { + if(stopOnKey) + { + GH.input().fetchEvents(); + if(GH.input().ignoreEventsUntilInput()) + { + CCS->soundh->stopSound(audioHandle); + return false; + } + } + + SDL_Rect rect; + rect.x = position.x; + rect.y = position.y; + rect.w = instance.dimensions.x; + rect.h = instance.dimensions.y; + + if(useOverlay) + SDL_RenderFillRect(mainRenderer, &rect); + else + SDL_RenderClear(mainRenderer); + + if(instance.textureYUV) + SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect); + else + SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect); + + SDL_RenderPresent(mainRenderer); + + // Framerate delay + double targetFrameTimeSeconds = instance.getCurrentFrameDuration(); + auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * targetFrameTimeSeconds)); + + auto timePointAfterPresent = boost::chrono::steady_clock::now(); + auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); + + logGlobal->info("Sleeping for %d", (targetFrameTime - timeSpentBusy).count()); + if(targetFrameTime > timeSpentBusy) + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + + lastTimePoint = boost::chrono::steady_clock::now(); + } + return true; +} + +bool CVideoPlayer::playIntroVideo(const VideoPath & name) +{ + return openAndPlayVideoImpl(name, Point(0, 0), true, true, true); +} + +void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) +{ + openAndPlayVideoImpl(name, position, false, false, false); +} + +std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool scaleToScreen) +{ + auto result = std::make_unique(); + + if (!result->openInput(name)) + return nullptr; + + result->openVideo(); + result->prepareOutput(scaleToScreen, false); + result->loadNextFrame(); // prepare 1st frame + + return result; +} + +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +{ + CAudioInstance audio; + return audio.extractAudio(videoToOpen); +} + +#endif diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h new file mode 100644 index 000000000..3fc6256cd --- /dev/null +++ b/client/media/CVideoHandler.h @@ -0,0 +1,110 @@ +/* + * CVideoHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#ifndef DISABLE_VIDEO + +#include "../lib/Point.h" +#include "IVideoPlayer.h" + +struct SDL_Surface; +struct SDL_Texture; +struct AVFormatContext; +struct AVCodecContext; +struct AVCodecParameters; +struct AVCodec; +struct AVFrame; +struct AVIOContext; + +VCMI_LIB_NAMESPACE_BEGIN +class CInputStream; +class Point; +VCMI_LIB_NAMESPACE_END + +class FFMpegStream : boost::noncopyable +{ + std::unique_ptr input; + + AVIOContext * context = nullptr; + AVFormatContext * formatContext = nullptr; + + const AVCodec * codec = nullptr; + AVCodecContext * codecContext = nullptr; + int streamIndex = -1; + + AVFrame * frame = nullptr; + +protected: + void openContext(); + void openCodec(int streamIndex); + + int findVideoStream() const; + int findAudioStream() const; + + const AVCodecParameters * getCodecParameters() const; + const AVCodecContext * getCodecContext() const; + void decodeNextFrame(); + const AVFrame * getCurrentFrame() const; + double getCurrentFrameEndTime() const; + double getCurrentFrameDuration() const; + +public: + virtual ~FFMpegStream(); + + bool openInput(const VideoPath & fname); +}; + +class CAudioInstance final : public FFMpegStream +{ +public: + std::pair, si64> extractAudio(const VideoPath & videoToOpen); +}; + +class CVideoInstance final : public IVideoInstance, public FFMpegStream +{ + friend class CVideoPlayer; + + struct SwsContext * sws = nullptr; + SDL_Texture * textureRGB = nullptr; + SDL_Texture * textureYUV = nullptr; + SDL_Surface * surface = nullptr; + Point dimensions; + + /// video playback current progress, in seconds + double frameTime = 0.0; + + void prepareOutput(bool scaleToScreenSize, bool useTextureOutput); + +public: + ~CVideoInstance(); + + void openVideo(); + bool loadNextFrame(); + + bool videoEnded() final; + Point size() final; + + void show(const Point & position, Canvas & canvas) final; + void tick(uint32_t msPassed) final; +}; + +class CVideoPlayer final : public IVideoPlayer +{ + bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey); + void openVideoFile(CVideoInstance & state, const VideoPath & fname); + +public: + bool playIntroVideo(const VideoPath & name) final; + void playSpellbookAnimation(const VideoPath & name, const Point & position) final; + std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; + std::pair, si64> getAudio(const VideoPath & videoToOpen) final; +}; + +#endif diff --git a/client/media/IMusicPlayer.h b/client/media/IMusicPlayer.h new file mode 100644 index 000000000..1dde50ed7 --- /dev/null +++ b/client/media/IMusicPlayer.h @@ -0,0 +1,33 @@ +/* + * IMusicPlayer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/filesystem/ResourcePath.h" + +class IMusicPlayer +{ +public: + virtual ~IMusicPlayer() = default; + + virtual void loadTerrainMusicThemes() = 0; + virtual void setVolume(ui32 percent) = 0; + virtual ui32 getVolume() const = 0; + + virtual void musicFinishedCallback() = 0; + + /// play track by URI, if loop = true music will be looped + virtual void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) = 0; + /// play random track from this set + virtual void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) = 0; + /// play random track from set (musicSet, entryID) + virtual void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) = 0; + /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any + virtual void stopMusic(int fade_ms = 1000) = 0; +}; diff --git a/client/media/ISoundPlayer.h b/client/media/ISoundPlayer.h new file mode 100644 index 000000000..9b3d9d5e9 --- /dev/null +++ b/client/media/ISoundPlayer.h @@ -0,0 +1,35 @@ +/* + * ISoundPlayer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/CSoundBase.h" +#include "../lib/filesystem/ResourcePath.h" + +class ISoundPlayer +{ +public: + virtual ~ISoundPlayer() = default; + + virtual int playSound(soundBase::soundID soundID, int repeats = 0) = 0; + virtual int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) = 0; + virtual int playSound(std::pair, si64> & data, int repeats = 0, bool cache = false) = 0; + virtual int playSoundFromSet(std::vector & sound_vec) = 0; + virtual void stopSound(int handler) = 0; + + virtual ui32 getVolume() const = 0; + virtual void setVolume(ui32 percent) = 0; + virtual uint32_t getSoundDurationMilliseconds(const AudioPath & sound) = 0; + virtual void setCallback(int channel, std::function function) = 0; + virtual void resetCallback(int channel) = 0; + virtual void soundFinishedCallback(int channel) = 0; + virtual void ambientUpdateChannels(std::map currentSounds) = 0; + virtual void ambientStopAllChannels() = 0; + virtual int ambientGetRange() const = 0; +}; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h new file mode 100644 index 000000000..a867e9b24 --- /dev/null +++ b/client/media/IVideoPlayer.h @@ -0,0 +1,54 @@ +/* + * IVideoPlayer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/filesystem/ResourcePath.h" + +class Canvas; + +VCMI_LIB_NAMESPACE_BEGIN +class Point; +VCMI_LIB_NAMESPACE_END + +class IVideoInstance +{ +public: + /// Returns true if video playback is over + virtual bool videoEnded() = 0; + + /// Returns dimensions of the video + virtual Point size() = 0; + + /// Displays current frame at specified position + virtual void show(const Point & position, Canvas & canvas) = 0; + + /// Advances video playback by specified duration + virtual void tick(uint32_t msPassed) = 0; + + virtual ~IVideoInstance() = default; +}; + +class IVideoPlayer : boost::noncopyable +{ +public: + /// Plays video on top of the screen, returns only after playback is over, aborts on input event + virtual bool playIntroVideo(const VideoPath & name) = 0; + + /// Plays video on top of the screen, returns only after playback is over + virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0; + + /// Load video from specified path. Returns nullptr on failure + virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) = 0; + + /// Extracts audio data from provided video in wav format + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; + + virtual ~IVideoPlayer() = default; +}; diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index b69cc6199..620c08587 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -13,7 +13,6 @@ #include "Images.h" #include "TextControls.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" @@ -23,6 +22,7 @@ #include "../gui/MouseButton.h" #include "../gui/Shortcut.h" #include "../gui/InterfaceObjectConfigurable.h" +#include "../media/ISoundPlayer.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 6699476da..58f50dcda 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -26,7 +26,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp new file mode 100644 index 000000000..2d68b82ca --- /dev/null +++ b/client/widgets/VideoWidget.cpp @@ -0,0 +1,148 @@ +/* + * TextControls.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "VideoWidget.h" + +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" +#include "../media/IVideoPlayer.h" +#include "../render/Canvas.h" + +VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio) + : playAudio(playAudio) +{ + addUsedEvents(TIME); + pos += position; + playVideo(video); +} + +VideoWidgetBase::~VideoWidgetBase() = default; + +void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) +{ + videoInstance = CCS->videoh->open(fileToPlay, false); + if (videoInstance) + { + pos.w = videoInstance->size().x; + pos.h = videoInstance->size().y; + } + + if (playAudio) + { + loadAudio(fileToPlay); + if (isActive()) + startAudio(); + } +} + +void VideoWidgetBase::show(Canvas & to) +{ + if(videoInstance) + videoInstance->show(pos.topLeft(), to); +} + +void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay) +{ + if (!playAudio) + return; + + audioData = CCS->videoh->getAudio(fileToPlay); +} + +void VideoWidgetBase::startAudio() +{ + if(audioData.first == nullptr) + return; + + audioHandle = CCS->soundh->playSound(audioData); + + if(audioHandle != -1) + { + CCS->soundh->setCallback( + audioHandle, + [this]() + { + this->audioHandle = -1; + } + ); + } +} + +void VideoWidgetBase::stopAudio() +{ + if(audioHandle != -1) + { + CCS->soundh->resetCallback(audioHandle); + CCS->soundh->stopSound(audioHandle); + audioHandle = -1; + } +} + +void VideoWidgetBase::activate() +{ + CIntObject::activate(); + startAudio(); +} + +void VideoWidgetBase::deactivate() +{ + CIntObject::deactivate(); + stopAudio(); +} + +void VideoWidgetBase::showAll(Canvas & to) +{ + if(videoInstance) + videoInstance->show(pos.topLeft(), to); +} + +void VideoWidgetBase::tick(uint32_t msPassed) +{ + if(videoInstance) + { + videoInstance->tick(msPassed); + + if(videoInstance->videoEnded()) + { + videoInstance.reset(); + stopAudio(); + onPlaybackFinished(); + } + } +} + +VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) + : VideoWidgetBase(position, prologue, playAudio) + , loopedVideo(looped) +{ +} + +VideoWidget::VideoWidget(const Point & position, const VideoPath & looped, bool playAudio) + : VideoWidgetBase(position, looped, playAudio) + , loopedVideo(looped) +{ +} + +void VideoWidget::onPlaybackFinished() +{ + playVideo(loopedVideo); +} + +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback) + : VideoWidgetBase(position, video, playAudio) + , callback(callback) +{ +} + +void VideoWidgetOnce::onPlaybackFinished() +{ + callback(); +} diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h new file mode 100644 index 000000000..f7c963d51 --- /dev/null +++ b/client/widgets/VideoWidget.h @@ -0,0 +1,65 @@ +/* + * VideoWidget.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../gui/CIntObject.h" + +#include "../lib/filesystem/ResourcePath.h" + +class IVideoInstance; + +class VideoWidgetBase : public CIntObject +{ + std::unique_ptr videoInstance; + + std::pair, si64> audioData = {nullptr, 0}; + int audioHandle = -1; + bool playAudio = false; + + void loadAudio(const VideoPath & file); + void startAudio(); + void stopAudio(); + +protected: + VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio); + + virtual void onPlaybackFinished() = 0; + void playVideo(const VideoPath & video); + +public: + ~VideoWidgetBase(); + + void activate() override; + void deactivate() override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; + + void setPlaybackFinishedCallback(std::function); +}; + +class VideoWidget final: public VideoWidgetBase +{ + VideoPath loopedVideo; + + void onPlaybackFinished() final; +public: + VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio); + VideoWidget(const Point & position, const VideoPath & looped, bool playAudio); +}; + +class VideoWidgetOnce final: public VideoWidgetBase +{ + std::function callback; + + void onPlaybackFinished() final; +public: + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback); +}; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index e27f20223..99066aef4 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -18,12 +18,12 @@ #include "CCreatureWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" #include "../widgets/MiscWidgets.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" @@ -43,6 +43,8 @@ #include "../../CCallback.h" #include "../../lib/CArtHandler.h" #include "../../lib/CBuildingHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CSoundBase.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/GameSettings.h" diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 8b5f70edb..d234e0e95 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -11,13 +11,13 @@ #include "CPuzzleWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../adventureMap/CResDataBar.h" #include "../gui/CGuiHandler.h" #include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../mapView/MapView.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 8a0d800d4..5c89602c3 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -19,12 +19,12 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" -#include "../CVideoHandler.h" #include "../battle/BattleInterface.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IVideoPlayer.h" #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/CComponent.h" #include "../widgets/CTextInput.h" @@ -524,13 +524,13 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); + CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNLFT.SMK"), pos.topLeft() + Point(13, 15)); } void CSpellWindow::turnPageRight() { if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); + CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNRGH.SMK"), pos.topLeft() + Point(13, 15)); } void CSpellWindow::keyPressed(EShortcut key) diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 72f945547..1655e9710 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -16,7 +16,6 @@ #include "../../lib/CGeneralTextHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" -#include "../CVideoHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" @@ -24,6 +23,7 @@ #include "../widgets/Images.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../render/Canvas.h" CTutorialWindow::CTutorialWindow(const TutorialMode & m) @@ -54,7 +54,10 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) void CTutorialWindow::setContent() { - video = "tutorial/" + videos[page]; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + auto video = VideoPath::builtin("tutorial/" + videos[page]); + + videoPlayer = std::make_shared(Point(30, 120), video, false); buttonLeft->block(page<1); buttonRight->block(page>videos.size() - 2); @@ -98,26 +101,3 @@ void CTutorialWindow::previous() deactivate(); activate(); } - -void CTutorialWindow::show(Canvas & to) -{ - CCS->videoh->update(pos.x + 30, pos.y + 120, to.getInternalSurface(), true, false, - [&]() - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin(video)); - }); - - CIntObject::show(to); -} - -void CTutorialWindow::activate() -{ - CCS->videoh->open(VideoPath::builtin(video)); - CIntObject::activate(); -} - -void CTutorialWindow::deactivate() -{ - CCS->videoh->close(); -} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index d002f9aed..c4fcf31f9 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -15,6 +15,7 @@ class CFilledTexture; class CButton; class CLabel; class CMultiLineLabel; +class VideoWidget; enum TutorialMode { @@ -33,8 +34,8 @@ class CTutorialWindow : public CWindowObject std::shared_ptr labelTitle; std::shared_ptr labelInformation; + std::shared_ptr videoPlayer; - std::string video; std::vector videos; int page; @@ -47,8 +48,4 @@ class CTutorialWindow : public CWindowObject public: CTutorialWindow(const TutorialMode & m); static void openWindowFirstTime(const TutorialMode & m); - - void show(Canvas & to) override; - void activate() override; - void deactivate() override; }; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 0403c8c91..30f70432b 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -25,7 +25,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 48ea11681..71476a14d 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -19,9 +19,7 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" #include "../Client.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CVideoHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" @@ -36,6 +34,7 @@ #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../widgets/ObjectLists.h" +#include "../widgets/VideoWidget.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" @@ -58,6 +57,7 @@ #include "../lib/GameSettings.h" #include "../lib/CondSh.h" #include "../lib/CSkillHandler.h" +#include "../lib/CSoundBase.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/TextOperations.h" @@ -515,11 +515,11 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->block(true); } if(LOCPLINT->castleInt) - CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo, false); else if(const auto * townObj = dynamic_cast(TavernObj)) - CCS->videoh->open(townObj->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), townObj->town->clientInfo.tavernVideo, false); else - CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); + videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false); addInvite(); } @@ -572,11 +572,6 @@ void CTavernWindow::close() CStatusbarWindow::close(); } -CTavernWindow::~CTavernWindow() -{ - CCS->videoh->close(); -} - void CTavernWindow::show(Canvas & to) { CWindowObject::show(to); @@ -600,8 +595,6 @@ void CTavernWindow::show(Canvas & to) to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2); } - - CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false); } void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition) diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 5edd609a2..1f2b61398 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -39,6 +39,7 @@ class CHeroArea; class CAnimImage; class CFilledTexture; class IImage; +class VideoWidget; enum class EUserEvent; @@ -261,6 +262,7 @@ public: std::shared_ptr cost; std::shared_ptr heroesForHire; std::shared_ptr heroDescription; + std::shared_ptr videoPlayer; std::shared_ptr rumor; @@ -272,7 +274,6 @@ public: void addInvite(); CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); - ~CTavernWindow(); void close() override; void recruitb(); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 2058b664c..46f409e1c 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -11,9 +11,10 @@ #include "GeneralOptionsTab.h" #include "CGameInfo.h" -#include "CMusicHandler.h" #include "CPlayerInterface.h" #include "CServerHandler.h" +#include "media/IMusicPlayer.h" +#include "media/ISoundPlayer.h" #include "render/IScreenHandler.h" #include "windows/GUIClasses.h" diff --git a/lib/Languages.h b/lib/Languages.h index f762d6e20..1ef49e559 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -68,6 +68,9 @@ struct Options /// primary IETF language tag std::string tagIETF; + /// ISO 639-2 (B) language code + std::string tagISO2; + /// DateTime format std::string dateTimeFormat; @@ -82,27 +85,27 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3, true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here - { "finnish", "Finnish", "Suomi", "CP1252", "fi", "%d.%m.%Y %T", EPluralForms::EN_2, true }, - { "french", "French", "Français", "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2, true }, - { "german", "German", "Deutsch", "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2, true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2, true }, - { "italian", "Italian", "Italiano", "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2, true }, - { "korean", "Korean", "한국어", "CP949", "ko", "%F %T", EPluralForms::VI_1, true }, - { "polish", "Polish", "Polski", "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3, true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3, true }, - { "spanish", "Spanish", "Español", "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2, true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", "%F %T", EPluralForms::EN_2, true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "%d.%m.%Y %T", EPluralForms::EN_2, true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "%d.%m.%Y %T", EPluralForms::UK_3, true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %T", EPluralForms::CZ_3, true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "eng", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %T", EPluralForms::FR_2, true }, + { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %T", EPluralForms::EN_2, true }, + { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %T", EPluralForms::EN_2, true }, + { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%F %T", EPluralForms::VI_1, true }, + { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %T", EPluralForms::PL_3, true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %T", EPluralForms::UK_3, true }, + { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %T", EPluralForms::EN_2, true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%F %T", EPluralForms::EN_2, true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %T", EPluralForms::UK_3, true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding - { "other_cp1250", "Other (East European)", "", "CP1250", "", "", EPluralForms::NONE, false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", EPluralForms::NONE, false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", "", EPluralForms::NONE, false } + { "other_cp1250", "Other (East European)", "", "CP1250", "", "", "", EPluralForms::NONE, false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", "", EPluralForms::NONE, false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", "", "", EPluralForms::NONE, false } } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index 15efb5cb7..4a94e34de 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -113,11 +113,8 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension) {".MP3", EResType::SOUND}, {".OGG", EResType::SOUND}, {".FLAC", EResType::SOUND}, - {".SMK", EResType::VIDEO}, + {".SMK", EResType::VIDEO_LOW_QUALITY}, {".BIK", EResType::VIDEO}, - {".MJPG", EResType::VIDEO}, - {".MPG", EResType::VIDEO}, - {".AVI", EResType::VIDEO}, {".WEBM", EResType::VIDEO}, {".ZIP", EResType::ARCHIVE_ZIP}, {".LOD", EResType::ARCHIVE_LOD}, @@ -157,6 +154,7 @@ std::string EResTypeHelper::getEResTypeAsString(EResType type) MAP_ENUM(TTF_FONT) MAP_ENUM(IMAGE) MAP_ENUM(VIDEO) + MAP_ENUM(VIDEO_LOW_QUALITY) MAP_ENUM(SOUND) MAP_ENUM(ARCHIVE_ZIP) MAP_ENUM(ARCHIVE_LOD) diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 250e76bfc..fde6bec9f 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -46,6 +46,7 @@ enum class EResType TTF_FONT, IMAGE, VIDEO, + VIDEO_LOW_QUALITY, SOUND, ARCHIVE_VID, ARCHIVE_ZIP, diff --git a/lib/json/JsonValidator.cpp b/lib/json/JsonValidator.cpp index fd4b67d49..e2f6b1e06 100644 --- a/lib/json/JsonValidator.cpp +++ b/lib/json/JsonValidator.cpp @@ -493,6 +493,7 @@ static std::string imageFile(const JsonNode & node) static std::string videoFile(const JsonNode & node) { TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO); + TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO_LOW_QUALITY); return "Video file \"" + node.String() + "\" was not found"; } #undef TEST_FILE