Implement mouth animation for NPCs based on say sound (Fixes #642)

This commit is contained in:
scrawl 2014-07-29 00:26:26 +02:00
parent 7dfb624ee2
commit 598c0c4ae7
13 changed files with 245 additions and 48 deletions

View File

@ -48,7 +48,7 @@ add_openmw_dir (mwscript
) )
add_openmw_dir (mwsound add_openmw_dir (mwsound
soundmanagerimp openal_output ffmpeg_decoder sound soundmanagerimp openal_output ffmpeg_decoder sound sound_decoder sound_output loudness
) )
add_openmw_dir (mwworld add_openmw_dir (mwworld

View File

@ -101,6 +101,11 @@ namespace MWBase
virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()) = 0; virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()) = 0;
///< Stop an actor speaking ///< Stop an actor speaking
virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const = 0;
///< Check the currently playing say sound for this actor
/// and get an average loudness value (scale [0,1]) at the current time position.
/// If the actor is not saying anything, returns 0.
virtual SoundPtr playTrack(const MWSound::DecoderPtr& decoder, PlayType type) = 0; virtual SoundPtr playTrack(const MWSound::DecoderPtr& decoder, PlayType type) = 0;
///< Play a 2D audio track, using a custom decoder ///< Play a 2D audio track, using a custom decoder

View File

@ -66,15 +66,40 @@ std::string getVampireHead(const std::string& race, bool female)
namespace MWRender namespace MWRender
{ {
HeadAnimationTime::HeadAnimationTime(MWWorld::Ptr reference)
: mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0)
{
}
float HeadAnimationTime::getValue() const float HeadAnimationTime::getValue() const
{ {
// TODO use time from text keys (Talk Start/Stop, Blink Start/Stop)
// TODO: Handle eye blinking // TODO: Handle eye blinking
if (MWBase::Environment::get().getSoundManager()->sayDone(mReference)) if (MWBase::Environment::get().getSoundManager()->sayDone(mReference))
return 0; return mBlinkStop;
else else
// TODO: Use the loudness of the currently playing sound return mTalkStart +
return 1; (mTalkStop - mTalkStart) *
std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*4); // Rescale a bit (most voices are not very loud)
}
void HeadAnimationTime::setTalkStart(float value)
{
mTalkStart = value;
}
void HeadAnimationTime::setTalkStop(float value)
{
mTalkStop = value;
}
void HeadAnimationTime::setBlinkStart(float value)
{
mBlinkStart = value;
}
void HeadAnimationTime::setBlinkStop(float value)
{
mBlinkStop = value;
} }
static NpcAnimation::PartBoneMap createPartListMap() static NpcAnimation::PartBoneMap createPartListMap()
@ -620,7 +645,21 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g
ctrl->setSource(mNullAnimationTimePtr); ctrl->setSource(mNullAnimationTimePtr);
if (type == ESM::PRT_Head) if (type == ESM::PRT_Head)
{
ctrl->setSource(mHeadAnimationTime); ctrl->setSource(mHeadAnimationTime);
const NifOgre::TextKeyMap& keys = mObjectParts[type]->mTextKeys;
for (NifOgre::TextKeyMap::const_iterator it = keys.begin(); it != keys.end(); ++it)
{
if (Misc::StringUtils::ciEqual(it->second, "talk: start"))
mHeadAnimationTime->setTalkStart(it->first);
if (Misc::StringUtils::ciEqual(it->second, "talk: stop"))
mHeadAnimationTime->setTalkStop(it->first);
if (Misc::StringUtils::ciEqual(it->second, "blink: start"))
mHeadAnimationTime->setBlinkStart(it->first);
if (Misc::StringUtils::ciEqual(it->second, "blink: stop"))
mHeadAnimationTime->setBlinkStop(it->first);
}
}
else if (type == ESM::PRT_Weapon) else if (type == ESM::PRT_Weapon)
ctrl->setSource(mWeaponAnimationTime); ctrl->setSource(mWeaponAnimationTime);
} }

View File

@ -19,8 +19,17 @@ class HeadAnimationTime : public Ogre::ControllerValue<Ogre::Real>
{ {
private: private:
MWWorld::Ptr mReference; MWWorld::Ptr mReference;
float mTalkStart;
float mTalkStop;
float mBlinkStart;
float mBlinkStop;
public: public:
HeadAnimationTime(MWWorld::Ptr reference) : mReference(reference) {} HeadAnimationTime(MWWorld::Ptr reference);
void setTalkStart(float value);
void setTalkStop(float value);
void setBlinkStart(float value);
void setBlinkStop(float value);
virtual Ogre::Real getValue() const; virtual Ogre::Real getValue() const;
virtual void setValue(Ogre::Real value) virtual void setValue(Ogre::Real value)

View File

@ -0,0 +1,53 @@
#include "loudness.hpp"
#include "soundmanagerimp.hpp"
namespace MWSound
{
void analyzeLoudness(const std::vector<char> &data, int sampleRate, ChannelConfig chans,
SampleType type, std::vector<float> &out, float valuesPerSecond)
{
int samplesPerSegment = sampleRate / valuesPerSecond;
int numSamples = bytesToFrames(data.size(), chans, type);
int advance = framesToBytes(1, chans, type);
out.reserve(numSamples/samplesPerSegment);
int segment=0;
int sample=0;
while (segment < numSamples/samplesPerSegment)
{
float sum=0;
int samplesAdded = 0;
while (sample < numSamples && sample < (segment+1)*samplesPerSegment)
{
// get sample on a scale from -1 to 1
float value = 0;
if (type == SampleType_UInt8)
value = data[sample*advance]/128.f;
else if (type == SampleType_Int16)
{
value = *reinterpret_cast<const Ogre::int16*>(&data[sample*advance]);
value /= float(std::numeric_limits<Ogre::uint16>().max());
}
else if (type == SampleType_Float32)
{
value = *reinterpret_cast<const float*>(&data[sample*advance]);
value /= std::numeric_limits<float>().max();
}
sum += value*value;
++samplesAdded;
++sample;
}
float rms = 0; // root mean square
if (samplesAdded > 0)
rms = std::sqrt(sum / samplesAdded);
out.push_back(rms);
++segment;
}
}
}

View File

@ -0,0 +1,20 @@
#include "sound_decoder.hpp"
namespace MWSound
{
/**
* Analyzes the energy (closely related to loudness) of a sound buffer.
* The buffer will be divided into segments according to \a valuesPerSecond,
* and for each segment a loudness value in the range of [0,1] will be computed.
* @param data the sound buffer to analyze, containing raw samples
* @param sampleRate the sample rate of the sound buffer
* @param chans channel layout of the buffer
* @param type sample type of the buffer
* @param out Will contain the output loudness values.
* @param valuesPerSecond How many loudness values per second of audio to compute.
*/
void analyzeLoudness (const std::vector<char>& data, int sampleRate, ChannelConfig chans, SampleType type,
std::vector<float>& out, float valuesPerSecond);
}

View File

@ -11,11 +11,16 @@
#include "sound_decoder.hpp" #include "sound_decoder.hpp"
#include "sound.hpp" #include "sound.hpp"
#include "soundmanagerimp.hpp" #include "soundmanagerimp.hpp"
#include "loudness.hpp"
#ifndef ALC_ALL_DEVICES_SPECIFIER #ifndef ALC_ALL_DEVICES_SPECIFIER
#define ALC_ALL_DEVICES_SPECIFIER 0x1013 #define ALC_ALL_DEVICES_SPECIFIER 0x1013
#endif #endif
namespace
{
const int loudnessFPS = 20; // loudness values per second of audio
}
namespace MWSound namespace MWSound
{ {
@ -750,7 +755,7 @@ void OpenAL_Output::deinit()
} }
ALuint OpenAL_Output::getBuffer(const std::string &fname) ALuint OpenAL_Output::getBuffer(const std::string &fname, std::vector<float>* loudnessBuffer)
{ {
ALuint buf = 0; ALuint buf = 0;
@ -765,11 +770,12 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname)
if(iter != mUnusedBuffers.end()) if(iter != mUnusedBuffers.end())
mUnusedBuffers.erase(iter); mUnusedBuffers.erase(iter);
} }
return buf;
} }
throwALerror(); throwALerror();
if (buf != 0 && loudnessBuffer == NULL)
return buf;
std::vector<char> data; std::vector<char> data;
ChannelConfig chans; ChannelConfig chans;
SampleType type; SampleType type;
@ -795,42 +801,50 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname)
decoder->readAll(data); decoder->readAll(data);
decoder->close(); decoder->close();
alGenBuffers(1, &buf); if (loudnessBuffer != NULL)
throwALerror();
alBufferData(buf, format, &data[0], data.size(), srate);
mBufferCache[fname] = buf;
mBufferRefs[buf] = 1;
ALint bufsize = 0;
alGetBufferi(buf, AL_SIZE, &bufsize);
mBufferCacheMemSize += bufsize;
// NOTE: Max buffer cache: 15MB
while(mBufferCacheMemSize > 15*1024*1024)
{ {
if(mUnusedBuffers.empty()) analyzeLoudness(data, srate, chans, type, *loudnessBuffer, loudnessFPS);
}
if (buf == 0)
{
alGenBuffers(1, &buf);
throwALerror();
alBufferData(buf, format, &data[0], data.size(), srate);
mBufferCache[fname] = buf;
mBufferRefs[buf] = 1;
ALint bufsize = 0;
alGetBufferi(buf, AL_SIZE, &bufsize);
mBufferCacheMemSize += bufsize;
// NOTE: Max buffer cache: 15MB
while(mBufferCacheMemSize > 15*1024*1024)
{ {
std::cout <<"No more unused buffers to clear!"<< std::endl; if(mUnusedBuffers.empty())
break; {
std::cout <<"No more unused buffers to clear!"<< std::endl;
break;
}
ALuint oldbuf = mUnusedBuffers.front();
mUnusedBuffers.pop_front();
NameMap::iterator nameiter = mBufferCache.begin();
while(nameiter != mBufferCache.end())
{
if(nameiter->second == oldbuf)
mBufferCache.erase(nameiter++);
else
++nameiter;
}
bufsize = 0;
alGetBufferi(oldbuf, AL_SIZE, &bufsize);
alDeleteBuffers(1, &oldbuf);
mBufferCacheMemSize -= bufsize;
} }
ALuint oldbuf = mUnusedBuffers.front();
mUnusedBuffers.pop_front();
NameMap::iterator nameiter = mBufferCache.begin();
while(nameiter != mBufferCache.end())
{
if(nameiter->second == oldbuf)
mBufferCache.erase(nameiter++);
else
++nameiter;
}
bufsize = 0;
alGetBufferi(oldbuf, AL_SIZE, &bufsize);
alDeleteBuffers(1, &oldbuf);
mBufferCacheMemSize -= bufsize;
} }
return buf; return buf;
} }
@ -883,7 +897,7 @@ MWBase::SoundPtr OpenAL_Output::playSound(const std::string &fname, float vol, f
} }
MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre::Vector3 &pos, float vol, float basevol, float pitch, MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre::Vector3 &pos, float vol, float basevol, float pitch,
float min, float max, int flags, float offset) float min, float max, int flags, float offset, bool extractLoudness)
{ {
boost::shared_ptr<OpenAL_Sound> sound; boost::shared_ptr<OpenAL_Sound> sound;
ALuint src=0, buf=0; ALuint src=0, buf=0;
@ -895,8 +909,12 @@ MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre
try try
{ {
buf = getBuffer(fname); std::vector<float> loudnessVector;
buf = getBuffer(fname, extractLoudness ? &loudnessVector : NULL);
sound.reset(new OpenAL_Sound3D(*this, src, buf, pos, vol, basevol, pitch, min, max, flags)); sound.reset(new OpenAL_Sound3D(*this, src, buf, pos, vol, basevol, pitch, min, max, flags));
sound->setLoudnessVector(loudnessVector, loudnessFPS);
} }
catch(std::exception&) catch(std::exception&)
{ {

View File

@ -36,7 +36,7 @@ namespace MWSound
typedef std::vector<Sound*> SoundVec; typedef std::vector<Sound*> SoundVec;
SoundVec mActiveSounds; SoundVec mActiveSounds;
ALuint getBuffer(const std::string &fname); ALuint getBuffer(const std::string &fname, std::vector<float>* loudnessBuffer=NULL);
void bufferFinished(ALuint buffer); void bufferFinished(ALuint buffer);
Environment mLastEnvironment; Environment mLastEnvironment;
@ -49,7 +49,7 @@ namespace MWSound
virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset); virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset);
/// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts.
virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos,
float vol, float basevol, float pitch, float min, float max, int flags, float offset); float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false);
virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags); virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags);
virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env); virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env);

View File

@ -0,0 +1,23 @@
#include "sound.hpp"
namespace MWSound
{
float Sound::getCurrentLoudness()
{
if (mLoudnessVector.empty())
return 0.f;
int index = getTimeOffset() * mLoudnessFPS;
index = std::max(0, std::min(index, int(mLoudnessVector.size()-1)));
return mLoudnessVector[index];
}
void Sound::setLoudnessVector(const std::vector<float> &loudnessVector, float loudnessFPS)
{
mLoudnessVector = loudnessVector;
mLoudnessFPS = loudnessFPS;
}
}

View File

@ -24,6 +24,9 @@ namespace MWSound
int mFlags; int mFlags;
float mFadeOutTime; float mFadeOutTime;
std::vector<float> mLoudnessVector;
float mLoudnessFPS;
public: public:
virtual void stop() = 0; virtual void stop() = 0;
virtual bool isPlaying() = 0; virtual bool isPlaying() = 0;
@ -31,6 +34,12 @@ namespace MWSound
void setPosition(const Ogre::Vector3 &pos) { mPos = pos; } void setPosition(const Ogre::Vector3 &pos) { mPos = pos; }
void setVolume(float volume) { mVolume = volume; } void setVolume(float volume) { mVolume = volume; }
void setFadeout(float duration) { mFadeOutTime=duration; } void setFadeout(float duration) { mFadeOutTime=duration; }
void setLoudnessVector(const std::vector<float>& loudnessVector, float loudnessFPS);
/// Get loudness at the current time position on a [0,1] scale.
/// Requires that loudnessVector was filled in by the user.
float getCurrentLoudness();
MWBase::SoundManager::PlayType getPlayType() const MWBase::SoundManager::PlayType getPlayType() const
{ return (MWBase::SoundManager::PlayType)(mFlags&MWBase::SoundManager::Play_TypeMask); } { return (MWBase::SoundManager::PlayType)(mFlags&MWBase::SoundManager::Play_TypeMask); }
@ -44,6 +53,7 @@ namespace MWSound
, mMaxDistance(maxdist) , mMaxDistance(maxdist)
, mFlags(flags) , mFlags(flags)
, mFadeOutTime(0) , mFadeOutTime(0)
, mLoudnessFPS(20)
{ } { }
virtual ~Sound() { } virtual ~Sound() { }

View File

@ -28,7 +28,7 @@ namespace MWSound
virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset) = 0; virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset) = 0;
/// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts.
virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos,
float vol, float basevol, float pitch, float min, float max, int flags, float offset) = 0; float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false) = 0;
virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags) = 0; virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags) = 0;
virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env) = 0; virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env) = 0;

View File

@ -256,7 +256,7 @@ namespace MWSound
const Ogre::Vector3 objpos(pos.pos); const Ogre::Vector3 objpos(pos.pos);
MWBase::SoundPtr sound = mOutput->playSound3D(filePath, objpos, 1.0f, basevol, 1.0f, MWBase::SoundPtr sound = mOutput->playSound3D(filePath, objpos, 1.0f, basevol, 1.0f,
20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0); 20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0, true);
mActiveSounds[sound] = std::make_pair(ptr, std::string("_say_sound")); mActiveSounds[sound] = std::make_pair(ptr, std::string("_say_sound"));
} }
catch(std::exception &e) catch(std::exception &e)
@ -265,6 +265,21 @@ namespace MWSound
} }
} }
float SoundManager::getSaySoundLoudness(const MWWorld::Ptr &ptr) const
{
SoundMap::const_iterator snditer = mActiveSounds.begin();
while(snditer != mActiveSounds.end())
{
if(snditer->second.first == ptr && snditer->second.second == "_say_sound")
break;
++snditer;
}
if (snditer == mActiveSounds.end())
return 0.f;
return snditer->first->getCurrentLoudness();
}
void SoundManager::say(const std::string& filename) void SoundManager::say(const std::string& filename)
{ {
if(!mOutput->isInitialized()) if(!mOutput->isInitialized())

View File

@ -110,6 +110,11 @@ namespace MWSound
virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()); virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr());
///< Stop an actor speaking ///< Stop an actor speaking
virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const;
///< Check the currently playing say sound for this actor
/// and get an average loudness value (scale [0,1]) at the current time position.
/// If the actor is not saying anything, returns 0.
virtual MWBase::SoundPtr playTrack(const DecoderPtr& decoder, PlayType type); virtual MWBase::SoundPtr playTrack(const DecoderPtr& decoder, PlayType type);
///< Play a 2D audio track, using a custom decoder ///< Play a 2D audio track, using a custom decoder