1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00

Merge pull request #3998 from IvanSavenko/refactor_sdl_media

[1.6] Reorganize SDL sound/music/video handling
This commit is contained in:
Ivan Savenko
2024-05-31 12:02:55 +03:00
committed by GitHub
65 changed files with 2372 additions and 2129 deletions

View File

@@ -36,21 +36,21 @@ class CMap;
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END
class CMapHandler; class CMapHandler;
class CSoundHandler; class ISoundPlayer;
class CMusicHandler; class IMusicPlayer;
class CursorHandler; class CursorHandler;
class IMainVideoPlayer; class IVideoPlayer;
class CServerHandler; class CServerHandler;
//a class for non-mechanical client GUI classes //a class for non-mechanical client GUI classes
class CClientState class CClientState
{ {
public: public:
CSoundHandler * soundh; ISoundPlayer * soundh;
CMusicHandler * musich; IMusicPlayer * musich;
CConsoleHandler * consoleh; CConsoleHandler * consoleh;
CursorHandler * curh; CursorHandler * curh;
IMainVideoPlayer * videoh; IVideoPlayer * videoh;
}; };
extern CClientState * CCS; extern CClientState * CCS;

View File

@@ -14,11 +14,13 @@
#include "CGameInfo.h" #include "CGameInfo.h"
#include "mainmenu/CMainMenu.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 "gui/CursorHandler.h"
#include "eventsSDL/InputHandler.h" #include "eventsSDL/InputHandler.h"
#include "CPlayerInterface.h" #include "CPlayerInterface.h"
#include "CVideoHandler.h"
#include "CMusicHandler.h"
#include "gui/CGuiHandler.h" #include "gui/CGuiHandler.h"
#include "gui/WindowHandler.h" #include "gui/WindowHandler.h"
#include "CServerHandler.h" #include "CServerHandler.h"
@@ -292,10 +294,8 @@ int main(int argc, char * argv[])
{ {
//initializing audio //initializing audio
CCS->soundh = new CSoundHandler(); CCS->soundh = new CSoundHandler();
CCS->soundh->init();
CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float()); CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float());
CCS->musich = new CMusicHandler(); CCS->musich = new CMusicHandler();
CCS->musich->init();
CCS->musich->setVolume((ui32)settings["general"]["music"].Float()); CCS->musich->setVolume((ui32)settings["general"]["music"].Float());
logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); 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) //plays intro, ends when intro is over or button has been pressed (handles events)
void playIntro() void playIntro()
{ {
auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); if(!CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK")))
int sound = CCS->soundh->playSound(audioData); return;
if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO))
{ if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK")))
audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); return;
sound = CCS->soundh->playSound(audioData);
if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO)) CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK"));
{
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);
} }
static void mainLoop() static void mainLoop()
@@ -457,9 +450,6 @@ static void mainLoop()
// cleanup, mostly to remove false leaks from analyzer // cleanup, mostly to remove false leaks from analyzer
if(CCS) if(CCS)
{ {
CCS->musich->release();
CCS->soundh->release();
delete CCS->consoleh; delete CCS->consoleh;
delete CCS->curh; delete CCS->curh;
delete CCS->videoh; delete CCS->videoh;

View File

@@ -75,6 +75,11 @@ set(client_SRCS
mapView/MapViewModel.cpp mapView/MapViewModel.cpp
mapView/mapHandler.cpp mapView/mapHandler.cpp
media/CAudioBase.cpp
media/CMusicHandler.cpp
media/CSoundHandler.cpp
media/CVideoHandler.cpp
render/CAnimation.cpp render/CAnimation.cpp
render/CBitmapHandler.cpp render/CBitmapHandler.cpp
render/CDefFile.cpp render/CDefFile.cpp
@@ -126,6 +131,7 @@ set(client_SRCS
widgets/CArtifactsOfHeroMarket.cpp widgets/CArtifactsOfHeroMarket.cpp
widgets/CArtifactsOfHeroBackpack.cpp widgets/CArtifactsOfHeroBackpack.cpp
widgets/RadialMenu.cpp widgets/RadialMenu.cpp
widgets/VideoWidget.cpp
widgets/markets/CAltarArtifacts.cpp widgets/markets/CAltarArtifacts.cpp
widgets/markets/CAltarCreatures.cpp widgets/markets/CAltarCreatures.cpp
widgets/markets/CArtifactsBuying.cpp widgets/markets/CArtifactsBuying.cpp
@@ -163,11 +169,9 @@ set(client_SRCS
CGameInfo.cpp CGameInfo.cpp
CMT.cpp CMT.cpp
CMusicHandler.cpp
CPlayerInterface.cpp CPlayerInterface.cpp
PlayerLocalState.cpp PlayerLocalState.cpp
CServerHandler.cpp CServerHandler.cpp
CVideoHandler.cpp
Client.cpp Client.cpp
ClientCommandManager.cpp ClientCommandManager.cpp
GameChatHandler.cpp GameChatHandler.cpp
@@ -260,6 +264,15 @@ set(client_HEADERS
mapView/MapViewModel.h mapView/MapViewModel.h
mapView/mapHandler.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/CAnimation.h
render/CBitmapHandler.h render/CBitmapHandler.h
render/CDefFile.h render/CDefFile.h
@@ -320,6 +333,7 @@ set(client_HEADERS
widgets/CArtifactsOfHeroMarket.h widgets/CArtifactsOfHeroMarket.h
widgets/CArtifactsOfHeroBackpack.h widgets/CArtifactsOfHeroBackpack.h
widgets/RadialMenu.h widgets/RadialMenu.h
widgets/VideoWidget.h
widgets/markets/CAltarArtifacts.h widgets/markets/CAltarArtifacts.h
widgets/markets/CAltarCreatures.h widgets/markets/CAltarCreatures.h
widgets/markets/CArtifactsBuying.h widgets/markets/CArtifactsBuying.h
@@ -357,11 +371,9 @@ set(client_HEADERS
CGameInfo.h CGameInfo.h
CMT.h CMT.h
CMusicHandler.h
CPlayerInterface.h CPlayerInterface.h
PlayerLocalState.h PlayerLocalState.h
CServerHandler.h CServerHandler.h
CVideoHandler.h
Client.h Client.h
ClientCommandManager.h ClientCommandManager.h
ClientNetPackVisitors.h ClientNetPackVisitors.h

View File

@@ -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 <SDL_mixer.h>
#include <SDL_timer.h>
#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<std::unique_ptr<ui8 []>, si64> & data, bool cache)
{
try
{
std::vector<ui8> startBytes = std::vector<ui8>(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<int>(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<std::unique_ptr<ui8 []>, 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<soundBase::soundID> &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<void()> 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<void()> & function)
{
boost::mutex::scoped_lock lockGuard(mutexCallbacks);
assert(callbacks.count(channel) == 0);
callbacks[channel].push_back(function);
}
int CSoundHandler::ambientGetRange() const
{
return static_cast<int>(ambientConfig["range"].Integer());
}
void CSoundHandler::ambientUpdateChannels(std::map<AudioPath, int> soundsArg)
{
boost::mutex::scoped_lock guard(mutex);
std::vector<AudioPath> 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<MusicEntry> 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<MusicEntry>(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;
}

View File

@@ -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<Mix_Chunk *, std::unique_ptr<ui8[]>>;
std::map<AudioPath, CachedChunk> soundChunks;
std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache);
Mix_Chunk *GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache);
/// have entry for every currently active channel
/// vector will be empty if callback was not set
std::map<int, std::vector<std::function<void()>> > 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<AudioPath, int> ambientChannels;
std::map<int, int> channelVolumes;
void initCallback(int channel, const std::function<void()> & 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<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);
int playSoundFromSet(std::vector<soundBase::soundID> &sound_vec);
void stopSound(int handler);
void setCallback(int channel, std::function<void()> function);
void resetCallback(int channel);
void soundFinishedCallback(int channel);
int ambientGetRange() const;
void ambientUpdateChannels(std::map<AudioPath, int> currentSounds);
void ambientStopAllChannels();
// Sets
std::vector<soundBase::soundID> 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<MusicEntry> current;
std::unique_ptr<MusicEntry> next;
void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart);
void queueNext(std::unique_ptr<MusicEntry> queued);
void musicFinishedCallback();
/// map <set name> -> <list of URI's to tracks belonging to the said set>
std::map<std::string, std::vector<AudioPath>> musicsSet;
/// stored position, in seconds at which music player should resume playing this track
std::map<AudioPath, float> 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;
};

View File

@@ -14,7 +14,6 @@
#include "CGameInfo.h" #include "CGameInfo.h"
#include "CMT.h" #include "CMT.h"
#include "CMusicHandler.h"
#include "CServerHandler.h" #include "CServerHandler.h"
#include "HeroMovementController.h" #include "HeroMovementController.h"
#include "PlayerLocalState.h" #include "PlayerLocalState.h"
@@ -41,6 +40,9 @@
#include "mapView/mapHandler.h" #include "mapView/mapHandler.h"
#include "media/IMusicPlayer.h"
#include "media/ISoundPlayer.h"
#include "render/CAnimation.h" #include "render/CAnimation.h"
#include "render/IImage.h" #include "render/IImage.h"
#include "render/IRenderHandler.h" #include "render/IRenderHandler.h"

View File

@@ -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 <SDL_render.h>
#ifndef DISABLE_VIDEO
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#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<CVideoPlayer *>(opaque);
int bytes = static_cast<int>(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<CVideoPlayer *>(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<CVideoPlayer *>(opaque);
int bytes = static_cast<int>(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<CVideoPlayer *>(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; i<format->nb_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<void()> 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<std::unique_ptr<ui8 []>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
{
std::pair<std::unique_ptr<ui8 []>, 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<ui8> 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<ui8*>(&wav);
dat = std::make_pair(std::make_unique<ui8[]>(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<int>(1000 * (targetFrameTimeSeconds)));
auto timePointAfterPresent = boost::chrono::steady_clock::now();
auto timeSpentBusy = boost::chrono::duration_cast<boost::chrono::milliseconds>(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

View File

@@ -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<void()> restart = nullptr){}
virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
{
return false;
}
virtual std::pair<std::unique_ptr<ui8 []>, 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<void()> 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<std::unique_ptr<ui8 []>, 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<CInputStream> data;
std::unique_ptr<CInputStream> dataAudio;
};
#endif

View File

@@ -11,7 +11,6 @@
#include "HeroMovementController.h" #include "HeroMovementController.h"
#include "CGameInfo.h" #include "CGameInfo.h"
#include "CMusicHandler.h"
#include "CPlayerInterface.h" #include "CPlayerInterface.h"
#include "PlayerLocalState.h" #include "PlayerLocalState.h"
#include "adventureMap/AdventureMapInterface.h" #include "adventureMap/AdventureMapInterface.h"
@@ -19,10 +18,12 @@
#include "gui/CGuiHandler.h" #include "gui/CGuiHandler.h"
#include "gui/CursorHandler.h" #include "gui/CursorHandler.h"
#include "mapView/mapHandler.h" #include "mapView/mapHandler.h"
#include "media/ISoundPlayer.h"
#include "../CCallback.h" #include "../CCallback.h"
#include "../lib/CondSh.h" #include "../lib/CondSh.h"
#include "../lib/CConfigHandler.h"
#include "../lib/pathfinder/CGPathNode.h" #include "../lib/pathfinder/CGPathNode.h"
#include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/CGHeroInstance.h"
#include "../lib/networkPacks/PacksForClient.h" #include "../lib/networkPacks/PacksForClient.h"

View File

@@ -12,7 +12,6 @@
#include "CInGameConsole.h" #include "CInGameConsole.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../GameChatHandler.h" #include "../GameChatHandler.h"
@@ -21,6 +20,7 @@
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/TextAlignment.h" #include "../gui/TextAlignment.h"
#include "../media/ISoundPlayer.h"
#include "../render/Colors.h" #include "../render/Colors.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IScreenHandler.h" #include "../render/IScreenHandler.h"

View File

@@ -20,11 +20,11 @@
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../PlayerLocalState.h" #include "../PlayerLocalState.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/IScreenHandler.h" #include "../render/IScreenHandler.h"
#include "../../CCallback.h" #include "../../CCallback.h"

View File

@@ -12,9 +12,10 @@
#include "../CCallback.h" #include "../CCallback.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../mapView/mapHandler.h" #include "../mapView/mapHandler.h"
#include "../media/IMusicPlayer.h"
#include "../media/ISoundPlayer.h"
#include "../../lib/TerrainHandler.h" #include "../../lib/TerrainHandler.h"
#include "../../lib/mapObjects/CArmedInstance.h" #include "../../lib/mapObjects/CArmedInstance.h"

View File

@@ -11,11 +11,11 @@
#include "TurnTimerWidget.h" #include "TurnTimerWidget.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../battle/BattleInterface.h" #include "../battle/BattleInterface.h"
#include "../battle/BattleStacksController.h" #include "../battle/BattleStacksController.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/Graphics.h" #include "../render/Graphics.h"
#include "../widgets/Images.h" #include "../widgets/Images.h"
#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/GraphicalPrimitiveCanvas.h"

View File

@@ -20,10 +20,10 @@
#include "CreatureAnimation.h" #include "CreatureAnimation.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/IRenderHandler.h" #include "../render/IRenderHandler.h"
#include "../../CCallback.h" #include "../../CCallback.h"

View File

@@ -18,9 +18,9 @@
#include "BattleStacksController.h" #include "BattleStacksController.h"
#include "BattleRenderer.h" #include "BattleRenderer.h"
#include "../CMusicHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../media/ISoundPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/CAnimation.h" #include "../render/CAnimation.h"
#include "../render/Graphics.h" #include "../render/Graphics.h"

View File

@@ -24,11 +24,12 @@
#include "BattleRenderer.h" #include "BattleRenderer.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/IMusicPlayer.h"
#include "../media/ISoundPlayer.h"
#include "../windows/CTutorialWindow.h" #include "../windows/CTutorialWindow.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../adventureMap/AdventureMapInterface.h" #include "../adventureMap/AdventureMapInterface.h"
@@ -113,7 +114,14 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
onIntroSoundPlayed(); onIntroSoundPlayed();
}; };
int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); std::vector<soundBase::soundID> 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) if (battleIntroSoundChannel != -1)
{ {
CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);

View File

@@ -19,14 +19,13 @@
#include "BattleWindow.h" #include "BattleWindow.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CVideoHandler.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/MouseButton.h" #include "../gui/MouseButton.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/IMusicPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IImage.h" #include "../render/IImage.h"
#include "../render/IFont.h" #include "../render/IFont.h"
@@ -35,6 +34,7 @@
#include "../widgets/Images.h" #include "../widgets/Images.h"
#include "../widgets/Slider.h" #include "../widgets/Slider.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../windows/CMessage.h" #include "../windows/CMessage.h"
#include "../windows/CCreatureWindow.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) BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
: owner(_owner), currentVideo(BattleResultVideo::NONE) : owner(_owner)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -705,68 +705,98 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
} }
} }
} }
auto resources = getResources(br);
description = std::make_shared<CTextBox>(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
videoPlayer = std::make_shared<VideoWidget>(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false);
CCS->musich->playMusic(resources.musicName, false, true);
}
BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
{
//printing result description //printing result description
bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); 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; if(isSiege && weAreDefender)
currentVideo = BattleResultVideo::WIN; {
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) switch(br.result)
{ {
case EBattleResult::NORMAL: case EBattleResult::NORMAL:
if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) resources.resultText.appendTextID("core.genrltxt.304");
currentVideo = BattleResultVideo::WIN_SIEGE;
break; break;
case EBattleResult::ESCAPE: case EBattleResult::ESCAPE:
text = 303; resources.resultText.appendTextID("core.genrltxt.303");
break; break;
case EBattleResult::SURRENDER: case EBattleResult::SURRENDER:
text = 302; resources.resultText.appendTextID("core.genrltxt.302");
break; break;
default: default:
logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result)); throw std::runtime_error("Invalid battle result!");
break;
} }
playVideo();
std::string str = CGI->generaltexth->allTexts[text];
const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
if (ourHero) if (ourHero)
{ {
str += CGI->generaltexth->allTexts[305]; resources.resultText.appendTextID("core.genrltxt.305");
boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated()); resources.resultText.replaceTextID(ourHero->getNameTranslated());
boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1])); resources.resultText.replaceNumber(br.exp[weAreAttacker ? 0 : 1]);
} }
description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
} }
else // we lose else // we lose
{ {
int text = 311;
currentVideo = BattleResultVideo::DEFEAT;
switch(br.result) switch(br.result)
{ {
case EBattleResult::NORMAL: case EBattleResult::NORMAL:
if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) resources.resultText.appendTextID("core.genrltxt.311");
currentVideo = BattleResultVideo::DEFEAT_SIEGE; resources.musicName = AudioPath::builtin("Music/LoseCombat");
resources.prologueVideo = VideoPath::builtin("LBSTART.BIK");
resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK");
break; break;
case EBattleResult::ESCAPE: case EBattleResult::ESCAPE:
currentVideo = BattleResultVideo::RETREAT; resources.resultText.appendTextID("core.genrltxt.310");
text = 310; resources.musicName = AudioPath::builtin("Music/Retreat Battle");
resources.prologueVideo = VideoPath::builtin("RTSTART.BIK");
resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK");
break; break;
case EBattleResult::SURRENDER: case EBattleResult::SURRENDER:
currentVideo = BattleResultVideo::SURRENDER; resources.resultText.appendTextID("core.genrltxt.309");
text = 309; resources.musicName = AudioPath::builtin("Music/Surrender Battle");
resources.prologueVideo = VideoPath();
resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK");
break; break;
default: default:
logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result)); throw std::runtime_error("Invalid battle result!");
break;
} }
playVideo();
labels.push_back(std::make_shared<CLabel>(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() void BattleResultWindow::activate()
@@ -775,81 +805,6 @@ void BattleResultWindow::activate()
CIntObject::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) void BattleResultWindow::buttonPressed(int button)
{ {
if (resultCallback) 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, //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. //so we can be sure that there is no dialogs left on GUI stack.
intTmp.showingDialog->setn(false); intTmp.showingDialog->setn(false);
CCS->videoh->close();
} }
void BattleResultWindow::bExitf() void BattleResultWindow::bExitf()

View File

@@ -14,6 +14,7 @@
#include "../../lib/FunctionList.h" #include "../../lib/FunctionList.h"
#include "../../lib/battle/BattleHex.h" #include "../../lib/battle/BattleHex.h"
#include "../windows/CWindowObject.h" #include "../windows/CWindowObject.h"
#include "../../lib/MetaString.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@@ -42,6 +43,7 @@ class CAnimImage;
class TransparentFilledRectangle; class TransparentFilledRectangle;
class CPlayerInterface; class CPlayerInterface;
class BattleRenderer; class BattleRenderer;
class VideoWidget;
/// Class which shows the console at the bottom of the battle screen and manages the text of the console /// 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 class BattleConsole : public CIntObject, public IStatusBar
@@ -185,6 +187,14 @@ public:
HeroInfoWindow(const InfoAboutHero & hero, Point * position); 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 which is responsible for showing the battle result window
class BattleResultWindow : public WindowBase class BattleResultWindow : public WindowBase
{ {
@@ -195,25 +205,10 @@ private:
std::shared_ptr<CButton> repeat; std::shared_ptr<CButton> repeat;
std::vector<std::shared_ptr<CAnimImage>> icons; std::vector<std::shared_ptr<CAnimImage>> icons;
std::shared_ptr<CTextBox> description; std::shared_ptr<CTextBox> description;
std::shared_ptr<VideoWidget> videoPlayer;
CPlayerInterface & owner; CPlayerInterface & owner;
enum BattleResultVideo BattleResultResources getResources(const BattleResult & br);
{
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);
void buttonPressed(int button); //internal function for button callbacks void buttonPressed(int button); //internal function for button callbacks
public: public:
@@ -224,7 +219,6 @@ public:
std::function<void(int result)> resultCallback; //callback receiving which button was pressed std::function<void(int result)> resultCallback; //callback receiving which button was pressed
void activate() override; void activate() override;
void show(Canvas & to) override;
}; };
/// Shows the stack queue /// Shows the stack queue

View File

@@ -17,10 +17,10 @@
#include "BattleRenderer.h" #include "BattleRenderer.h"
#include "CreatureAnimation.h" #include "CreatureAnimation.h"
#include "../CMusicHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IRenderHandler.h" #include "../render/IRenderHandler.h"

View File

@@ -17,10 +17,10 @@
#include "BattleFieldController.h" #include "BattleFieldController.h"
#include "BattleRenderer.h" #include "BattleRenderer.h"
#include "../CMusicHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IImage.h" #include "../render/IImage.h"
#include "../render/IRenderHandler.h" #include "../render/IRenderHandler.h"

View File

@@ -23,10 +23,10 @@
#include "CreatureAnimation.h" #include "CreatureAnimation.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CMusicHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/Colors.h" #include "../render/Colors.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IRenderHandler.h" #include "../render/IRenderHandler.h"

View File

@@ -18,7 +18,6 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CMusicHandler.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"

View File

@@ -22,10 +22,11 @@
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../gui/EventDispatcher.h" #include "../gui/EventDispatcher.h"
#include "../gui/MouseButton.h" #include "../gui/MouseButton.h"
#include "../media/IMusicPlayer.h"
#include "../media/ISoundPlayer.h"
#include "../CMT.h" #include "../CMT.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"

View File

@@ -17,11 +17,11 @@
#include "GlobalLobbyWindow.h" #include "GlobalLobbyWindow.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../mainmenu/CMainMenu.h" #include "../mainmenu/CMainMenu.h"
#include "../media/ISoundPlayer.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"

View File

@@ -16,10 +16,10 @@
#include "GlobalLobbyRoomWindow.h" #include "GlobalLobbyRoomWindow.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/ISoundPlayer.h"
#include "../render/Colors.h" #include "../render/Colors.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/CTextInput.h" #include "../widgets/CTextInput.h"

View File

@@ -18,12 +18,11 @@
#include "ExtraOptionsTab.h" #include "ExtraOptionsTab.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CVideoHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../mainmenu/CMainMenu.h" #include "../mainmenu/CMainMenu.h"
#include "../mainmenu/CPrologEpilogVideo.h" #include "../mainmenu/CPrologEpilogVideo.h"
#include "../media/IMusicPlayer.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
@@ -41,9 +40,9 @@
#include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/Filesystem.h"
#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CGeneralTextHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CBuildingHandler.h" #include "../../lib/CBuildingHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CSkillHandler.h" #include "../../lib/CSkillHandler.h"
#include "../../lib/CTownHandler.h" #include "../../lib/CTownHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"

View File

@@ -20,14 +20,13 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CMusicHandler.h"
#include "../CVideoHandler.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../globalLobby/GlobalLobbyClient.h" #include "../globalLobby/GlobalLobbyClient.h"
#include "../mainmenu/CMainMenu.h" #include "../mainmenu/CMainMenu.h"
#include "../media/ISoundPlayer.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/CTextInput.h" #include "../widgets/CTextInput.h"

View File

@@ -14,12 +14,12 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../CMusicHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../render/Graphics.h" #include "../render/Graphics.h"
#include "../render/IFont.h" #include "../render/IFont.h"
#include "../media/ISoundPlayer.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/ComboBox.h" #include "../widgets/ComboBox.h"
#include "../widgets/CTextInput.h" #include "../widgets/CTextInput.h"
@@ -38,6 +38,7 @@
#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/networkPacks/PacksForLobby.h"
#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CGeneralTextHandler.h"
#include "../../lib/CArtHandler.h" #include "../../lib/CArtHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CTownHandler.h" #include "../../lib/CTownHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
#include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapInfo.h"

View File

@@ -14,18 +14,18 @@
#include "CMainMenu.h" #include "CMainMenu.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CVideoHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../media/IMusicPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
#include "../widgets/ObjectLists.h" #include "../widgets/ObjectLists.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../windows/GUIClasses.h" #include "../windows/GUIClasses.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../windows/CWindowObject.h" #include "../windows/CWindowObject.h"
@@ -36,7 +36,7 @@
#include "../../lib/CArtHandler.h" #include "../../lib/CArtHandler.h"
#include "../../lib/CBuildingHandler.h" #include "../../lib/CBuildingHandler.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CSkillHandler.h" #include "../../lib/CSkillHandler.h"
#include "../../lib/CTownHandler.h" #include "../../lib/CTownHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
@@ -100,7 +100,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
pos.h = 116; pos.h = 116;
campFile = config["file"].String(); campFile = config["file"].String();
video = VideoPath::fromJson(config["video"]); videoPath = VideoPath::fromJson(config["video"]);
status = CCampaignScreen::ENABLED; status = CCampaignScreen::ENABLED;
@@ -127,7 +127,6 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
{ {
addUsedEvents(LCLICK | HOVER); addUsedEvents(LCLICK | HOVER);
graphicsImage = std::make_shared<CPicture>(ImagePath::fromJson(config["image"])); graphicsImage = std::make_shared<CPicture>(ImagePath::fromJson(config["image"]));
hoverLabel = std::make_shared<CLabel>(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, ""); hoverLabel = std::make_shared<CLabel>(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, "");
parent->addChild(hoverLabel.get()); parent->addChild(hoverLabel.get());
} }
@@ -136,30 +135,19 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
graphicsCompleted = std::make_shared<CPicture>(ImagePath::builtin("CAMPCHK")); graphicsCompleted = std::make_shared<CPicture>(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) void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition)
{ {
CCS->videoh->close();
CMainMenu::openCampaignLobby(campFile, campaignSet); CMainMenu::openCampaignLobby(campFile, campaignSet);
} }
void CCampaignScreen::CCampaignButton::hover(bool on) void CCampaignScreen::CCampaignButton::hover(bool on)
{ {
if (on) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
CCS->videoh->open(video);
if (on && !videoPath.empty())
videoPlayer = std::make_shared<VideoWidget>(Point(), videoPath, false);
else else
CCS->videoh->close(); videoPlayer.reset();
if(hoverLabel) if(hoverLabel)
{ {

View File

@@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_END
class CLabel; class CLabel;
class CPicture; class CPicture;
class CButton; class CButton;
class VideoWidget;
class CCampaignScreen : public CWindowObject class CCampaignScreen : public CWindowObject
{ {
@@ -34,10 +35,11 @@ private:
std::shared_ptr<CLabel> hoverLabel; std::shared_ptr<CLabel> hoverLabel;
std::shared_ptr<CPicture> graphicsImage; std::shared_ptr<CPicture> graphicsImage;
std::shared_ptr<CPicture> graphicsCompleted; std::shared_ptr<CPicture> graphicsCompleted;
std::shared_ptr<VideoWidget> videoPlayer;
CampaignStatus status; CampaignStatus status;
VideoPath videoPath;
std::string campFile; // the filename/resourcename of the campaign std::string campFile; // the filename/resourcename of the campaign
VideoPath video; // the resource name of the video
std::string hoverText; std::string hoverText;
std::string campaignSet; std::string campaignSet;
@@ -47,7 +49,6 @@ private:
public: public:
CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet); CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet);
void show(Canvas & to) override;
}; };
std::string campaignSet; std::string campaignSet;

View File

@@ -14,17 +14,18 @@
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../media/IMusicPlayer.h"
#include "../media/ISoundPlayer.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/CTextInput.h" #include "../widgets/CTextInput.h"
#include "../widgets/Images.h" #include "../widgets/Images.h"
#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/VideoWidget.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CVideoHandler.h"
#include "../CMusicHandler.h"
#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CGeneralTextHandler.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CCreatureHandler.h" #include "../../lib/CCreatureHandler.h"
@@ -216,7 +217,7 @@ void CHighScoreScreen::buttonExitClick()
} }
CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc)
: CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1) : CWindowObject(BORDERED), won(won), calc(calc)
{ {
addUsedEvents(LCLICK | KEYBOARD); addUsedEvents(LCLICK | KEYBOARD);
@@ -228,6 +229,9 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
if(won) if(won)
{ {
videoPlayer = std::make_shared<VideoWidget>(Point(0, 0), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK"), true);
int border = 100; int border = 100;
int textareaW = ((pos.w - 2 * border) / 4); int textareaW = ((pos.w - 2 * border) / 4);
std::vector<std::string> t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank std::vector<std::string> 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); CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true);
} }
else else
{
videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();});
CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true);
}
video = won ? "HSANIM.SMK" : "LOSEGAME.SMK";
} }
int CHighScoreInputScreen::addEntry(std::string text) { int CHighScoreInputScreen::addEntry(std::string text) {
@@ -289,43 +294,7 @@ int CHighScoreInputScreen::addEntry(std::string text) {
void CHighScoreInputScreen::show(Canvas & to) void CHighScoreInputScreen::show(Canvas & to)
{ {
CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, CWindowObject::showAll(to);
[&]()
{
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);
} }
void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)

View File

@@ -15,6 +15,7 @@ class CLabel;
class CMultiLineLabel; class CMultiLineLabel;
class CAnimImage; class CAnimImage;
class CTextInput; class CTextInput;
class VideoWidgetBase;
class TransparentFilledRectangle; class TransparentFilledRectangle;
@@ -93,9 +94,8 @@ class CHighScoreInputScreen : public CWindowObject
std::vector<std::shared_ptr<CMultiLineLabel>> texts; std::vector<std::shared_ptr<CMultiLineLabel>> texts;
std::shared_ptr<CHighScoreInput> input; std::shared_ptr<CHighScoreInput> input;
std::shared_ptr<TransparentFilledRectangle> background; std::shared_ptr<TransparentFilledRectangle> background;
std::shared_ptr<VideoWidgetBase> videoPlayer;
std::string video;
int videoSoundHandle;
bool won; bool won;
HighScoreCalculation calc; HighScoreCalculation calc;
public: public:
@@ -103,9 +103,7 @@ public:
int addEntry(std::string text); int addEntry(std::string text);
void show(Canvas & to) override;
void activate() override;
void deactivate() override;
void clickPressed(const Point & cursorPosition) override; void clickPressed(const Point & cursorPosition) override;
void keyPressed(EShortcut key) override; void keyPressed(EShortcut key) override;
}; void show(Canvas & to) override;
};

View File

@@ -17,6 +17,7 @@
#include "../lobby/CBonusSelection.h" #include "../lobby/CBonusSelection.h"
#include "../lobby/CSelectionBase.h" #include "../lobby/CSelectionBase.h"
#include "../lobby/CLobbyScreen.h" #include "../lobby/CLobbyScreen.h"
#include "../media/IMusicPlayer.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
#include "../windows/GUIClasses.h" #include "../windows/GUIClasses.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
@@ -33,12 +34,11 @@
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
#include "../widgets/ObjectLists.h" #include "../widgets/ObjectLists.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CVideoHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../Client.h" #include "../Client.h"
#include "../CMT.h" #include "../CMT.h"
@@ -92,8 +92,14 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
menuNameToEntry.push_back("credits"); menuNameToEntry.push_back("credits");
tabs = std::make_shared<CTabbedInt>(std::bind(&CMenuScreen::createTab, this, _1)); tabs = std::make_shared<CTabbedInt>(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<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
}
else
tabs->setRedrawParent(true); tabs->setRedrawParent(true);
} }
std::shared_ptr<CIntObject> CMenuScreen::createTab(size_t index) std::shared_ptr<CIntObject> CMenuScreen::createTab(size_t index)
@@ -106,32 +112,16 @@ std::shared_ptr<CIntObject> CMenuScreen::createTab(size_t index)
void CMenuScreen::show(Canvas & to) void CMenuScreen::show(Canvas & to)
{ {
if(!config["video"].isNull()) // TODO: avoid excessive redraws
{ CIntObject::showAll(to);
// 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);
} }
void CMenuScreen::activate() void CMenuScreen::activate()
{ {
CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true);
if(!config["video"].isNull())
CCS->videoh->open(VideoPath::fromJson(config["video"]["name"]));
CIntObject::activate(); CIntObject::activate();
} }
void CMenuScreen::deactivate()
{
if(!config["video"].isNull())
CCS->videoh->close();
CIntObject::deactivate();
}
void CMenuScreen::switchToTab(size_t index) void CMenuScreen::switchToTab(size_t index)
{ {
tabs->setActive(index); tabs->setActive(index);

View File

@@ -28,7 +28,7 @@ class CAnimation;
class CButton; class CButton;
class CFilledTexture; class CFilledTexture;
class CLabel; class CLabel;
class VideoWidget;
// TODO: Find new location for these enums // TODO: Find new location for these enums
enum class ESelectionScreen : ui8 { enum class ESelectionScreen : ui8 {
@@ -48,6 +48,7 @@ class CMenuScreen : public CWindowObject
std::shared_ptr<CTabbedInt> tabs; std::shared_ptr<CTabbedInt> tabs;
std::shared_ptr<CPicture> background; std::shared_ptr<CPicture> background;
std::shared_ptr<VideoWidget> videoPlayer;
std::vector<std::shared_ptr<CPicture>> images; std::vector<std::shared_ptr<CPicture>> images;
std::shared_ptr<CIntObject> createTab(size_t index); std::shared_ptr<CIntObject> createTab(size_t index);
@@ -57,9 +58,8 @@ public:
CMenuScreen(const JsonNode & configNode); CMenuScreen(const JsonNode & configNode);
void show(Canvas & to) override;
void activate() override; void activate() override;
void deactivate() override; void show(Canvas & to) override;
void switchToTab(size_t index); void switchToTab(size_t index);
void switchToTab(std::string name); void switchToTab(std::string name);

View File

@@ -12,15 +12,15 @@
#include "CPrologEpilogVideo.h" #include "CPrologEpilogVideo.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h" #include "../media/IMusicPlayer.h"
#include "../CVideoHandler.h" #include "../media/ISoundPlayer.h"
#include "../gui/WindowHandler.h" //#include "../gui/WindowHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/FramerateManager.h" //#include "../gui/FramerateManager.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback) CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback)
: CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0) : 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)); pos = center(Rect(0, 0, 800, 600));
updateShadow(); updateShadow();
auto audioData = CCS->videoh->getAudio(spe.prologVideo); //TODO: remove hardcoded paths. Some of campaigns video actually consist from 2 parts
videoSoundHandle = CCS->soundh->playSound(audioData, -1); // however, currently our campaigns format expects only a single video file
CCS->videoh->open(spe.prologVideo); static const std::map<VideoPath, VideoPath> 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<VideoWidget>(Point(0, 0), spe.prologVideo, pairedVideoFiles.at(spe.prologVideo), true);
else
videoPlayer = std::make_shared<VideoWidget>(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); CCS->musich->playMusic(spe.prologMusic, true, true);
voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice); voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice);
voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice);
@@ -65,9 +79,8 @@ void CPrologEpilogVideo::tick(uint32_t msPassed)
void CPrologEpilogVideo::show(Canvas & to) void CPrologEpilogVideo::show(Canvas & to)
{ {
to.drawColor(pos, Colors::BLACK); 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 text->showAll(to); // blit text over video, if needed
} }

View File

@@ -13,6 +13,7 @@
#include "../../lib/campaign/CampaignScenarioPrologEpilog.h" #include "../../lib/campaign/CampaignScenarioPrologEpilog.h"
class CMultiLineLabel; class CMultiLineLabel;
class VideoWidget;
class CPrologEpilogVideo : public CWindowObject class CPrologEpilogVideo : public CWindowObject
{ {
@@ -25,6 +26,7 @@ class CPrologEpilogVideo : public CWindowObject
std::function<void()> exitCb; std::function<void()> exitCb;
std::shared_ptr<CMultiLineLabel> text; std::shared_ptr<CMultiLineLabel> text;
std::shared_ptr<VideoWidget> videoPlayer;
bool voiceStopped = false; bool voiceStopped = false;

View File

@@ -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 <SDL_mixer.h>
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;
}

22
client/media/CAudioBase.h Normal file
View File

@@ -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();
};

View File

@@ -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<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) override
{
return nullptr;
};
/// Extracts audio data from provided video in wav format
std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) override
{
return {nullptr, 0};
};
};

View File

@@ -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 <SDL_mixer.h>
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<MusicEntry> 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<MusicEntry>(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<uint32_t>(-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;
}

View File

@@ -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<MusicEntry> current;
std::unique_ptr<MusicEntry> 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<MusicEntry> queued);
void musicFinishedCallback() final;
/// map <set name> -> <list of URI's to tracks belonging to the said set>
std::map<std::string, std::vector<AudioPath>> musicsSet;
/// stored position, in seconds at which music player should resume playing this track
std::map<AudioPath, float> 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;
};

View File

@@ -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 <SDL_mixer.h>
#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<std::unique_ptr<ui8[]>, si64> & data, bool cache)
{
try
{
std::vector<ui8> startBytes = std::vector<ui8>(data.first.get(), data.first.get() + std::min(static_cast<si64>(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<int>(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<std::unique_ptr<ui8[]>, 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<soundBase::soundID> & 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<void()> 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<void()> & 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<AudioPath, int> soundsArg)
{
boost::mutex::scoped_lock guard(mutex);
std::vector<AudioPath> 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();
}

View File

@@ -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<Mix_Chunk *, std::unique_ptr<ui8[]>>;
std::map<AudioPath, CachedChunk> soundChunks;
std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
Mix_Chunk * GetSoundChunk(const AudioPath & sound, bool cache);
Mix_Chunk * GetSoundChunk(std::pair<std::unique_ptr<ui8[]>, si64> & data, bool cache);
/// have entry for every currently active channel
/// vector will be empty if callback was not set
std::map<int, std::vector<std::function<void()>>> 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<AudioPath, int> ambientChannels;
std::map<int, int> channelVolumes;
int volume = 0;
void initCallback(int channel, const std::function<void()> & 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<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) final;
int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) final;
void stopSound(int handler) final;
void setCallback(int channel, std::function<void()> function) final;
void resetCallback(int channel) final;
void soundFinishedCallback(int channel) final;
int ambientGetRange() const final;
void ambientUpdateChannels(std::map<AudioPath, int> currentSounds) final;
void ambientStopAllChannels() final;
};

View File

@@ -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 <SDL_render.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
// Define a set of functions to read data
static int lodRead(void * opaque, uint8_t * buf, int size)
{
auto * data = static_cast<CInputStream *>(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<CInputStream *>(opaque);
if(whence & AVSEEK_SIZE)
return data->getSize();
return data->seek(pos);
}
[[noreturn]] static void throwFFmpegError(int errorCode)
{
std::array<char, AV_ERROR_MAX_STRING_SIZE> errorMessage{};
av_strerror(errorCode, errorMessage.data(), errorMessage.size());
throw std::runtime_error(errorMessage.data());
}
static std::unique_ptr<CInputStream> findVideoData(const VideoPath & videoToOpen)
{
if(CResourceHandler::get()->existsResource(videoToOpen))
return CResourceHandler::get()->load(videoToOpen);
auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/");
auto lowQualityVideo = videoToOpen.toType<EResType::VIDEO_LOW_QUALITY>();
auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType<EResType::VIDEO_LOW_QUALITY>();
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<unsigned char *>(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<int> 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<int, std::string> 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<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen)
{
if (!openInput(videoToOpen))
return { nullptr, 0};
openContext();
openCodec(findAudioStream());
const auto * codecpar = getCodecParameters();
std::vector<ui8> 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<ui8*>(&wav);
auto dat = std::make_pair(std::make_unique<ui8[]>(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<int>(1000 * targetFrameTimeSeconds));
auto timePointAfterPresent = boost::chrono::steady_clock::now();
auto timeSpentBusy = boost::chrono::duration_cast<boost::chrono::milliseconds>(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<IVideoInstance> CVideoPlayer::open(const VideoPath & name, bool scaleToScreen)
{
auto result = std::make_unique<CVideoInstance>();
if (!result->openInput(name))
return nullptr;
result->openVideo();
result->prepareOutput(scaleToScreen, false);
result->loadNextFrame(); // prepare 1st frame
return result;
}
std::pair<std::unique_ptr<ui8[]>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
{
CAudioInstance audio;
return audio.extractAudio(videoToOpen);
}
#endif

View File

@@ -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<CInputStream> 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<std::unique_ptr<ui8[]>, 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<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) final;
std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) final;
};
#endif

View File

@@ -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;
};

View File

@@ -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<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) = 0;
virtual int playSoundFromSet(std::vector<soundBase::soundID> & 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<void()> function) = 0;
virtual void resetCallback(int channel) = 0;
virtual void soundFinishedCallback(int channel) = 0;
virtual void ambientUpdateChannels(std::map<AudioPath, int> currentSounds) = 0;
virtual void ambientStopAllChannels() = 0;
virtual int ambientGetRange() const = 0;
};

View File

@@ -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<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) = 0;
/// Extracts audio data from provided video in wav format
virtual std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) = 0;
virtual ~IVideoPlayer() = default;
};

View File

@@ -13,7 +13,6 @@
#include "Images.h" #include "Images.h"
#include "TextControls.h" #include "TextControls.h"
#include "../CMusicHandler.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../battle/BattleInterface.h" #include "../battle/BattleInterface.h"
@@ -23,6 +22,7 @@
#include "../gui/MouseButton.h" #include "../gui/MouseButton.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/InterfaceObjectConfigurable.h" #include "../gui/InterfaceObjectConfigurable.h"
#include "../media/ISoundPlayer.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../render/CAnimation.h" #include "../render/CAnimation.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"

View File

@@ -26,7 +26,6 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CMusicHandler.h"
#include "../../CCallback.h" #include "../../CCallback.h"

View File

@@ -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<void()> & callback)
: VideoWidgetBase(position, video, playAudio)
, callback(callback)
{
}
void VideoWidgetOnce::onPlaybackFinished()
{
callback();
}

View File

@@ -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<IVideoInstance> videoInstance;
std::pair<std::unique_ptr<ui8[]>, 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<void()>);
};
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<void()> callback;
void onPlaybackFinished() final;
public:
VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function<void()> & callback);
};

View File

@@ -18,12 +18,12 @@
#include "CCreatureWindow.h" #include "CCreatureWindow.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../PlayerLocalState.h" #include "../PlayerLocalState.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/IMusicPlayer.h"
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/CGarrisonInt.h" #include "../widgets/CGarrisonInt.h"
@@ -43,6 +43,8 @@
#include "../../CCallback.h" #include "../../CCallback.h"
#include "../../lib/CArtHandler.h" #include "../../lib/CArtHandler.h"
#include "../../lib/CBuildingHandler.h" #include "../../lib/CBuildingHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CSoundBase.h"
#include "../../lib/CCreatureHandler.h" #include "../../lib/CCreatureHandler.h"
#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CGeneralTextHandler.h"
#include "../../lib/GameSettings.h" #include "../../lib/GameSettings.h"

View File

@@ -11,13 +11,13 @@
#include "CPuzzleWindow.h" #include "CPuzzleWindow.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../adventureMap/CResDataBar.h" #include "../adventureMap/CResDataBar.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/TextAlignment.h" #include "../gui/TextAlignment.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../mapView/MapView.h" #include "../mapView/MapView.h"
#include "../media/ISoundPlayer.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/Images.h" #include "../widgets/Images.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"

View File

@@ -19,12 +19,12 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../PlayerLocalState.h" #include "../PlayerLocalState.h"
#include "../CVideoHandler.h"
#include "../battle/BattleInterface.h" #include "../battle/BattleInterface.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../media/IVideoPlayer.h"
#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/CComponent.h" #include "../widgets/CComponent.h"
#include "../widgets/CTextInput.h" #include "../widgets/CTextInput.h"
@@ -524,13 +524,13 @@ void CSpellWindow::setCurrentPage(int value)
void CSpellWindow::turnPageLeft() void CSpellWindow::turnPageLeft()
{ {
if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) 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() void CSpellWindow::turnPageRight()
{ {
if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) 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) void CSpellWindow::keyPressed(EShortcut key)

View File

@@ -16,7 +16,6 @@
#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CGeneralTextHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CVideoHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
@@ -24,6 +23,7 @@
#include "../widgets/Images.h" #include "../widgets/Images.h"
#include "../widgets/Buttons.h" #include "../widgets/Buttons.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
CTutorialWindow::CTutorialWindow(const TutorialMode & m) CTutorialWindow::CTutorialWindow(const TutorialMode & m)
@@ -54,7 +54,10 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m)
void CTutorialWindow::setContent() void CTutorialWindow::setContent()
{ {
video = "tutorial/" + videos[page]; OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
auto video = VideoPath::builtin("tutorial/" + videos[page]);
videoPlayer = std::make_shared<VideoWidget>(Point(30, 120), video, false);
buttonLeft->block(page<1); buttonLeft->block(page<1);
buttonRight->block(page>videos.size() - 2); buttonRight->block(page>videos.size() - 2);
@@ -98,26 +101,3 @@ void CTutorialWindow::previous()
deactivate(); deactivate();
activate(); 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();
}

View File

@@ -15,6 +15,7 @@ class CFilledTexture;
class CButton; class CButton;
class CLabel; class CLabel;
class CMultiLineLabel; class CMultiLineLabel;
class VideoWidget;
enum TutorialMode enum TutorialMode
{ {
@@ -33,8 +34,8 @@ class CTutorialWindow : public CWindowObject
std::shared_ptr<CLabel> labelTitle; std::shared_ptr<CLabel> labelTitle;
std::shared_ptr<CMultiLineLabel> labelInformation; std::shared_ptr<CMultiLineLabel> labelInformation;
std::shared_ptr<VideoWidget> videoPlayer;
std::string video;
std::vector<std::string> videos; std::vector<std::string> videos;
int page; int page;
@@ -47,8 +48,4 @@ class CTutorialWindow : public CWindowObject
public: public:
CTutorialWindow(const TutorialMode & m); CTutorialWindow(const TutorialMode & m);
static void openWindowFirstTime(const TutorialMode & m); static void openWindowFirstTime(const TutorialMode & m);
void show(Canvas & to) override;
void activate() override;
void deactivate() override;
}; };

View File

@@ -25,7 +25,6 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CMusicHandler.h"
#include "../../CCallback.h" #include "../../CCallback.h"

View File

@@ -19,9 +19,7 @@
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../Client.h" #include "../Client.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../CVideoHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/CursorHandler.h" #include "../gui/CursorHandler.h"
@@ -36,6 +34,7 @@
#include "../widgets/Slider.h" #include "../widgets/Slider.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/ObjectLists.h" #include "../widgets/ObjectLists.h"
#include "../widgets/VideoWidget.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/CAnimation.h" #include "../render/CAnimation.h"
@@ -58,6 +57,7 @@
#include "../lib/GameSettings.h" #include "../lib/GameSettings.h"
#include "../lib/CondSh.h" #include "../lib/CondSh.h"
#include "../lib/CSkillHandler.h" #include "../lib/CSkillHandler.h"
#include "../lib/CSoundBase.h"
#include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/Filesystem.h"
#include "../lib/TextOperations.h" #include "../lib/TextOperations.h"
@@ -515,11 +515,11 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func
recruit->block(true); recruit->block(true);
} }
if(LOCPLINT->castleInt) if(LOCPLINT->castleInt)
CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo, false);
else if(const auto * townObj = dynamic_cast<const CGTownInstance *>(TavernObj)) else if(const auto * townObj = dynamic_cast<const CGTownInstance *>(TavernObj))
CCS->videoh->open(townObj->town->clientInfo.tavernVideo); videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), townObj->town->clientInfo.tavernVideo, false);
else else
CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false);
addInvite(); addInvite();
} }
@@ -572,11 +572,6 @@ void CTavernWindow::close()
CStatusbarWindow::close(); CStatusbarWindow::close();
} }
CTavernWindow::~CTavernWindow()
{
CCS->videoh->close();
}
void CTavernWindow::show(Canvas & to) void CTavernWindow::show(Canvas & to)
{ {
CWindowObject::show(to); CWindowObject::show(to);
@@ -600,8 +595,6 @@ void CTavernWindow::show(Canvas & to)
to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2); 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) void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition)

View File

@@ -39,6 +39,7 @@ class CHeroArea;
class CAnimImage; class CAnimImage;
class CFilledTexture; class CFilledTexture;
class IImage; class IImage;
class VideoWidget;
enum class EUserEvent; enum class EUserEvent;
@@ -261,6 +262,7 @@ public:
std::shared_ptr<CLabel> cost; std::shared_ptr<CLabel> cost;
std::shared_ptr<CLabel> heroesForHire; std::shared_ptr<CLabel> heroesForHire;
std::shared_ptr<CTextBox> heroDescription; std::shared_ptr<CTextBox> heroDescription;
std::shared_ptr<VideoWidget> videoPlayer;
std::shared_ptr<CTextBox> rumor; std::shared_ptr<CTextBox> rumor;
@@ -272,7 +274,6 @@ public:
void addInvite(); void addInvite();
CTavernWindow(const CGObjectInstance * TavernObj, const std::function<void()> & onWindowClosed); CTavernWindow(const CGObjectInstance * TavernObj, const std::function<void()> & onWindowClosed);
~CTavernWindow();
void close() override; void close() override;
void recruitb(); void recruitb();

View File

@@ -11,9 +11,10 @@
#include "GeneralOptionsTab.h" #include "GeneralOptionsTab.h"
#include "CGameInfo.h" #include "CGameInfo.h"
#include "CMusicHandler.h"
#include "CPlayerInterface.h" #include "CPlayerInterface.h"
#include "CServerHandler.h" #include "CServerHandler.h"
#include "media/IMusicPlayer.h"
#include "media/ISoundPlayer.h"
#include "render/IScreenHandler.h" #include "render/IScreenHandler.h"
#include "windows/GUIClasses.h" #include "windows/GUIClasses.h"

View File

@@ -68,6 +68,9 @@ struct Options
/// primary IETF language tag /// primary IETF language tag
std::string tagIETF; std::string tagIETF;
/// ISO 639-2 (B) language code
std::string tagISO2;
/// DateTime format /// DateTime format
std::string dateTimeFormat; std::string dateTimeFormat;
@@ -82,27 +85,27 @@ inline const auto & getLanguageList()
{ {
static const std::array<Options, 20> languages static const std::array<Options, 20> languages
{ { { {
{ "czech", "Czech", "Čeština", "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3, true }, { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %T", EPluralForms::CZ_3, true },
{ "chinese", "Chinese", "简体中文", "GBK", "zh", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%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 { "english", "English", "English", "CP1252", "en", "eng", "%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 }, { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %T", EPluralForms::EN_2, true },
{ "french", "French", "Français", "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2, true }, { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %T", EPluralForms::FR_2, true },
{ "german", "German", "Deutsch", "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2, true }, { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %T", EPluralForms::EN_2, true },
{ "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2, true }, { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %T", EPluralForms::EN_2, true },
{ "italian", "Italian", "Italiano", "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2, true }, { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %T", EPluralForms::EN_2, true },
{ "korean", "Korean", "한국어", "CP949", "ko", "%F %T", EPluralForms::VI_1, true }, { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%F %T", EPluralForms::VI_1, true },
{ "polish", "Polish", "Polski", "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3, true }, { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%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 { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
{ "russian", "Russian", "Русский", "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3, true }, { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %T", EPluralForms::UK_3, true },
{ "spanish", "Spanish", "Español", "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2, true }, { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %T", EPluralForms::EN_2, true },
{ "swedish", "Swedish", "Svenska", "CP1252", "sv", "%F %T", EPluralForms::EN_2, true }, { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%F %T", EPluralForms::EN_2, true },
{ "turkish", "Turkish", "Türkçe", "CP1254", "tr", "%d.%m.%Y %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", "%d.%m.%Y %T", EPluralForms::UK_3, true }, { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%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 { "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_cp1250", "Other (East European)", "", "CP1250", "", "", "", EPluralForms::NONE, false },
{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", EPluralForms::NONE, false }, { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", "", EPluralForms::NONE, false },
{ "other_cp1252", "Other (West European)", "", "CP1252", "", "", EPluralForms::NONE, false } { "other_cp1252", "Other (West European)", "", "CP1252", "", "", "", EPluralForms::NONE, false }
} }; } };
static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!"); static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");

View File

@@ -113,11 +113,8 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension)
{".MP3", EResType::SOUND}, {".MP3", EResType::SOUND},
{".OGG", EResType::SOUND}, {".OGG", EResType::SOUND},
{".FLAC", EResType::SOUND}, {".FLAC", EResType::SOUND},
{".SMK", EResType::VIDEO}, {".SMK", EResType::VIDEO_LOW_QUALITY},
{".BIK", EResType::VIDEO}, {".BIK", EResType::VIDEO},
{".MJPG", EResType::VIDEO},
{".MPG", EResType::VIDEO},
{".AVI", EResType::VIDEO},
{".WEBM", EResType::VIDEO}, {".WEBM", EResType::VIDEO},
{".ZIP", EResType::ARCHIVE_ZIP}, {".ZIP", EResType::ARCHIVE_ZIP},
{".LOD", EResType::ARCHIVE_LOD}, {".LOD", EResType::ARCHIVE_LOD},
@@ -157,6 +154,7 @@ std::string EResTypeHelper::getEResTypeAsString(EResType type)
MAP_ENUM(TTF_FONT) MAP_ENUM(TTF_FONT)
MAP_ENUM(IMAGE) MAP_ENUM(IMAGE)
MAP_ENUM(VIDEO) MAP_ENUM(VIDEO)
MAP_ENUM(VIDEO_LOW_QUALITY)
MAP_ENUM(SOUND) MAP_ENUM(SOUND)
MAP_ENUM(ARCHIVE_ZIP) MAP_ENUM(ARCHIVE_ZIP)
MAP_ENUM(ARCHIVE_LOD) MAP_ENUM(ARCHIVE_LOD)

View File

@@ -46,6 +46,7 @@ enum class EResType
TTF_FONT, TTF_FONT,
IMAGE, IMAGE,
VIDEO, VIDEO,
VIDEO_LOW_QUALITY,
SOUND, SOUND,
ARCHIVE_VID, ARCHIVE_VID,
ARCHIVE_ZIP, ARCHIVE_ZIP,

View File

@@ -493,6 +493,7 @@ static std::string imageFile(const JsonNode & node)
static std::string videoFile(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);
TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO_LOW_QUALITY);
return "Video file \"" + node.String() + "\" was not found"; return "Video file \"" + node.String() + "\" was not found";
} }
#undef TEST_FILE #undef TEST_FILE